/// <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) { Debug.Assert(input != null); Debug.Assert(encryptor != null); Debug.Assert(encryptionOptions != null); Debug.Assert(diagnosticsContext != null); if (string.IsNullOrWhiteSpace(encryptionOptions.DataEncryptionKeyId)) { throw new ArgumentNullException(nameof(encryptionOptions.DataEncryptionKeyId)); } if (string.IsNullOrWhiteSpace(encryptionOptions.EncryptionAlgorithm)) { throw new ArgumentNullException(nameof(encryptionOptions.EncryptionAlgorithm)); } if (encryptionOptions.PathsToEncrypt == null) { throw new ArgumentNullException(nameof(encryptionOptions.PathsToEncrypt)); } if (!encryptionOptions.PathsToEncrypt.Any()) { return(input); } foreach (string path in encryptionOptions.PathsToEncrypt) { if (string.IsNullOrWhiteSpace(path) || path[0] != '/' || path.LastIndexOf('/') != 0) { throw new ArgumentException($"Invalid path {path ?? string.Empty}", nameof(encryptionOptions.PathsToEncrypt)); } } JObject itemJObj = EncryptionProcessor.baseSerializer.FromStream <JObject>(input); JObject toEncryptJObj = new JObject(); foreach (string pathToEncrypt in encryptionOptions.PathsToEncrypt) { string propertyName = pathToEncrypt.Substring(1); JToken propertyValueHolder = itemJObj.Property(propertyName).Value; // Even null in the JSON is a JToken with Type Null, this null check is just a sanity check if (propertyValueHolder != null) { toEncryptJObj.Add(propertyName, propertyValueHolder.Value <JToken>()); itemJObj.Remove(propertyName); } } MemoryStream memoryStream = EncryptionProcessor.baseSerializer.ToStream <JObject>(toEncryptJObj); Debug.Assert(memoryStream != null); Debug.Assert(memoryStream.TryGetBuffer(out _)); byte[] plainText = memoryStream.ToArray(); byte[] 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 encryptionProperties = new EncryptionProperties( encryptionFormatVersion: 2, encryptionOptions.EncryptionAlgorithm, encryptionOptions.DataEncryptionKeyId, encryptedData: cipherText); itemJObj.Add(Constants.EncryptedInfo, JObject.FromObject(encryptionProperties)); input.Dispose(); return(EncryptionProcessor.baseSerializer.ToStream(itemJObj)); }
private async Task <Stream> DeserializeAndDecryptResponseAsync( Stream content, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { JObject contentJObj = EncryptionProcessor.baseSerializer.FromStream <JObject>(content); JArray result = new JArray(); if (!(contentJObj.SelectToken(Constants.DocumentsResourcePropertyName) is JArray documents)) { throw new InvalidOperationException("Feed response Body Contract was violated. Feed response did not have an array of Documents"); } foreach (JToken value in documents) { if (!(value is JObject document)) { result.Add(value); continue; } try { JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( document, this.encryptor, diagnosticsContext, cancellationToken); result.Add(decryptedDocument); } catch (Exception exception) { if (this.decryptionResultHandler == null) { throw; } result.Add(document); MemoryStream memoryStream = EncryptionProcessor.baseSerializer.ToStream(document); Debug.Assert(memoryStream != null); ArraySegment <byte> encryptedStream; bool wasBufferReturned = memoryStream.TryGetBuffer(out encryptedStream); Debug.Assert(wasBufferReturned); this.decryptionResultHandler( DecryptionResult.CreateFailure( encryptedStream, exception)); } } JObject decryptedResponse = new JObject(); foreach (JProperty property in contentJObj.Properties()) { if (property.Name.Equals(Constants.DocumentsResourcePropertyName)) { decryptedResponse.Add(property.Name, (JToken)result); } else { decryptedResponse.Add(property.Name, property.Value); } } return(EncryptionProcessor.baseSerializer.ToStream(decryptedResponse)); }
/// <inheritdoc/> public override async Task <ItemResponse <DataEncryptionKeyProperties> > RewrapDataEncryptionKeyAsync( string id, EncryptionKeyWrapMetadata newWrapMetadata, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { if (newWrapMetadata == null) { throw new ArgumentNullException(nameof(newWrapMetadata)); } CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); (DataEncryptionKeyProperties dekProperties, InMemoryRawDek inMemoryRawDek) = await this.FetchUnwrappedAsync( id, diagnosticsContext, cancellationToken); (byte[] wrappedDek, EncryptionKeyWrapMetadata updatedMetadata, InMemoryRawDek updatedRawDek) = await this.WrapAsync( id, inMemoryRawDek.DataEncryptionKey.RawKey, dekProperties.EncryptionAlgorithm, newWrapMetadata, diagnosticsContext, cancellationToken); if (requestOptions == null) { requestOptions = new ItemRequestOptions(); } requestOptions.IfMatchEtag = dekProperties.ETag; DataEncryptionKeyProperties newDekProperties = new DataEncryptionKeyProperties(dekProperties) { WrappedDataEncryptionKey = wrappedDek, EncryptionKeyWrapMetadata = updatedMetadata, }; ItemResponse <DataEncryptionKeyProperties> response; try { response = await this.DekProvider.Container.ReplaceItemAsync( newDekProperties, newDekProperties.Id, new PartitionKey(newDekProperties.Id), requestOptions, cancellationToken); Debug.Assert(response.Resource != null); } catch (CosmosException ex) { if (!ex.StatusCode.Equals(HttpStatusCode.PreconditionFailed)) { throw; } // Handle if exception is due to etag mismatch. The scenario is as follows - say there are 2 clients A and B that both have the DEK properties cached. // From A, rewrap worked and the DEK is updated. Now from B, rewrap was attempted later based on the cached properties which will fail due to etag mismatch. // To address this, we do an explicit read, which reads the key from storage and updates the cached properties; and then attempt rewrap again. await this.ReadDataEncryptionKeyAsync( newDekProperties.Id, requestOptions, cancellationToken); return(await this.RewrapDataEncryptionKeyAsync( id, newWrapMetadata, requestOptions, cancellationToken)); } this.DekProvider.DekCache.SetDekProperties(id, response.Resource); this.DekProvider.DekCache.SetRawDek(id, updatedRawDek); return(response); }