public override async Task <ItemResponse <T> > UpsertItemAsync <T>( T item, PartitionKey?partitionKey = null, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { if (item == null) { throw new ArgumentNullException(nameof(item)); } if (!(requestOptions is EncryptionItemRequestOptions encryptionItemRequestOptions) || encryptionItemRequestOptions.EncryptionOptions == null) { return(await this.container.UpsertItemAsync( item, partitionKey, requestOptions, cancellationToken)); } if (partitionKey == null) { throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); } CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("UpsertItem")) { ResponseMessage responseMessage; if (item is EncryptableItem encryptableItem) { using (Stream streamPayload = encryptableItem.ToStream(this.CosmosSerializer)) { responseMessage = await this.UpsertItemHelperAsync( streamPayload, partitionKey.Value, requestOptions, decryptResponse : false, diagnosticsContext, cancellationToken); } encryptableItem.SetDecryptableItem( EncryptionProcessor.BaseSerializer.FromStream <JObject>(responseMessage.Content), this.Encryptor, this.CosmosSerializer); return(new EncryptionItemResponse <T>( responseMessage, item)); } else { using (Stream itemStream = this.CosmosSerializer.ToStream <T>(item)) { responseMessage = await this.UpsertItemHelperAsync( itemStream, partitionKey.Value, requestOptions, decryptResponse : true, diagnosticsContext, cancellationToken); } return(this.ResponseFactory.CreateItemResponse <T>(responseMessage)); } } }
/// <remarks> /// If there isn't any PathsToEncrypt, input stream will be returned without any modification. /// Else input stream will be disposed, and a new stream is returned. /// In case of an exception, input stream won't be disposed, but position will be end of stream. /// </remarks> public static async Task <Stream> EncryptAsync( Stream input, Encryptor encryptor, EncryptionOptions encryptionOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { EncryptionProcessor.ValidateInputForEncrypt( input, encryptor, encryptionOptions); if (!encryptionOptions.PathsToEncrypt.Any()) { return(input); } if (!encryptionOptions.PathsToEncrypt.Distinct().SequenceEqual(encryptionOptions.PathsToEncrypt)) { throw new InvalidOperationException("Duplicate paths in PathsToEncrypt passed via EncryptionOptions."); } foreach (string path in encryptionOptions.PathsToEncrypt) { if (string.IsNullOrWhiteSpace(path) || path[0] != '/' || path.LastIndexOf('/') != 0) { throw new InvalidOperationException($"Invalid path {path ?? string.Empty}, {nameof(encryptionOptions.PathsToEncrypt)}"); } if (string.Equals(path.Substring(1), "id")) { throw new InvalidOperationException($"{nameof(encryptionOptions.PathsToEncrypt)} includes a invalid path: '{path}'."); } } JObject itemJObj = EncryptionProcessor.BaseSerializer.FromStream <JObject>(input); List <string> pathsEncrypted = new List <string>(); EncryptionProperties encryptionProperties = null; byte[] plainText = null; byte[] cipherText = null; TypeMarker typeMarker; switch (encryptionOptions.EncryptionAlgorithm) { case CosmosEncryptionAlgorithm.MdeAeadAes256CbcHmac256Randomized: foreach (string pathToEncrypt in encryptionOptions.PathsToEncrypt) { string propertyName = pathToEncrypt.Substring(1); if (!itemJObj.TryGetValue(propertyName, out JToken propertyValue)) { continue; } if (propertyValue.Type == JTokenType.Null) { continue; } (typeMarker, plainText) = EncryptionProcessor.Serialize(propertyValue); cipherText = await encryptor.EncryptAsync( plainText, encryptionOptions.DataEncryptionKeyId, encryptionOptions.EncryptionAlgorithm); if (cipherText == null) { throw new InvalidOperationException($"{nameof(Encryptor)} returned null cipherText from {nameof(EncryptAsync)}."); } byte[] cipherTextWithTypeMarker = new byte[cipherText.Length + 1]; cipherTextWithTypeMarker[0] = (byte)typeMarker; Buffer.BlockCopy(cipherText, 0, cipherTextWithTypeMarker, 1, cipherText.Length); itemJObj[propertyName] = cipherTextWithTypeMarker; pathsEncrypted.Add(pathToEncrypt); } encryptionProperties = new EncryptionProperties( encryptionFormatVersion: 3, encryptionOptions.EncryptionAlgorithm, encryptionOptions.DataEncryptionKeyId, encryptedData: null, pathsEncrypted); break; case CosmosEncryptionAlgorithm.AEAes256CbcHmacSha256Randomized: JObject toEncryptJObj = new JObject(); foreach (string pathToEncrypt in encryptionOptions.PathsToEncrypt) { string propertyName = pathToEncrypt.Substring(1); if (!itemJObj.TryGetValue(propertyName, out JToken propertyValue)) { continue; } toEncryptJObj.Add(propertyName, propertyValue.Value <JToken>()); itemJObj.Remove(propertyName); } MemoryStream memoryStream = EncryptionProcessor.BaseSerializer.ToStream <JObject>(toEncryptJObj); Debug.Assert(memoryStream != null); Debug.Assert(memoryStream.TryGetBuffer(out _)); plainText = memoryStream.ToArray(); cipherText = await encryptor.EncryptAsync( plainText, encryptionOptions.DataEncryptionKeyId, encryptionOptions.EncryptionAlgorithm, cancellationToken); if (cipherText == null) { throw new InvalidOperationException($"{nameof(Encryptor)} returned null cipherText from {nameof(EncryptAsync)}."); } encryptionProperties = new EncryptionProperties( encryptionFormatVersion: 2, encryptionOptions.EncryptionAlgorithm, encryptionOptions.DataEncryptionKeyId, encryptedData: cipherText, encryptionOptions.PathsToEncrypt); break; default: throw new NotSupportedException($"Encryption Algorithm : {encryptionOptions.EncryptionAlgorithm} is not supported."); } itemJObj.Add(Constants.EncryptedInfo, JObject.FromObject(encryptionProperties)); input.Dispose(); return(EncryptionProcessor.BaseSerializer.ToStream(itemJObj)); }