From 9f14c48edb1c027bd5e1470a9f8ffdf3fec94253 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Thu, 11 Mar 2021 08:00:03 +0530 Subject: [PATCH 01/27] Remove caching of AeadAes256CbcHmac256EncryptionAlgorithm object --- .../src/CachedEncryptionSettings.cs | 24 --- .../src/EncryptionProcessor.cs | 137 ++++--------- .../src/EncryptionSettings.cs | 185 ++++++++---------- .../src/QueryDefinitionExtensions.cs | 8 +- .../tests/EmulatorTests/MdeEncryptionTests.cs | 44 +++-- .../Contracts/DotNetPreviewSDKAPI.json | 4 +- 6 files changed, 161 insertions(+), 241 deletions(-) delete mode 100644 Microsoft.Azure.Cosmos.Encryption/src/CachedEncryptionSettings.cs diff --git a/Microsoft.Azure.Cosmos.Encryption/src/CachedEncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/CachedEncryptionSettings.cs deleted file mode 100644 index 9684bb6920..0000000000 --- a/Microsoft.Azure.Cosmos.Encryption/src/CachedEncryptionSettings.cs +++ /dev/null @@ -1,24 +0,0 @@ -//------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -//------------------------------------------------------------ - -namespace Microsoft.Azure.Cosmos.Encryption -{ - using System; - using System.Diagnostics; - - internal sealed class CachedEncryptionSettings - { - public EncryptionSettings EncryptionSettings { get; } - - public DateTime EncryptionSettingsExpiryUtc { get; } - - public CachedEncryptionSettings( - EncryptionSettings encryptionSettings, - DateTime encryptionSettingsExpiryUtc) - { - this.EncryptionSettings = encryptionSettings ?? throw new ArgumentNullException(nameof(encryptionSettings)); - this.EncryptionSettingsExpiryUtc = encryptionSettingsExpiryUtc; - } - } -} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs index 3d4171c2ea..7b86e23df0 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs @@ -9,11 +9,9 @@ namespace Microsoft.Azure.Cosmos.Encryption using System.Diagnostics; using System.IO; using System.Linq; - using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; - using global::Azure; using Microsoft.Data.Encryption.Cryptography; using Microsoft.Data.Encryption.Cryptography.Serializers; using Newtonsoft.Json; @@ -85,90 +83,23 @@ internal async Task InitializeEncryptionSettingsAsync(CancellationToken cancella return; } - Dictionary settingsByDekId = new Dictionary(); - // update the property level setting. foreach (ClientEncryptionIncludedPath propertyToEncrypt in this.ClientEncryptionPolicy.IncludedPaths) { - if (!settingsByDekId.ContainsKey(propertyToEncrypt.ClientEncryptionKeyId)) - { - string clientEncryptionKeyId = propertyToEncrypt.ClientEncryptionKeyId; - - ClientEncryptionKeyProperties clientEncryptionKeyProperties = await this.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: clientEncryptionKeyId, - container: this.Container, - cancellationToken: cancellationToken, - shouldForceRefresh: false); + EncryptionType encryptionType = this.EncryptionSettings.GetEncryptionTypeForProperty(propertyToEncrypt); - ProtectedDataEncryptionKey protectedDataEncryptionKey = null; - - try - { - // we pull out the Encrypted Data Encryption Key and build the Protected Data Encryption key - // Here a request is sent out to unwrap using the Master Key configured via the Key Encryption Key. - protectedDataEncryptionKey = this.EncryptionSettings.BuildProtectedDataEncryptionKey( - clientEncryptionKeyProperties, - this.EncryptionKeyStoreProvider, - clientEncryptionKeyId); - } - catch (RequestFailedException ex) - { - // The access to master key was probably revoked. Try to fetch the latest ClientEncryptionKeyProperties from the backend. - // This will succeed provided the user has rewraped the Client Encryption Key with right set of meta data. - // This is based on the AKV provider implementaion so we expect a RequestFailedException in case other providers are used in unwrap implementation. - if (ex.Status == (int)HttpStatusCode.Forbidden) - { - clientEncryptionKeyProperties = await this.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: clientEncryptionKeyId, - container: this.Container, - cancellationToken: cancellationToken, - shouldForceRefresh: true); - - // just bail out if this fails. - protectedDataEncryptionKey = this.EncryptionSettings.BuildProtectedDataEncryptionKey( - clientEncryptionKeyProperties, - this.EncryptionKeyStoreProvider, - clientEncryptionKeyId); - } - else - { - throw; - } - } - - settingsByDekId[clientEncryptionKeyId] = new EncryptionSettings - { - // we cache the setting for performance reason. - EncryptionSettingTimeToLive = DateTime.UtcNow + TimeSpan.FromMinutes(Constants.CachedEncryptionSettingsDefaultTTLInMinutes), - ClientEncryptionKeyId = clientEncryptionKeyId, - DataEncryptionKey = protectedDataEncryptionKey, - }; - } - - EncryptionType encryptionType = EncryptionType.Plaintext; - switch (propertyToEncrypt.EncryptionType) + EncryptionSettings encryptionSettings = new EncryptionSettings { - case CosmosEncryptionType.Deterministic: - encryptionType = EncryptionType.Deterministic; - break; - case CosmosEncryptionType.Randomized: - encryptionType = EncryptionType.Randomized; - break; - case CosmosEncryptionType.Plaintext: - encryptionType = EncryptionType.Plaintext; - break; - default: - throw new ArgumentException($"Invalid encryption type {propertyToEncrypt.EncryptionType}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); - } + ClientEncryptionKeyId = propertyToEncrypt.ClientEncryptionKeyId, + EncryptionType = encryptionType, + }; string propertyName = propertyToEncrypt.Path.Substring(1); this.EncryptionSettings.SetEncryptionSettingForProperty( propertyName, EncryptionSettings.Create( - settingsByDekId[propertyToEncrypt.ClientEncryptionKeyId], - encryptionType), - settingsByDekId[propertyToEncrypt.ClientEncryptionKeyId].EncryptionSettingTimeToLive); + encryptionSettings)); } this.isEncryptionSettingsInitDone = true; @@ -209,7 +140,7 @@ internal async Task InitEncryptionSettingsIfNotInitializedAsync(CancellationToke private void EncryptProperty( JObject itemJObj, JToken propertyValue, - EncryptionSettings settings) + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm) { /* Top Level can be an Object*/ if (propertyValue.Type == JTokenType.Object) @@ -221,11 +152,11 @@ private void EncryptProperty( this.EncryptProperty( itemJObj, jProperty.Value, - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } else { - jProperty.Value = this.SerializeAndEncryptValue(jProperty.Value, settings); + jProperty.Value = this.SerializeAndEncryptValue(jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -245,13 +176,13 @@ private void EncryptProperty( this.EncryptProperty( itemJObj, jProperty.Value, - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } // primitive type else { - jProperty.Value = this.SerializeAndEncryptValue(jProperty.Value, settings); + jProperty.Value = this.SerializeAndEncryptValue(jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -270,13 +201,13 @@ private void EncryptProperty( this.EncryptProperty( itemJObj, jArray[i], - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } // primitive type else { - jArray[i] = this.SerializeAndEncryptValue(jArray[i], settings); + jArray[i] = this.SerializeAndEncryptValue(jArray[i], aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -287,7 +218,7 @@ private void EncryptProperty( { for (int i = 0; i < propertyValue.Count(); i++) { - propertyValue[i] = this.SerializeAndEncryptValue(propertyValue[i], settings); + propertyValue[i] = this.SerializeAndEncryptValue(propertyValue[i], aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -296,13 +227,13 @@ private void EncryptProperty( { itemJObj.Property(propertyValue.Path).Value = this.SerializeAndEncryptValue( itemJObj.Property(propertyValue.Path).Value, - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } } private JToken SerializeAndEncryptValue( JToken jToken, - EncryptionSettings settings) + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm) { JToken propertyValueToEncrypt = jToken; @@ -313,11 +244,11 @@ private JToken SerializeAndEncryptValue( (TypeMarker typeMarker, byte[] plainText) = Serialize(propertyValueToEncrypt); - byte[] cipherText = settings.AeadAes256CbcHmac256EncryptionAlgorithm.Encrypt(plainText); + byte[] cipherText = aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt(plainText); if (cipherText == null) { - throw new InvalidOperationException($"{nameof(this.SerializeAndEncryptValue)} returned null cipherText from {nameof(settings.AeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. "); + throw new InvalidOperationException($"{nameof(this.SerializeAndEncryptValue)} returned null cipherText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. "); } byte[] cipherTextWithTypeMarker = new byte[cipherText.Length + 1]; @@ -369,10 +300,12 @@ public async Task EncryptAsync( throw new ArgumentException($"Invalid Encryption Setting for the Property:{propertyName}. "); } + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settings.BuildEncryptionAlgorithmForSettingAsync(this, cancellationToken: default); + this.EncryptProperty( itemJObj, propertyValue, - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } input.Dispose(); @@ -381,7 +314,7 @@ public async Task EncryptAsync( private JToken DecryptAndDeserializeValue( JToken jToken, - EncryptionSettings settings) + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm) { byte[] cipherTextWithTypeMarker = jToken.ToObject(); @@ -393,11 +326,11 @@ private JToken DecryptAndDeserializeValue( byte[] cipherText = new byte[cipherTextWithTypeMarker.Length - 1]; Buffer.BlockCopy(cipherTextWithTypeMarker, 1, cipherText, 0, cipherTextWithTypeMarker.Length - 1); - byte[] plainText = settings.AeadAes256CbcHmac256EncryptionAlgorithm.Decrypt(cipherText); + byte[] plainText = aeadAes256CbcHmac256EncryptionAlgorithm.Decrypt(cipherText); if (plainText == null) { - throw new InvalidOperationException($"{nameof(this.DecryptAndDeserializeValue)} returned null plainText from {nameof(settings.AeadAes256CbcHmac256EncryptionAlgorithm.Decrypt)}. "); + throw new InvalidOperationException($"{nameof(this.DecryptAndDeserializeValue)} returned null plainText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Decrypt)}. "); } return DeserializeAndAddProperty( @@ -407,7 +340,7 @@ private JToken DecryptAndDeserializeValue( private void DecryptProperty( JObject itemJObj, - EncryptionSettings settings, + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm, string propertyName, JToken propertyValue) { @@ -419,7 +352,7 @@ private void DecryptProperty( { this.DecryptProperty( itemJObj, - settings, + aeadAes256CbcHmac256EncryptionAlgorithm, jProperty.Name, jProperty.Value); } @@ -427,7 +360,7 @@ private void DecryptProperty( { jProperty.Value = this.DecryptAndDeserializeValue( jProperty.Value, - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -445,7 +378,7 @@ private void DecryptProperty( { this.DecryptProperty( itemJObj, - settings, + aeadAes256CbcHmac256EncryptionAlgorithm, jProperty.Name, jProperty.Value); } @@ -453,7 +386,7 @@ private void DecryptProperty( { jProperty.Value = this.DecryptAndDeserializeValue( jProperty.Value, - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -469,7 +402,7 @@ private void DecryptProperty( { this.DecryptProperty( itemJObj, - settings, + aeadAes256CbcHmac256EncryptionAlgorithm, jArray[i].Path, jArray[i]); } @@ -477,7 +410,7 @@ private void DecryptProperty( { jArray[i] = this.DecryptAndDeserializeValue( jArray[i], - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -490,7 +423,7 @@ private void DecryptProperty( { propertyValue[i] = this.DecryptAndDeserializeValue( propertyValue[i], - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -499,7 +432,7 @@ private void DecryptProperty( { itemJObj.Property(propertyName).Value = this.DecryptAndDeserializeValue( itemJObj.Property(propertyName).Value, - settings); + aeadAes256CbcHmac256EncryptionAlgorithm); } } @@ -522,9 +455,11 @@ private async Task DecryptObjectAsync( throw new ArgumentException($"Invalid Encryption Setting for Property:{propertyName}. "); } + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settings.BuildEncryptionAlgorithmForSettingAsync(this, cancellationToken: default); + this.DecryptProperty( document, - settings, + aeadAes256CbcHmac256EncryptionAlgorithm, propertyName, propertyValue); } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index a1f7c80dac..5961279b2b 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -5,7 +5,6 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; - using System.Diagnostics; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -14,16 +13,10 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionSettings { - internal AsyncCache EncryptionSettingCacheByPropertyName { get; } = new AsyncCache(); + internal AsyncCache EncryptionSettingCacheByPropertyName { get; } = new AsyncCache(); public string ClientEncryptionKeyId { get; set; } - public DateTime EncryptionSettingTimeToLive { get; set; } - - internal DataEncryptionKey DataEncryptionKey { get; set; } - - internal AeadAes256CbcHmac256EncryptionAlgorithm AeadAes256CbcHmac256EncryptionAlgorithm { get; set; } - public EncryptionType EncryptionType { get; set; } public EncryptionSettings() @@ -35,36 +28,21 @@ internal async Task GetEncryptionSettingForPropertyAsync( EncryptionProcessor encryptionProcessor, CancellationToken cancellationToken) { - CachedEncryptionSettings cachedEncryptionSettings = await this.EncryptionSettingCacheByPropertyName.GetAsync( + EncryptionSettings encryptionSettings = await this.EncryptionSettingCacheByPropertyName.GetAsync( propertyName, obsoleteValue: null, - async () => await this.FetchCachedEncryptionSettingsAsync(propertyName, encryptionProcessor, cancellationToken), + async () => await this.FetchEncryptionSettingForPropertyAsync(propertyName, encryptionProcessor, cancellationToken), cancellationToken); - if (cachedEncryptionSettings == null) + if (encryptionSettings == null) { return null; } - // we just cache the algo for the property for a duration of 1 hour and when it expires we try to fetch the cached Encrypted key - // from the Cosmos Client and try to create a Protected Data Encryption Key which tries to unwrap the key. - // 1) Try to check if the KEK has been revoked may be post rotation. If the request fails this could mean the KEK was revoked, - // the user might have rewraped the Key and that is when we try to force fetch it from the Backend. - // So we only read back from the backend only when an operation like wrap/unwrap with the Master Key fails. - if (cachedEncryptionSettings.EncryptionSettingsExpiryUtc <= DateTime.UtcNow) - { - cachedEncryptionSettings = await this.EncryptionSettingCacheByPropertyName.GetAsync( - propertyName, - obsoleteValue: null, - async () => await this.FetchCachedEncryptionSettingsAsync(propertyName, encryptionProcessor, cancellationToken), - cancellationToken, - forceRefresh: true); - } - - return cachedEncryptionSettings.EncryptionSettings; + return encryptionSettings; } - private async Task FetchCachedEncryptionSettingsAsync( + private async Task FetchEncryptionSettingForPropertyAsync( string propertyName, EncryptionProcessor encryptionProcessor, CancellationToken cancellationToken) @@ -80,69 +58,15 @@ private async Task FetchCachedEncryptionSettingsAsync( { if (string.Equals(propertyToEncrypt.Path.Substring(1), propertyName)) { - ClientEncryptionKeyProperties clientEncryptionKeyProperties = await encryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: propertyToEncrypt.ClientEncryptionKeyId, - container: encryptionProcessor.Container, - cancellationToken: cancellationToken, - shouldForceRefresh: false); - - ProtectedDataEncryptionKey protectedDataEncryptionKey = null; - - try - { - protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( - clientEncryptionKeyProperties, - encryptionProcessor.EncryptionKeyStoreProvider, - propertyToEncrypt.ClientEncryptionKeyId); - } - catch (RequestFailedException ex) - { - // the key was revoked. Try to fetch the latest EncryptionKeyProperties from the backend. - // This should succeed provided the user has rewraped the key with right set of meta data. - if (ex.Status == (int)HttpStatusCode.Forbidden) - { - clientEncryptionKeyProperties = await encryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: propertyToEncrypt.ClientEncryptionKeyId, - container: encryptionProcessor.Container, - cancellationToken: cancellationToken, - shouldForceRefresh: true); - - protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( - clientEncryptionKeyProperties, - encryptionProcessor.EncryptionKeyStoreProvider, - propertyToEncrypt.ClientEncryptionKeyId); - } - else - { - throw; - } - } + EncryptionType encryptionType = this.GetEncryptionTypeForProperty(propertyToEncrypt); EncryptionSettings encryptionSettings = new EncryptionSettings { - EncryptionSettingTimeToLive = DateTime.UtcNow + TimeSpan.FromMinutes(Constants.CachedEncryptionSettingsDefaultTTLInMinutes), ClientEncryptionKeyId = propertyToEncrypt.ClientEncryptionKeyId, - DataEncryptionKey = protectedDataEncryptionKey, + EncryptionType = encryptionType, }; - EncryptionType encryptionType = EncryptionType.Plaintext; - switch (propertyToEncrypt.EncryptionType) - { - case CosmosEncryptionType.Deterministic: - encryptionType = EncryptionType.Deterministic; - break; - case CosmosEncryptionType.Randomized: - encryptionType = EncryptionType.Randomized; - break; - case CosmosEncryptionType.Plaintext: - encryptionType = EncryptionType.Plaintext; - break; - default: - throw new ArgumentException($"Invalid encryption type {propertyToEncrypt.EncryptionType}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); - } - - encryptionSettings = EncryptionSettings.Create(encryptionSettings, encryptionType); - return new CachedEncryptionSettings(encryptionSettings, encryptionSettings.EncryptionSettingTimeToLive); + return EncryptionSettings.Create(encryptionSettings); } } } @@ -150,6 +74,74 @@ private async Task FetchCachedEncryptionSettingsAsync( return null; } + internal EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPath clientEncryptionIncludedPath) + { + switch (clientEncryptionIncludedPath.EncryptionType) + { + case CosmosEncryptionType.Deterministic: + return EncryptionType.Deterministic; + case CosmosEncryptionType.Randomized: + return EncryptionType.Randomized; + case CosmosEncryptionType.Plaintext: + return EncryptionType.Plaintext; + default: + throw new ArgumentException($"Invalid encryption type {clientEncryptionIncludedPath.EncryptionType}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); + } + } + + internal async Task BuildEncryptionAlgorithmForSettingAsync( + EncryptionProcessor encryptionProcessor, + CancellationToken cancellationToken) + { + ClientEncryptionKeyProperties clientEncryptionKeyProperties = await encryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( + clientEncryptionKeyId: this.ClientEncryptionKeyId, + container: encryptionProcessor.Container, + cancellationToken: cancellationToken, + shouldForceRefresh: false); + + ProtectedDataEncryptionKey protectedDataEncryptionKey; + + try + { + // we pull out the Encrypted Data Encryption Key and build the Protected Data Encryption key + // Here a request is sent out to unwrap using the Master Key configured via the Key Encryption Key. + protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( + clientEncryptionKeyProperties, + encryptionProcessor.EncryptionKeyStoreProvider, + this.ClientEncryptionKeyId); + } + catch (RequestFailedException ex) + { + // The access to master key was probably revoked. Try to fetch the latest ClientEncryptionKeyProperties from the backend. + // This will succeed provided the user has rewraped the Client Encryption Key with right set of meta data. + // This is based on the AKV provider implementaion so we expect a RequestFailedException in case other providers are used in unwrap implementation. + if (ex.Status == (int)HttpStatusCode.Forbidden) + { + clientEncryptionKeyProperties = await encryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( + clientEncryptionKeyId: this.ClientEncryptionKeyId, + container: encryptionProcessor.Container, + cancellationToken: cancellationToken, + shouldForceRefresh: true); + + // just bail out if this fails. + protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( + clientEncryptionKeyProperties, + encryptionProcessor.EncryptionKeyStoreProvider, + this.ClientEncryptionKeyId); + } + else + { + throw; + } + } + + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = new AeadAes256CbcHmac256EncryptionAlgorithm( + protectedDataEncryptionKey, + this.EncryptionType); + + return aeadAes256CbcHmac256EncryptionAlgorithm; + } + internal ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( ClientEncryptionKeyProperties clientEncryptionKeyProperties, EncryptionKeyStoreProvider encryptionKeyStoreProvider, @@ -160,31 +152,28 @@ internal ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Value, encryptionKeyStoreProvider); - return new ProtectedDataEncryptionKey( + ProtectedDataEncryptionKey protectedDataEncryptionKey = ProtectedDataEncryptionKey.GetOrCreate( keyId, keyEncryptionKey, clientEncryptionKeyProperties.WrappedDataEncryptionKey); + + protectedDataEncryptionKey.TimeToLive = (TimeSpan)keyEncryptionKey.KeyStoreProvider.DataEncryptionKeyCacheTimeToLive; + + return protectedDataEncryptionKey; } - internal void SetEncryptionSettingForProperty(string propertyName, EncryptionSettings encryptionSettings, DateTime expiryUtc) + internal void SetEncryptionSettingForProperty(string propertyName, EncryptionSettings encryptionSettings) { - CachedEncryptionSettings cachedEncryptionSettings = new CachedEncryptionSettings(encryptionSettings, expiryUtc); - this.EncryptionSettingCacheByPropertyName.Set(propertyName, cachedEncryptionSettings); + this.EncryptionSettingCacheByPropertyName.Set(propertyName, encryptionSettings); } internal static EncryptionSettings Create( - EncryptionSettings settingsForKey, - EncryptionType encryptionType) + EncryptionSettings settings) { return new EncryptionSettings() { - ClientEncryptionKeyId = settingsForKey.ClientEncryptionKeyId, - DataEncryptionKey = settingsForKey.DataEncryptionKey, - EncryptionType = encryptionType, - EncryptionSettingTimeToLive = settingsForKey.EncryptionSettingTimeToLive, - AeadAes256CbcHmac256EncryptionAlgorithm = AeadAes256CbcHmac256EncryptionAlgorithm.GetOrCreate( - settingsForKey.DataEncryptionKey, - encryptionType), + ClientEncryptionKeyId = settings.ClientEncryptionKeyId, + EncryptionType = settings.EncryptionType, }; } } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs index 0994412c7b..62d66a7334 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs @@ -94,6 +94,10 @@ public static async Task AddParameterAsync( return queryDefinitionwithEncryptedValues; } + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settings.BuildEncryptionAlgorithmForSettingAsync( + encryptionContainer.EncryptionProcessor, + cancellationToken: default); + if (settings.EncryptionType == EncryptionType.Randomized) { throw new ArgumentException($"Unsupported argument with Path: {path} for query. For executing queries on encrypted path requires the use of deterministic encryption type. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); @@ -102,11 +106,11 @@ public static async Task AddParameterAsync( JToken propertyValueToEncrypt = EncryptionProcessor.BaseSerializer.FromStream(valueStream); (EncryptionProcessor.TypeMarker typeMarker, byte[] serializedData) = EncryptionProcessor.Serialize(propertyValueToEncrypt); - byte[] cipherText = settings.AeadAes256CbcHmac256EncryptionAlgorithm.Encrypt(serializedData); + byte[] cipherText = aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt(serializedData); if (cipherText == null) { - throw new InvalidOperationException($"{nameof(AddParameterAsync)} returned null cipherText from {nameof(settings.AeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); + throw new InvalidOperationException($"{nameof(AddParameterAsync)} returned null cipherText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); } byte[] cipherTextWithTypeMarker = new byte[cipherText.Length + 1]; diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index bc0f6ac6e1..c7f2d30d7d 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -111,7 +111,7 @@ await MdeEncryptionTests.CreateClientEncryptionKeyAsync( new ClientEncryptionIncludedPath() { Path = "/Sensitive_BoolFormat", - ClientEncryptionKeyId = "key1", + ClientEncryptionKeyId = "key2", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -135,7 +135,15 @@ await MdeEncryptionTests.CreateClientEncryptionKeyAsync( new ClientEncryptionIncludedPath() { Path = "/Sensitive_IntMultiDimArray", - ClientEncryptionKeyId = "key1", + ClientEncryptionKeyId = "key2", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_Dict", + ClientEncryptionKeyId = "key2", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1029,7 +1037,7 @@ public async Task ValidateCachingofProtectedDataEncryptionKey() await MdeEncryptionTests.MdeCreateItemAsync(MdeEncryptionTests.encryptionContainer); testEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(metadata1.Value, out int unwrapcount); - Assert.AreEqual(2, unwrapcount); + Assert.AreEqual(1, unwrapcount); } private static async Task ValidateQueryResultsMultipleDocumentsAsync( Container container, @@ -1412,6 +1420,8 @@ public class TestDoc public Sensitive_NestedObjectL1 Sensitive_NestedObjectFormatL1 { get; set; } + public Dictionary Sensitive_Dict { get; set; } + public TestDoc() { } @@ -1540,10 +1550,11 @@ public static TestDoc Create(string partitionKey = null) Sensitive_DateFormat = new DateTime(1987, 12, 25), Sensitive_DecimalFormat = 472.3108m, Sensitive_IntArray = new int[2] { 999, 1000 }, - Sensitive_IntMultiDimArray = new [,] { { 1,2},{ 2,3}, { 4,5} }, + Sensitive_IntMultiDimArray = new[,] { { 1, 2 }, { 2, 3 }, { 4, 5 } }, Sensitive_IntFormat = 1965, Sensitive_BoolFormat = true, Sensitive_FloatFormat = 8923.124f, + Sensitive_Dict = new Dictionary() { { "key", "value"} }, Sensitive_ArrayFormat = new Sensitive_ArrayData[] { new Sensitive_ArrayData @@ -1697,18 +1708,23 @@ public override byte[] UnwrapKey(string masterKeyPath, KeyEncryptionKeyAlgorithm throw new RequestFailedException((int)HttpStatusCode.Forbidden, "Forbidden"); } - if (!this.UnWrapKeyCallsCount.ContainsKey(masterKeyPath)) - { - this.UnWrapKeyCallsCount[masterKeyPath] = 1; - } - else + return this.GetOrCreateDataEncryptionKey(encryptedKey.ToHexString(), DecryptEncryptionKey); + + byte[] DecryptEncryptionKey() { - this.UnWrapKeyCallsCount[masterKeyPath]++; - } + if (!this.UnWrapKeyCallsCount.ContainsKey(masterKeyPath)) + { + this.UnWrapKeyCallsCount[masterKeyPath] = 1; + } + else + { + this.UnWrapKeyCallsCount[masterKeyPath]++; + } - this.keyinfo.TryGetValue(masterKeyPath, out int moveBy); - byte[] plainkey = encryptedKey.Select(b => (byte)(b - moveBy)).ToArray(); - return plainkey; + this.keyinfo.TryGetValue(masterKeyPath, out int moveBy); + byte[] plainkey = encryptedKey.Select(b => (byte)(b - moveBy)).ToArray(); + return plainkey; + } } public override byte[] WrapKey(string masterKeyPath, KeyEncryptionKeyAlgorithm encryptionAlgorithm, byte[] key) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index 441015f883..bd90453c30 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -817,10 +817,10 @@ "Attributes": [], "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.EncryptionKeyWrapMetadata), Void .ctor(Microsoft.Azure.Cosmos.EncryptionKeyWrapMetadata)]" }, - "Void .ctor(System.String, System.String)": { + "Void .ctor(System.String, System.String, System.String, System.String)": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(System.String, System.String), Void .ctor(System.String, System.String)]" + "MethodInfo": "[Void .ctor(System.String, System.String, System.String, System.String), Void .ctor(System.String, System.String, System.String, System.String)]" } }, "NestedTypes": {} From 7fa67b625946e65a1f6a4e18f597d1dbbb1080b2 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Thu, 11 Mar 2021 08:08:34 +0530 Subject: [PATCH 02/27] Update DotNetPreviewSDKAPI.json --- .../Contracts/DotNetPreviewSDKAPI.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index bd90453c30..441015f883 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -817,10 +817,10 @@ "Attributes": [], "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.EncryptionKeyWrapMetadata), Void .ctor(Microsoft.Azure.Cosmos.EncryptionKeyWrapMetadata)]" }, - "Void .ctor(System.String, System.String, System.String, System.String)": { + "Void .ctor(System.String, System.String)": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(System.String, System.String, System.String, System.String), Void .ctor(System.String, System.String, System.String, System.String)]" + "MethodInfo": "[Void .ctor(System.String, System.String), Void .ctor(System.String, System.String)]" } }, "NestedTypes": {} From e695aa834bd88db40d16f30bf77591d795b18d2c Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Mon, 29 Mar 2021 10:04:08 +0530 Subject: [PATCH 03/27] Updates to latest Cryptography package.Test updates. --- .../Custom/CosmosDataEncryptionKeyProvider.cs | 20 +++++++++++-- .../MdeServices/MdeEncryptionAlgorithm.cs | 2 -- .../src/EncryptionCosmosClientExtensions.cs | 3 ++ .../src/EncryptionDatabaseExtensions.cs | 6 ++++ .../src/EncryptionSettings.cs | 2 -- .../Microsoft.Azure.Cosmos.Encryption.csproj | 2 +- .../EmulatorTests/MdeCustomEncryptionTests.cs | 24 ++++++++-------- .../tests/EmulatorTests/MdeEncryptionTests.cs | 28 ++++++++++++++----- 8 files changed, 60 insertions(+), 27 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs b/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs index 927ff6f4c8..8af78fdc2a 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs @@ -23,7 +23,7 @@ public sealed class CosmosDataEncryptionKeyProvider : DataEncryptionKeyProvider internal DekCache DekCache { get; } - /* MDE's Protected Data Encryption key Cache TTL*/ + // MDE's Protected Data Encryption key Cache TTL. internal TimeSpan? PdekCacheTimeToLive { get; } internal Container Container @@ -74,7 +74,9 @@ public CosmosDataEncryptionKeyProvider( /// Initializes a new instance of the class. /// /// MDE EncryptionKeyStoreProvider for Wrapping/UnWrapping services. - /// Time to live for EncryptionKeyStoreProvider's ProtectedDataEncryptionKey before having to refresh. 0 results in no Caching. + /// Time to live for EncryptionKeyStoreProvider's ProtectedDataEncryptionKey before having to refresh. 0 results in no Caching. + /// Note: If cacheTimeToLive is null, ProtectedDataEncryptionKey TimeToLive is a static field and will be carried over from previous value set to cacheTimeToLive if any else + /// set to Default TimeToToLive of ProtectedDataEncryptionKey. /// Time to live for DEK properties before having to refresh. public CosmosDataEncryptionKeyProvider( EncryptionKeyStoreProvider encryptionKeyStoreProvider, @@ -86,6 +88,11 @@ public CosmosDataEncryptionKeyProvider( this.dataEncryptionKeyContainerCore = new DataEncryptionKeyContainerCore(this); this.DekCache = new DekCache(dekPropertiesTimeToLive); this.PdekCacheTimeToLive = cacheTimeToLive; + if (this.PdekCacheTimeToLive.HasValue) + { + // set the TTL for Protected Data Encryption. + ProtectedDataEncryptionKey.TimeToLive = (TimeSpan)this.PdekCacheTimeToLive; + } } /// @@ -93,7 +100,9 @@ public CosmosDataEncryptionKeyProvider( /// /// A provider that will be used to wrap (encrypt) and unwrap (decrypt) data encryption keys for envelope based encryption /// MDE EncryptionKeyStoreProvider for Wrapping/UnWrapping services. - /// Time to live for EncryptionKeyStoreProvider ProtectedDataEncryptionKey before having to refresh. 0 results in no Caching. + /// Time to live for EncryptionKeyStoreProvider ProtectedDataEncryptionKey before having to refresh. 0 results in no Caching. + /// Note: If cacheTimeToLive is null, ProtectedDataEncryptionKey TimeToLive is a static field and will be carried over from previous value set to cacheTimeToLive if any else + /// set to Default TimeToToLive of ProtectedDataEncryptionKey. /// Time to live for DEK properties before having to refresh. public CosmosDataEncryptionKeyProvider( EncryptionKeyWrapProvider encryptionKeyWrapProvider, @@ -107,6 +116,11 @@ public CosmosDataEncryptionKeyProvider( this.dataEncryptionKeyContainerCore = new DataEncryptionKeyContainerCore(this); this.DekCache = new DekCache(dekPropertiesTimeToLive); this.PdekCacheTimeToLive = cacheTimeToLive; + if (this.PdekCacheTimeToLive.HasValue) + { + // set the TTL for Protected Data Encryption. + ProtectedDataEncryptionKey.TimeToLive = (TimeSpan)this.PdekCacheTimeToLive; + } } /// diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Custom/MdeServices/MdeEncryptionAlgorithm.cs b/Microsoft.Azure.Cosmos.Encryption/src/Custom/MdeServices/MdeEncryptionAlgorithm.cs index 3dd0c7f3fb..d9144a5c4f 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Custom/MdeServices/MdeEncryptionAlgorithm.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/Custom/MdeServices/MdeEncryptionAlgorithm.cs @@ -66,8 +66,6 @@ public MdeEncryptionAlgorithm( dekProperties.Id, keyEncryptionKey, dekProperties.WrappedDataEncryptionKey); - - protectedDataEncryptionKey.TimeToLive = cacheTimeToLive.Value; } } else diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs index 681a9c04e6..af7195966c 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs @@ -32,6 +32,9 @@ public static CosmosClient WithEncryption( throw new ArgumentNullException(nameof(cosmosClient)); } + // set the TTL for ProtectedDataEncryption at the Encryption CosmosClient Init so that we have a uniform expiry of the KeyStoreProvider and ProtectedDataEncryption cache items. + ProtectedDataEncryptionKey.TimeToLive = (TimeSpan)encryptionKeyStoreProvider.DataEncryptionKeyCacheTimeToLive; + return new EncryptionCosmosClient(cosmosClient, encryptionKeyStoreProvider); } } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabaseExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabaseExtensions.cs index 674e18ca49..7070bfed79 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabaseExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabaseExtensions.cs @@ -85,6 +85,12 @@ public static async Task CreateClientEncryptionKeyA byte[] wrappedDataEncryptionKey = protectedDataEncryptionKey.EncryptedValue; + // cache it. + ProtectedDataEncryptionKey.GetOrCreate( + clientEncryptionKeyId, + keyEncryptionKey, + wrappedDataEncryptionKey); + ClientEncryptionKeyProperties clientEncryptionKeyProperties = new ClientEncryptionKeyProperties( clientEncryptionKeyId, encryptionAlgorithm, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index 5961279b2b..d15efd59d7 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -157,8 +157,6 @@ internal ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( keyEncryptionKey, clientEncryptionKeyProperties.WrappedDataEncryptionKey); - protectedDataEncryptionKey.TimeToLive = (TimeSpan)keyEncryptionKey.KeyStoreProvider.DataEncryptionKeyCacheTimeToLive; - return protectedDataEncryptionKey; } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj b/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj index 6a0afb628c..a4fded3678 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj +++ b/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj @@ -29,7 +29,7 @@ - + diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs index fefec62605..348eb35e3b 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs @@ -55,7 +55,7 @@ public static async Task ClassInitialize(TestContext context) { _ = context; MdeCustomEncryptionTests.testKeyStoreProvider = new TestEncryptionKeyStoreProvider(); - MdeCustomEncryptionTests.dekProvider = new CosmosDataEncryptionKeyProvider(new TestKeyWrapProvider(),MdeCustomEncryptionTests.testKeyStoreProvider, cacheTimeToLive: TimeSpan.FromSeconds(3600)); + MdeCustomEncryptionTests.dekProvider = new CosmosDataEncryptionKeyProvider(new TestKeyWrapProvider(),MdeCustomEncryptionTests.testKeyStoreProvider); MdeCustomEncryptionTests.encryptor = new TestEncryptor(MdeCustomEncryptionTests.dekProvider); MdeCustomEncryptionTests.client = TestCommon.CreateCosmosClient(); @@ -70,7 +70,7 @@ public static async Task ClassInitialize(TestContext context) /*For Legacy Compatibility*/ MdeCustomEncryptionTests.legacytestKeyWrapProvider = new TestKeyWrapProvider(); - MdeCustomEncryptionTests.dualDekProvider = new CosmosDataEncryptionKeyProvider(legacytestKeyWrapProvider, MdeCustomEncryptionTests.testKeyStoreProvider,cacheTimeToLive: TimeSpan.FromSeconds(0)); + MdeCustomEncryptionTests.dualDekProvider = new CosmosDataEncryptionKeyProvider(legacytestKeyWrapProvider, MdeCustomEncryptionTests.testKeyStoreProvider); await MdeCustomEncryptionTests.dualDekProvider.InitializeAsync(MdeCustomEncryptionTests.database, MdeCustomEncryptionTests.keyContainer.Id); MdeCustomEncryptionTests.legacyDekProperties = await MdeCustomEncryptionTests.CreateLegacyDekAsync(MdeCustomEncryptionTests.dualDekProvider, MdeCustomEncryptionTests.legacydekId); MdeCustomEncryptionTests.encryptorWithDualWrapProvider = new TestEncryptor(MdeCustomEncryptionTests.dualDekProvider); @@ -582,16 +582,16 @@ await MdeCustomEncryptionTests.ValidateQueryResultsAsync( [TestMethod] public async Task ValidateCachingofProtectedDataEncryptionKey() - { - TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider(); + { string dekId = "pDekCache"; DataEncryptionKeyProperties dekProperties = await MdeCustomEncryptionTests.CreateDekAsync(MdeCustomEncryptionTests.dualDekProvider, dekId); Assert.AreEqual( new EncryptionKeyWrapMetadata(name: "metadata1", value: MdeCustomEncryptionTests.metadata1.Value), dekProperties.EncryptionKeyWrapMetadata); - // Caching for 30 min. - CosmosDataEncryptionKeyProvider dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider, TimeSpan.FromMinutes(30)); + TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider(); + // default 2 hours. + CosmosDataEncryptionKeyProvider dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider); await dekProvider.InitializeAsync(MdeCustomEncryptionTests.database, MdeCustomEncryptionTests.keyContainer.Id); TestEncryptor encryptor = new TestEncryptor(dekProvider); @@ -602,9 +602,9 @@ public async Task ValidateCachingofProtectedDataEncryptionKey() testEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(masterKeyUri1.ToString(), out int unwrapcount); Assert.AreEqual(1, unwrapcount); + // Caching for 30 min. testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider(); - // No caching - dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider, cacheTimeToLive: TimeSpan.FromSeconds(0)); + dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider, TimeSpan.FromMinutes(30)); await dekProvider.InitializeAsync(MdeCustomEncryptionTests.database, MdeCustomEncryptionTests.keyContainer.Id); encryptor = new TestEncryptor(dekProvider); @@ -613,11 +613,11 @@ public async Task ValidateCachingofProtectedDataEncryptionKey() await MdeCustomEncryptionTests.CreateItemAsync(encryptionContainer, dekId, TestDoc.PathsToEncrypt); testEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(masterKeyUri1.ToString(), out unwrapcount); - Assert.AreEqual(32, unwrapcount); + Assert.AreEqual(1, unwrapcount); + // No caching testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider(); - // default 2 hours. - dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider); + dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider, cacheTimeToLive: TimeSpan.FromSeconds(0)); await dekProvider.InitializeAsync(MdeCustomEncryptionTests.database, MdeCustomEncryptionTests.keyContainer.Id); encryptor = new TestEncryptor(dekProvider); @@ -626,7 +626,7 @@ public async Task ValidateCachingofProtectedDataEncryptionKey() await MdeCustomEncryptionTests.CreateItemAsync(encryptionContainer, dekId, TestDoc.PathsToEncrypt); testEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(masterKeyUri1.ToString(), out unwrapcount); - Assert.AreEqual(1, unwrapcount); + Assert.AreEqual(32, unwrapcount); } [TestMethod] diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index 1045e3bd80..7dbbcff767 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -831,14 +831,26 @@ public async Task EncryptionRestrictedProperties() [TestMethod] public async Task VerifyKekRevokeHandling() { + CosmosClient clientWithNoCaching = TestCommon.CreateCosmosClient(builder => builder + .Build()); + + TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider + { + DataEncryptionKeyCacheTimeToLive = TimeSpan.Zero + }; + + CosmosClient encryptionCosmosClientWithBulk = clientWithNoCaching.WithEncryption(testEncryptionKeyStoreProvider); + Database database = encryptionCosmosClientWithBulk.GetDatabase(MdeEncryptionTests.database.Id); + // Once a Dek gets cached and the Kek is revoked, calls to unwrap/wrap keys would fail since KEK is revoked. // The Dek should be rewrapped if the KEK is revoked. // When an access to KeyVault fails, the Dek is fetched from the backend(force refresh to update the stale DEK) and cache is updated. - EncryptionKeyWrapMetadata revokedKekmetadata = new EncryptionKeyWrapMetadata("revokedKek", "revokedKek-metadata"); - - await MdeEncryptionTests.CreateClientEncryptionKeyAsync( - "keywithRevokedKek", - revokedKekmetadata); + EncryptionKeyWrapMetadata revokedKekmetadata = new EncryptionKeyWrapMetadata("revokedKek", "revokedKek-metadata"); + + await database.CreateClientEncryptionKeyAsync( + "keywithRevokedKek", + DataEncryptionKeyAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256, + revokedKekmetadata); ClientEncryptionIncludedPath pathwithRevokedKek = new ClientEncryptionIncludedPath() { @@ -855,9 +867,9 @@ await MdeEncryptionTests.CreateClientEncryptionKeyAsync( ContainerProperties containerProperties = new ContainerProperties(Guid.NewGuid().ToString(), "/PK") { ClientEncryptionPolicy = clientEncryptionPolicyWithRevokedKek }; Container encryptionContainer = await database.CreateContainerAsync(containerProperties, 400); - - TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider = MdeEncryptionTests.testEncryptionKeyStoreProvider; + testEncryptionKeyStoreProvider.RevokeAccessSet = true; + // try creating it and it should fail as it has been revoked. try { @@ -870,6 +882,7 @@ await MdeEncryptionTests.CreateClientEncryptionKeyAsync( // for unwrap to succeed testEncryptionKeyStoreProvider.RevokeAccessSet = false; + // lets rewrap it. await database.RewrapClientEncryptionKeyAsync("keywithRevokedKek", MdeEncryptionTests.metadata2); @@ -877,6 +890,7 @@ await MdeEncryptionTests.CreateClientEncryptionKeyAsync( // Should fail but will try to fetch the lastest from the Backend and updates the cache. await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); testEncryptionKeyStoreProvider.RevokeAccessSet = false; + testEncryptionKeyStoreProvider.DataEncryptionKeyCacheTimeToLive = TimeSpan.FromMinutes(120); } [TestMethod] From 86ed4b57b9bbcb36421b0b4cf35a4a3a571ca631 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Mon, 29 Mar 2021 10:53:12 +0530 Subject: [PATCH 04/27] Minor Refactoring. --- .../src/EncryptionProcessor.cs | 1 - .../src/QueryDefinitionExtensions.cs | 8 ++++---- .../tests/EmulatorTests/MdeEncryptionTests.cs | 11 ----------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs index 7b86e23df0..5047dd6390 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs @@ -5,7 +5,6 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; - using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; diff --git a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs index 62d66a7334..bef254114b 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs @@ -94,15 +94,15 @@ public static async Task AddParameterAsync( return queryDefinitionwithEncryptedValues; } - AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settings.BuildEncryptionAlgorithmForSettingAsync( - encryptionContainer.EncryptionProcessor, - cancellationToken: default); - if (settings.EncryptionType == EncryptionType.Randomized) { throw new ArgumentException($"Unsupported argument with Path: {path} for query. For executing queries on encrypted path requires the use of deterministic encryption type. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); } + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settings.BuildEncryptionAlgorithmForSettingAsync( + encryptionContainer.EncryptionProcessor, + cancellationToken: default); + JToken propertyValueToEncrypt = EncryptionProcessor.BaseSerializer.FromStream(valueStream); (EncryptionProcessor.TypeMarker typeMarker, byte[] serializedData) = EncryptionProcessor.Serialize(propertyValueToEncrypt); diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index 7dbbcff767..da04ca65b4 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -139,14 +139,6 @@ await MdeEncryptionTests.CreateClientEncryptionKeyAsync( EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, - - new ClientEncryptionIncludedPath() - { - Path = "/Sensitive_Dict", - ClientEncryptionKeyId = "key2", - EncryptionType = "Deterministic", - EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", - }, }; @@ -1450,8 +1442,6 @@ public class TestDoc public Sensitive_NestedObjectL1 Sensitive_NestedObjectFormatL1 { get; set; } - public Dictionary Sensitive_Dict { get; set; } - public TestDoc() { } @@ -1584,7 +1574,6 @@ public static TestDoc Create(string partitionKey = null) Sensitive_IntFormat = 1965, Sensitive_BoolFormat = true, Sensitive_FloatFormat = 8923.124f, - Sensitive_Dict = new Dictionary() { { "key", "value"} }, Sensitive_ArrayFormat = new Sensitive_ArrayData[] { new Sensitive_ArrayData From 5a02a6cfa4b8da985112fa641faa1c7ee7276640 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Mon, 29 Mar 2021 17:58:46 +0530 Subject: [PATCH 05/27] Fixes as per review comments. --- .../Custom/CosmosDataEncryptionKeyProvider.cs | 26 ++-- .../src/EncryptionCosmosClientExtensions.cs | 10 +- .../src/EncryptionProcessor.cs | 27 ++-- .../src/EncryptionSettingForProperty.cs | 98 +++++++++++++ .../src/EncryptionSettings.cs | 131 +++--------------- .../src/QueryDefinitionExtensions.cs | 11 +- .../EmulatorTests/MdeCustomEncryptionTests.cs | 24 +++- .../tests/EmulatorTests/MdeEncryptionTests.cs | 5 +- .../Contracts/DotNetSDKEncryptionAPI.json | 16 +-- 9 files changed, 190 insertions(+), 158 deletions(-) create mode 100644 Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs b/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs index 8af78fdc2a..5a377b1fe9 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs @@ -74,24 +74,25 @@ public CosmosDataEncryptionKeyProvider( /// Initializes a new instance of the class. /// /// MDE EncryptionKeyStoreProvider for Wrapping/UnWrapping services. - /// Time to live for EncryptionKeyStoreProvider's ProtectedDataEncryptionKey before having to refresh. 0 results in no Caching. - /// Note: If cacheTimeToLive is null, ProtectedDataEncryptionKey TimeToLive is a static field and will be carried over from previous value set to cacheTimeToLive if any else - /// set to Default TimeToToLive of ProtectedDataEncryptionKey. /// Time to live for DEK properties before having to refresh. public CosmosDataEncryptionKeyProvider( EncryptionKeyStoreProvider encryptionKeyStoreProvider, - TimeSpan? cacheTimeToLive = null, TimeSpan? dekPropertiesTimeToLive = null) { this.EncryptionKeyStoreProvider = encryptionKeyStoreProvider ?? throw new ArgumentNullException(nameof(encryptionKeyStoreProvider)); this.MdeKeyWrapProvider = new MdeKeyWrapProvider(encryptionKeyStoreProvider); this.dataEncryptionKeyContainerCore = new DataEncryptionKeyContainerCore(this); this.DekCache = new DekCache(dekPropertiesTimeToLive); - this.PdekCacheTimeToLive = cacheTimeToLive; + this.PdekCacheTimeToLive = this.EncryptionKeyStoreProvider.DataEncryptionKeyCacheTimeToLive; if (this.PdekCacheTimeToLive.HasValue) { // set the TTL for Protected Data Encryption. - ProtectedDataEncryptionKey.TimeToLive = (TimeSpan)this.PdekCacheTimeToLive; + ProtectedDataEncryptionKey.TimeToLive = this.PdekCacheTimeToLive.Value; + } + else + { + // arbitrarily large caching period. + ProtectedDataEncryptionKey.TimeToLive = TimeSpan.FromDays(36500); } } @@ -100,14 +101,10 @@ public CosmosDataEncryptionKeyProvider( /// /// A provider that will be used to wrap (encrypt) and unwrap (decrypt) data encryption keys for envelope based encryption /// MDE EncryptionKeyStoreProvider for Wrapping/UnWrapping services. - /// Time to live for EncryptionKeyStoreProvider ProtectedDataEncryptionKey before having to refresh. 0 results in no Caching. - /// Note: If cacheTimeToLive is null, ProtectedDataEncryptionKey TimeToLive is a static field and will be carried over from previous value set to cacheTimeToLive if any else - /// set to Default TimeToToLive of ProtectedDataEncryptionKey. /// Time to live for DEK properties before having to refresh. public CosmosDataEncryptionKeyProvider( EncryptionKeyWrapProvider encryptionKeyWrapProvider, EncryptionKeyStoreProvider encryptionKeyStoreProvider, - TimeSpan? cacheTimeToLive = null, TimeSpan? dekPropertiesTimeToLive = null) { this.EncryptionKeyWrapProvider = encryptionKeyWrapProvider ?? throw new ArgumentNullException(nameof(encryptionKeyWrapProvider)); @@ -115,11 +112,16 @@ public CosmosDataEncryptionKeyProvider( this.MdeKeyWrapProvider = new MdeKeyWrapProvider(encryptionKeyStoreProvider); this.dataEncryptionKeyContainerCore = new DataEncryptionKeyContainerCore(this); this.DekCache = new DekCache(dekPropertiesTimeToLive); - this.PdekCacheTimeToLive = cacheTimeToLive; + this.PdekCacheTimeToLive = this.EncryptionKeyStoreProvider.DataEncryptionKeyCacheTimeToLive; if (this.PdekCacheTimeToLive.HasValue) { // set the TTL for Protected Data Encryption. - ProtectedDataEncryptionKey.TimeToLive = (TimeSpan)this.PdekCacheTimeToLive; + ProtectedDataEncryptionKey.TimeToLive = this.PdekCacheTimeToLive.Value; + } + else + { + // arbitrarily large caching period. + ProtectedDataEncryptionKey.TimeToLive = TimeSpan.FromDays(36500); } } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs index af7195966c..765ffc36d5 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs @@ -33,7 +33,15 @@ public static CosmosClient WithEncryption( } // set the TTL for ProtectedDataEncryption at the Encryption CosmosClient Init so that we have a uniform expiry of the KeyStoreProvider and ProtectedDataEncryption cache items. - ProtectedDataEncryptionKey.TimeToLive = (TimeSpan)encryptionKeyStoreProvider.DataEncryptionKeyCacheTimeToLive; + if (encryptionKeyStoreProvider.DataEncryptionKeyCacheTimeToLive.HasValue) + { + ProtectedDataEncryptionKey.TimeToLive = encryptionKeyStoreProvider.DataEncryptionKeyCacheTimeToLive.Value; + } + else + { + // arbitrarily large caching period. + ProtectedDataEncryptionKey.TimeToLive = TimeSpan.FromDays(36500); + } return new EncryptionCosmosClient(cosmosClient, encryptionKeyStoreProvider); } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs index 5047dd6390..383a19e87a 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs @@ -5,6 +5,7 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; + using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -51,7 +52,7 @@ public EncryptionProcessor( this.Container = container ?? throw new ArgumentNullException(nameof(container)); this.EncryptionCosmosClient = encryptionCosmosClient ?? throw new ArgumentNullException(nameof(encryptionCosmosClient)); this.isEncryptionSettingsInitDone = false; - this.EncryptionSettings = new EncryptionSettings(); + this.EncryptionSettings = new EncryptionSettings(this); } /// @@ -87,18 +88,16 @@ internal async Task InitializeEncryptionSettingsAsync(CancellationToken cancella { EncryptionType encryptionType = this.EncryptionSettings.GetEncryptionTypeForProperty(propertyToEncrypt); - EncryptionSettings encryptionSettings = new EncryptionSettings - { - ClientEncryptionKeyId = propertyToEncrypt.ClientEncryptionKeyId, - EncryptionType = encryptionType, - }; + EncryptionSettingForProperty encryptionSettingsForProperty = new EncryptionSettingForProperty( + propertyToEncrypt.ClientEncryptionKeyId, + encryptionType, + this); string propertyName = propertyToEncrypt.Path.Substring(1); this.EncryptionSettings.SetEncryptionSettingForProperty( propertyName, - EncryptionSettings.Create( - encryptionSettings)); + encryptionSettingsForProperty); } this.isEncryptionSettingsInitDone = true; @@ -292,14 +291,14 @@ public async Task EncryptAsync( continue; } - EncryptionSettings settings = await this.EncryptionSettings.GetEncryptionSettingForPropertyAsync(propertyName, this, cancellationToken); + EncryptionSettingForProperty settingforProperty = await this.EncryptionSettings.GetEncryptionSettingForPropertyAsync(propertyName,cancellationToken); - if (settings == null) + if (settingforProperty == null) { throw new ArgumentException($"Invalid Encryption Setting for the Property:{propertyName}. "); } - AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settings.BuildEncryptionAlgorithmForSettingAsync(this, cancellationToken: default); + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingforProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); this.EncryptProperty( itemJObj, @@ -447,14 +446,14 @@ private async Task DecryptObjectAsync( if (document.TryGetValue(path.Path.Substring(1), out JToken propertyValue)) { string propertyName = path.Path.Substring(1); - EncryptionSettings settings = await this.EncryptionSettings.GetEncryptionSettingForPropertyAsync(propertyName, this, cancellationToken); + EncryptionSettingForProperty settingsForProperty = await this.EncryptionSettings.GetEncryptionSettingForPropertyAsync(propertyName, cancellationToken); - if (settings == null) + if (settingsForProperty == null) { throw new ArgumentException($"Invalid Encryption Setting for Property:{propertyName}. "); } - AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settings.BuildEncryptionAlgorithmForSettingAsync(this, cancellationToken: default); + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingsForProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); this.DecryptProperty( document, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs new file mode 100644 index 0000000000..2aed12a903 --- /dev/null +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs @@ -0,0 +1,98 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.Encryption +{ + using System; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using global::Azure; + using Microsoft.Data.Encryption.Cryptography; + + internal sealed class EncryptionSettingForProperty + { + public string ClientEncryptionKeyId { get; } + + public EncryptionType EncryptionType { get; } + + public EncryptionProcessor EncryptionProcessor { get; } + + public EncryptionSettingForProperty(string clientEncryptionKeyId, EncryptionType encryptionType, EncryptionProcessor encryptionProcessor) + { + this.ClientEncryptionKeyId = clientEncryptionKeyId ?? throw new ArgumentNullException(nameof(clientEncryptionKeyId)); + this.EncryptionType = encryptionType; + this.EncryptionProcessor = encryptionProcessor ?? throw new ArgumentNullException(nameof(encryptionProcessor)); + } + + internal async Task BuildEncryptionAlgorithmForSettingAsync(CancellationToken cancellationToken) + { + ClientEncryptionKeyProperties clientEncryptionKeyProperties = await this.EncryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( + clientEncryptionKeyId: this.ClientEncryptionKeyId, + container: this.EncryptionProcessor.Container, + cancellationToken: cancellationToken, + shouldForceRefresh: false); + + ProtectedDataEncryptionKey protectedDataEncryptionKey; + + try + { + // we pull out the Encrypted Data Encryption Key and build the Protected Data Encryption key + // Here a request is sent out to unwrap using the Master Key configured via the Key Encryption Key. + protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( + clientEncryptionKeyProperties, + this.EncryptionProcessor.EncryptionKeyStoreProvider, + this.ClientEncryptionKeyId); + } + catch (RequestFailedException ex) + { + // The access to master key was probably revoked. Try to fetch the latest ClientEncryptionKeyProperties from the backend. + // This will succeed provided the user has rewraped the Client Encryption Key with right set of meta data. + // This is based on the AKV provider implementaion so we expect a RequestFailedException in case other providers are used in unwrap implementation. + if (ex.Status == (int)HttpStatusCode.Forbidden) + { + clientEncryptionKeyProperties = await this.EncryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( + clientEncryptionKeyId: this.ClientEncryptionKeyId, + container: this.EncryptionProcessor.Container, + cancellationToken: cancellationToken, + shouldForceRefresh: true); + + // just bail out if this fails. + protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( + clientEncryptionKeyProperties, + this.EncryptionProcessor.EncryptionKeyStoreProvider, + this.ClientEncryptionKeyId); + } + else + { + throw; + } + } + + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = new AeadAes256CbcHmac256EncryptionAlgorithm( + protectedDataEncryptionKey, + this.EncryptionType); + + return aeadAes256CbcHmac256EncryptionAlgorithm; + } + + internal ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( + ClientEncryptionKeyProperties clientEncryptionKeyProperties, + EncryptionKeyStoreProvider encryptionKeyStoreProvider, + string keyId) + { + KeyEncryptionKey keyEncryptionKey = KeyEncryptionKey.GetOrCreate( + clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Name, + clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Value, + encryptionKeyStoreProvider); + + ProtectedDataEncryptionKey protectedDataEncryptionKey = ProtectedDataEncryptionKey.GetOrCreate( + keyId, + keyEncryptionKey, + clientEncryptionKeyProperties.WrappedDataEncryptionKey); + + return protectedDataEncryptionKey; + } + } +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index d15efd59d7..6922be863e 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -5,52 +5,47 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; - using System.Net; using System.Threading; using System.Threading.Tasks; - using global::Azure; using Microsoft.Data.Encryption.Cryptography; internal sealed class EncryptionSettings { - internal AsyncCache EncryptionSettingCacheByPropertyName { get; } = new AsyncCache(); + internal AsyncCache EncryptionSettingCacheByPropertyName { get; } = new AsyncCache(); - public string ClientEncryptionKeyId { get; set; } + public EncryptionProcessor EncryptionProcessor { get; } - public EncryptionType EncryptionType { get; set; } - - public EncryptionSettings() + public EncryptionSettings(EncryptionProcessor encryptionProcessor) { + this.EncryptionProcessor = encryptionProcessor ?? throw new ArgumentNullException(nameof(encryptionProcessor)); } - internal async Task GetEncryptionSettingForPropertyAsync( + internal async Task GetEncryptionSettingForPropertyAsync( string propertyName, - EncryptionProcessor encryptionProcessor, CancellationToken cancellationToken) { - EncryptionSettings encryptionSettings = await this.EncryptionSettingCacheByPropertyName.GetAsync( + EncryptionSettingForProperty encryptionSettingsForProperty = await this.EncryptionSettingCacheByPropertyName.GetAsync( propertyName, obsoleteValue: null, - async () => await this.FetchEncryptionSettingForPropertyAsync(propertyName, encryptionProcessor, cancellationToken), + async () => await this.FetchEncryptionSettingForPropertyAsync(propertyName, cancellationToken), cancellationToken); - if (encryptionSettings == null) + if (encryptionSettingsForProperty == null) { return null; } - return encryptionSettings; + return encryptionSettingsForProperty; } - private async Task FetchEncryptionSettingForPropertyAsync( + private async Task FetchEncryptionSettingForPropertyAsync( string propertyName, - EncryptionProcessor encryptionProcessor, CancellationToken cancellationToken) { - ClientEncryptionPolicy clientEncryptionPolicy = await encryptionProcessor.EncryptionCosmosClient.GetClientEncryptionPolicyAsync( - encryptionProcessor.Container, - cancellationToken, - false); + ClientEncryptionPolicy clientEncryptionPolicy = await this.EncryptionProcessor.EncryptionCosmosClient.GetClientEncryptionPolicyAsync( + this.EncryptionProcessor.Container, + cancellationToken: cancellationToken, + shouldForceRefresh: false); if (clientEncryptionPolicy != null) { @@ -60,13 +55,10 @@ private async Task FetchEncryptionSettingForPropertyAsync( { EncryptionType encryptionType = this.GetEncryptionTypeForProperty(propertyToEncrypt); - EncryptionSettings encryptionSettings = new EncryptionSettings - { - ClientEncryptionKeyId = propertyToEncrypt.ClientEncryptionKeyId, - EncryptionType = encryptionType, - }; - - return EncryptionSettings.Create(encryptionSettings); + return new EncryptionSettingForProperty( + propertyToEncrypt.ClientEncryptionKeyId, + encryptionType, + this.EncryptionProcessor); } } } @@ -89,90 +81,11 @@ internal EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPat } } - internal async Task BuildEncryptionAlgorithmForSettingAsync( - EncryptionProcessor encryptionProcessor, - CancellationToken cancellationToken) - { - ClientEncryptionKeyProperties clientEncryptionKeyProperties = await encryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: this.ClientEncryptionKeyId, - container: encryptionProcessor.Container, - cancellationToken: cancellationToken, - shouldForceRefresh: false); - - ProtectedDataEncryptionKey protectedDataEncryptionKey; - - try - { - // we pull out the Encrypted Data Encryption Key and build the Protected Data Encryption key - // Here a request is sent out to unwrap using the Master Key configured via the Key Encryption Key. - protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( - clientEncryptionKeyProperties, - encryptionProcessor.EncryptionKeyStoreProvider, - this.ClientEncryptionKeyId); - } - catch (RequestFailedException ex) - { - // The access to master key was probably revoked. Try to fetch the latest ClientEncryptionKeyProperties from the backend. - // This will succeed provided the user has rewraped the Client Encryption Key with right set of meta data. - // This is based on the AKV provider implementaion so we expect a RequestFailedException in case other providers are used in unwrap implementation. - if (ex.Status == (int)HttpStatusCode.Forbidden) - { - clientEncryptionKeyProperties = await encryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: this.ClientEncryptionKeyId, - container: encryptionProcessor.Container, - cancellationToken: cancellationToken, - shouldForceRefresh: true); - - // just bail out if this fails. - protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( - clientEncryptionKeyProperties, - encryptionProcessor.EncryptionKeyStoreProvider, - this.ClientEncryptionKeyId); - } - else - { - throw; - } - } - - AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = new AeadAes256CbcHmac256EncryptionAlgorithm( - protectedDataEncryptionKey, - this.EncryptionType); - - return aeadAes256CbcHmac256EncryptionAlgorithm; - } - - internal ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( - ClientEncryptionKeyProperties clientEncryptionKeyProperties, - EncryptionKeyStoreProvider encryptionKeyStoreProvider, - string keyId) - { - KeyEncryptionKey keyEncryptionKey = KeyEncryptionKey.GetOrCreate( - clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Name, - clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Value, - encryptionKeyStoreProvider); - - ProtectedDataEncryptionKey protectedDataEncryptionKey = ProtectedDataEncryptionKey.GetOrCreate( - keyId, - keyEncryptionKey, - clientEncryptionKeyProperties.WrappedDataEncryptionKey); - - return protectedDataEncryptionKey; - } - - internal void SetEncryptionSettingForProperty(string propertyName, EncryptionSettings encryptionSettings) - { - this.EncryptionSettingCacheByPropertyName.Set(propertyName, encryptionSettings); - } - - internal static EncryptionSettings Create( - EncryptionSettings settings) + internal void SetEncryptionSettingForProperty( + string propertyName, + EncryptionSettingForProperty encryptionSettingsForProperty) { - return new EncryptionSettings() - { - ClientEncryptionKeyId = settings.ClientEncryptionKeyId, - EncryptionType = settings.EncryptionType, - }; + this.EncryptionSettingCacheByPropertyName.Set(propertyName, encryptionSettingsForProperty); } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs index bef254114b..d7ffb1c761 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs @@ -82,26 +82,23 @@ public static async Task AddParameterAsync( await encryptionContainer.EncryptionProcessor.InitEncryptionSettingsIfNotInitializedAsync(cancellationToken); // get the path's encryption setting. - EncryptionSettings settings = await encryptionContainer.EncryptionProcessor.EncryptionSettings.GetEncryptionSettingForPropertyAsync( + EncryptionSettingForProperty settingsForProperty = await encryptionContainer.EncryptionProcessor.EncryptionSettings.GetEncryptionSettingForPropertyAsync( path.Substring(1), - encryptionContainer.EncryptionProcessor, cancellationToken); - if (settings == null) + if (settingsForProperty == null) { // property not encrypted. queryDefinitionwithEncryptedValues.WithParameter(name, value); return queryDefinitionwithEncryptedValues; } - if (settings.EncryptionType == EncryptionType.Randomized) + if (settingsForProperty.EncryptionType == EncryptionType.Randomized) { throw new ArgumentException($"Unsupported argument with Path: {path} for query. For executing queries on encrypted path requires the use of deterministic encryption type. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); } - AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settings.BuildEncryptionAlgorithmForSettingAsync( - encryptionContainer.EncryptionProcessor, - cancellationToken: default); + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingsForProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); JToken propertyValueToEncrypt = EncryptionProcessor.BaseSerializer.FromStream(valueStream); (EncryptionProcessor.TypeMarker typeMarker, byte[] serializedData) = EncryptionProcessor.Serialize(propertyValueToEncrypt); diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs index 348eb35e3b..358e3708d2 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs @@ -135,8 +135,12 @@ public async Task EncryptionCreateDekWithDualDekProvider() public async Task EncryptionCreateDekWithNonMdeAlgorithmFails() { string dekId = "oldDek"; - TestEncryptionKeyStoreProvider testKeyStoreProvider = new TestEncryptionKeyStoreProvider(); - CosmosDataEncryptionKeyProvider dekProvider = new CosmosDataEncryptionKeyProvider(testKeyStoreProvider, cacheTimeToLive: TimeSpan.FromSeconds(3600)); + TestEncryptionKeyStoreProvider testKeyStoreProvider = new TestEncryptionKeyStoreProvider + { + DataEncryptionKeyCacheTimeToLive = TimeSpan.FromSeconds(3600) + }; + + CosmosDataEncryptionKeyProvider dekProvider = new CosmosDataEncryptionKeyProvider(testKeyStoreProvider); try { await MdeCustomEncryptionTests.CreateDekAsync(dekProvider, dekId, CosmosEncryptionAlgorithm.AEAes256CbcHmacSha256Randomized); @@ -603,8 +607,12 @@ public async Task ValidateCachingofProtectedDataEncryptionKey() Assert.AreEqual(1, unwrapcount); // Caching for 30 min. - testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider(); - dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider, TimeSpan.FromMinutes(30)); + testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider + { + DataEncryptionKeyCacheTimeToLive = null + }; + + dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider); await dekProvider.InitializeAsync(MdeCustomEncryptionTests.database, MdeCustomEncryptionTests.keyContainer.Id); encryptor = new TestEncryptor(dekProvider); @@ -616,8 +624,12 @@ public async Task ValidateCachingofProtectedDataEncryptionKey() Assert.AreEqual(1, unwrapcount); // No caching - testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider(); - dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider, cacheTimeToLive: TimeSpan.FromSeconds(0)); + testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider + { + DataEncryptionKeyCacheTimeToLive = TimeSpan.Zero + }; + + dekProvider = new CosmosDataEncryptionKeyProvider(testEncryptionKeyStoreProvider,TimeSpan.FromMinutes(30)); await dekProvider.InitializeAsync(MdeCustomEncryptionTests.database, MdeCustomEncryptionTests.keyContainer.Id); encryptor = new TestEncryptor(dekProvider); diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index da04ca65b4..2d662bd475 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -38,7 +38,10 @@ public class MdeEncryptionTests public static async Task ClassInitialize(TestContext context) { MdeEncryptionTests.client = TestCommon.CreateCosmosClient(); - testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider(); + testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider + { + DataEncryptionKeyCacheTimeToLive = null + }; MdeEncryptionTests.encryptionCosmosClient = MdeEncryptionTests.client.WithEncryption(testEncryptionKeyStoreProvider); MdeEncryptionTests.database = await MdeEncryptionTests.encryptionCosmosClient.CreateDatabaseAsync(Guid.NewGuid().ToString()); diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/Microsoft.Azure.Cosmos.Encryption.Tests/Contracts/DotNetSDKEncryptionAPI.json b/Microsoft.Azure.Cosmos.Encryption/tests/Microsoft.Azure.Cosmos.Encryption.Tests/Contracts/DotNetSDKEncryptionAPI.json index e37f2e9299..61a60c976c 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/Microsoft.Azure.Cosmos.Encryption.Tests/Contracts/DotNetSDKEncryptionAPI.json +++ b/Microsoft.Azure.Cosmos.Encryption/tests/Microsoft.Azure.Cosmos.Encryption.Tests/Contracts/DotNetSDKEncryptionAPI.json @@ -103,20 +103,20 @@ ], "MethodInfo": "System.Threading.Tasks.Task`1[Microsoft.Azure.Cosmos.Encryption.Custom.DataEncryptionKey] FetchDataEncryptionKeyAsync(System.String, System.String, System.Threading.CancellationToken);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])": { + "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])]" + "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])]" }, "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])": { "Type": "Constructor", "Attributes": [], "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])]" }, - "Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])": { + "Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])]" + "MethodInfo": "[Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])]" } }, "NestedTypes": {} @@ -444,20 +444,20 @@ ], "MethodInfo": "System.Threading.Tasks.Task`1[Microsoft.Azure.Cosmos.Encryption.Custom.DataEncryptionKey] FetchDataEncryptionKeyAsync(System.String, System.String, System.Threading.CancellationToken);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])": { + "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])]" + "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])]" }, "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])": { "Type": "Constructor", "Attributes": [], "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])]" }, - "Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])": { + "Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])]" + "MethodInfo": "[Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])]" } }, "NestedTypes": {} From c03830024b2390b32d17b80f51dcfb3e872d4cbd Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 9 Apr 2021 11:12:33 +0530 Subject: [PATCH 06/27] Changes as per review comments. --- .../Custom/CosmosDataEncryptionKeyProvider.cs | 6 ++-- .../tests/EmulatorTests/MdeEncryptionTests.cs | 32 +++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs b/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs index 5a377b1fe9..ef273adb9a 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/Custom/CosmosDataEncryptionKeyProvider.cs @@ -91,7 +91,8 @@ public CosmosDataEncryptionKeyProvider( } else { - // arbitrarily large caching period. + // If null is passed to DataEncryptionKeyCacheTimeToLive it results in forever caching hence setting + // arbitrarily large caching period. ProtectedDataEncryptionKey does not seem to handle TimeSpan.MaxValue. ProtectedDataEncryptionKey.TimeToLive = TimeSpan.FromDays(36500); } } @@ -120,7 +121,8 @@ public CosmosDataEncryptionKeyProvider( } else { - // arbitrarily large caching period. + // If null is passed to DataEncryptionKeyCacheTimeToLive it results in forever caching hence setting + // arbitrarily large caching period. ProtectedDataEncryptionKey does not seem to handle TimeSpan.MaxValue. ProtectedDataEncryptionKey.TimeToLive = TimeSpan.FromDays(36500); } } diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index 2d662bd475..6e02b87c2c 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -1058,12 +1058,38 @@ await MdeEncryptionTests.ValidateQueryResultsAsync( [TestMethod] public async Task ValidateCachingofProtectedDataEncryptionKey() { - for (int i = 0; i < 6; i++) - await MdeEncryptionTests.MdeCreateItemAsync(MdeEncryptionTests.encryptionContainer); + // Default cache TTL 2 hours. + TestEncryptionKeyStoreProvider newtestEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider(); + CosmosClient newEncryptionClient = MdeEncryptionTests.client.WithEncryption(newtestEncryptionKeyStoreProvider); + Database database = newEncryptionClient.GetDatabase(MdeEncryptionTests.database.Id); - testEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(metadata1.Value, out int unwrapcount); + Container encryptionContainer = database.GetContainer(MdeEncryptionTests.encryptionContainer.Id); + + for (int i = 0; i < 2; i++) + await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); + + newtestEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(metadata1.Value, out int unwrapcount); + // expecting just one unwrap. Assert.AreEqual(1, unwrapcount); + + // no caching. + newtestEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider() + { + DataEncryptionKeyCacheTimeToLive = TimeSpan.Zero, + }; + + newEncryptionClient = MdeEncryptionTests.client.WithEncryption(newtestEncryptionKeyStoreProvider); + database = newEncryptionClient.GetDatabase(MdeEncryptionTests.database.Id); + + encryptionContainer = database.GetContainer(MdeEncryptionTests.encryptionContainer.Id); + + for (int i = 0; i < 2; i++) + await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); + + newtestEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(metadata1.Value, out unwrapcount); + Assert.IsTrue(unwrapcount > 1, "The actual unwrap count was not greater than 1"); } + private static async Task ValidateQueryResultsMultipleDocumentsAsync( Container container, TestDoc testDoc1, From 02a394c9bc61a516bd26a575ba6d030b193dcd7b Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 9 Apr 2021 12:29:33 +0530 Subject: [PATCH 07/27] Update MdeCustomEncryptionTests.cs --- .../tests/EmulatorTests/MdeCustomEncryptionTests.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs index a16a1fd39a..9ee6fb1d91 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeCustomEncryptionTests.cs @@ -289,7 +289,7 @@ public async Task EncryptionFailsWithUnknownDek() [TestMethod] public async Task ValidateCachingOfProtectedDataEncryptionKey() { - TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider = testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider + TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider { DataEncryptionKeyCacheTimeToLive = TimeSpan.FromMinutes(30) }; @@ -341,7 +341,7 @@ public async Task ValidateCachingOfProtectedDataEncryptionKey() await MdeCustomEncryptionTests.CreateItemAsync(encryptionContainer, dekId, TestDoc.PathsToEncrypt); testEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(masterKeyUri1.ToString(), out unwrapcount); - Assert.AreEqual(1, unwrapcount); + Assert.AreEqual(1, unwrapcount); } [TestMethod] @@ -2136,15 +2136,12 @@ private static async Task> CreateItemAsyncUsingLegacyAlgor private static async Task LegacyClassInitializeAsync() { - TestEncryptionKeyStoreProvider testKeyStoreProvider = new TestEncryptionKeyStoreProvider - { - DataEncryptionKeyCacheTimeToLive = TimeSpan.FromSeconds(3600) - }; + MdeCustomEncryptionTests.testKeyStoreProvider.DataEncryptionKeyCacheTimeToLive = TimeSpan.FromSeconds(3600); - MdeCustomEncryptionTests.dekProvider = new CosmosDataEncryptionKeyProvider(new TestKeyWrapProvider(), testKeyStoreProvider); + MdeCustomEncryptionTests.dekProvider = new CosmosDataEncryptionKeyProvider(new TestKeyWrapProvider(), MdeCustomEncryptionTests.testKeyStoreProvider); MdeCustomEncryptionTests.legacytestKeyWrapProvider = new TestKeyWrapProvider(); - testKeyStoreProvider = new TestEncryptionKeyStoreProvider + TestEncryptionKeyStoreProvider testKeyStoreProvider = new TestEncryptionKeyStoreProvider { DataEncryptionKeyCacheTimeToLive = TimeSpan.Zero }; From 10f97eb3f035e136eafb074fcabc7b60adcf5e32 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 9 Apr 2021 12:39:36 +0530 Subject: [PATCH 08/27] Updated contracts. --- .../src/EncryptionCosmosClientExtensions.cs | 3 ++- .../Contracts/DotNetSDKEncryptionAPI.json | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs index 765ffc36d5..2419e78676 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClientExtensions.cs @@ -39,7 +39,8 @@ public static CosmosClient WithEncryption( } else { - // arbitrarily large caching period. + // If null is passed to DataEncryptionKeyCacheTimeToLive it results in forever caching hence setting + // arbitrarily large caching period. ProtectedDataEncryptionKey does not seem to handle TimeSpan.MaxValue. ProtectedDataEncryptionKey.TimeToLive = TimeSpan.FromDays(36500); } diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/Microsoft.Azure.Cosmos.Encryption.Tests/Contracts/DotNetSDKEncryptionAPI.json b/Microsoft.Azure.Cosmos.Encryption/tests/Microsoft.Azure.Cosmos.Encryption.Tests/Contracts/DotNetSDKEncryptionAPI.json index 6a7bcbd204..ef4f67dc47 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/Microsoft.Azure.Cosmos.Encryption.Tests/Contracts/DotNetSDKEncryptionAPI.json +++ b/Microsoft.Azure.Cosmos.Encryption/tests/Microsoft.Azure.Cosmos.Encryption.Tests/Contracts/DotNetSDKEncryptionAPI.json @@ -51,12 +51,12 @@ ], "MethodInfo": "System.Threading.Tasks.Task`1[Microsoft.Azure.Cosmos.Encryption.Custom.DataEncryptionKey] FetchDataEncryptionKeyAsync(System.String, System.String, System.Threading.CancellationToken);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")]": { + "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")]": { "Type": "Constructor", "Attributes": [ "ObsoleteAttribute" ], - "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")], Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])]" + "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")], Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])]" }, "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")]": { "Type": "Constructor", @@ -65,10 +65,10 @@ ], "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")], Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])]" }, - "Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])": { + "Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])]" + "MethodInfo": "[Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])]" } }, "NestedTypes": {} @@ -398,12 +398,12 @@ ], "MethodInfo": "System.Threading.Tasks.Task`1[Microsoft.Azure.Cosmos.Encryption.Custom.DataEncryptionKey] FetchDataEncryptionKeyAsync(System.String, System.String, System.Threading.CancellationToken);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")]": { + "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")]": { "Type": "Constructor", "Attributes": [ "ObsoleteAttribute" ], - "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")], Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])]" + "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")], Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])]" }, "Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")]": { "Type": "Constructor", @@ -412,10 +412,10 @@ ], "MethodInfo": "[Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])[System.ObsoleteAttribute(\"Please use the constructor with EncryptionKeyStoreProvider only.\")], Void .ctor(Microsoft.Azure.Cosmos.Encryption.Custom.EncryptionKeyWrapProvider, System.Nullable`1[System.TimeSpan])]" }, - "Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])": { + "Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan], System.Nullable`1[System.TimeSpan])]" + "MethodInfo": "[Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan]), Void .ctor(Microsoft.Data.Encryption.Cryptography.EncryptionKeyStoreProvider, System.Nullable`1[System.TimeSpan])]" } }, "NestedTypes": {} From 9310436ef8e29c561f426b6e43677e64f2a109dd Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 9 Apr 2021 13:58:45 +0530 Subject: [PATCH 09/27] Update MdeEncryptionTests.cs --- .../tests/EmulatorTests/MdeEncryptionTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index 6e02b87c2c..eef2319dd3 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -834,8 +834,8 @@ public async Task VerifyKekRevokeHandling() DataEncryptionKeyCacheTimeToLive = TimeSpan.Zero }; - CosmosClient encryptionCosmosClientWithBulk = clientWithNoCaching.WithEncryption(testEncryptionKeyStoreProvider); - Database database = encryptionCosmosClientWithBulk.GetDatabase(MdeEncryptionTests.database.Id); + CosmosClient encryptionCosmosClient = clientWithNoCaching.WithEncryption(testEncryptionKeyStoreProvider); + Database database = encryptionCosmosClient.GetDatabase(MdeEncryptionTests.database.Id); // Once a Dek gets cached and the Kek is revoked, calls to unwrap/wrap keys would fail since KEK is revoked. // The Dek should be rewrapped if the KEK is revoked. @@ -1066,7 +1066,9 @@ public async Task ValidateCachingofProtectedDataEncryptionKey() Container encryptionContainer = database.GetContainer(MdeEncryptionTests.encryptionContainer.Id); for (int i = 0; i < 2; i++) + { await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); + } newtestEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(metadata1.Value, out int unwrapcount); // expecting just one unwrap. @@ -1084,7 +1086,9 @@ public async Task ValidateCachingofProtectedDataEncryptionKey() encryptionContainer = database.GetContainer(MdeEncryptionTests.encryptionContainer.Id); for (int i = 0; i < 2; i++) + { await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); + } newtestEncryptionKeyStoreProvider.UnWrapKeyCallsCount.TryGetValue(metadata1.Value, out unwrapcount); Assert.IsTrue(unwrapcount > 1, "The actual unwrap count was not greater than 1"); From 67a13bb29c9a6bcced65bcb18e42c107e4fd6fef Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Tue, 20 Apr 2021 12:19:01 +0530 Subject: [PATCH 10/27] Fixes Encryption Cosmos Client Encryption Policy and Keys cache. --- .../src/Custom/EncryptionContainer.cs | 28 + .../src/EncryptionContainer.cs | 301 ++++++++-- .../src/EncryptionContainerExtensions.cs | 2 +- .../src/EncryptionCosmosClient.cs | 18 +- .../src/EncryptionFeedIterator.cs | 21 +- .../src/EncryptionProcessor.cs | 28 +- .../src/EncryptionSettings.cs | 25 +- .../src/EncryptionTransactionalBatch.cs | 69 ++- .../tests/EmulatorTests/MdeEncryptionTests.cs | 556 +++++++++++++++++- 9 files changed, 961 insertions(+), 87 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Custom/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/Custom/EncryptionContainer.cs index bf821a1f90..5d57e2dabe 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Custom/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/Custom/EncryptionContainer.cs @@ -926,5 +926,33 @@ public override Task PatchItemStreamAsync( { throw new NotImplementedException(); } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( + string processorName, + ChangeFeedHandler onChangesDelegate) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint( + string processorName, + ChangeFeedHandlerWithManualCheckpoint onChangesDelegate) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( + string processorName, + ChangeFeedStreamHandler onChangesDelegate) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint( + string processorName, + ChangeFeedStreamHandlerWithManualCheckpoint onChangesDelegate) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index e7eb8ceafd..8965e4582d 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -21,10 +21,12 @@ internal sealed class EncryptionContainer : Container public CosmosResponseFactory ResponseFactory { get; } - public EncryptionProcessor EncryptionProcessor { get; } - public EncryptionCosmosClient EncryptionCosmosClient { get; } + public EncryptionProcessor EncryptionProcessor => this.encryptionProcessorLazy.Value; + + private readonly Lazy encryptionProcessorLazy; + private bool isEncryptionContainerCacheInitDone; private static readonly SemaphoreSlim CacheInitSema = new SemaphoreSlim(1, 1); @@ -40,14 +42,12 @@ public EncryptionContainer( { this.Container = container ?? throw new ArgumentNullException(nameof(container)); this.EncryptionCosmosClient = encryptionCosmosClient ?? throw new ArgumentNullException(nameof(container)); - - this.EncryptionProcessor = new EncryptionProcessor( - container, - this.EncryptionCosmosClient); - this.ResponseFactory = this.Database.Client.ResponseFactory; this.CosmosSerializer = this.Database.Client.ClientOptions.Serializer; + this.isEncryptionContainerCacheInitDone = false; + this.DatabaseContainerRidCacheByContainerName = new AsyncCache>(); + this.encryptionProcessorLazy = new Lazy(() => new EncryptionProcessor(this, this.EncryptionCosmosClient)); } public override string Id => this.Container.Id; @@ -58,21 +58,57 @@ public EncryptionContainer( public override Database Database => this.Container.Database; - internal async Task InitEncryptionContainerCacheIfNotInitAsync(CancellationToken cancellationToken) + internal AsyncCache> DatabaseContainerRidCacheByContainerName { get; set; } + + internal async Task> FetchDatabaseAndContainerRidAsync(Container container) + { + ContainerResponse resp = await container.ReadContainerAsync(); + string databaseRid = resp.Resource.SelfLink.Split('/').ElementAt(1); + string containerRid = resp.Resource.SelfLink.Split('/').ElementAt(3); + return new Tuple(databaseRid, containerRid); + } + + internal async Task> GetorUpdateDatabaseAndContainerRidFromCacheAsync( + CancellationToken cancellationToken, + bool shouldForceRefresh = false) { - if (this.isEncryptionContainerCacheInitDone) + return await this.DatabaseContainerRidCacheByContainerName.GetAsync( + this.Id, + obsoleteValue: new Tuple(null, null), + singleValueInitFunc: async () => await this.FetchDatabaseAndContainerRidAsync(this.Container), + cancellationToken: cancellationToken, + forceRefresh: shouldForceRefresh); + } + + internal async Task InitEncryptionContainerCacheIfNotInitAsync(CancellationToken cancellationToken, bool shouldForceRefresh = false) + { + if (this.isEncryptionContainerCacheInitDone && !shouldForceRefresh) { return; } if (await CacheInitSema.WaitAsync(-1)) { - if (!this.isEncryptionContainerCacheInitDone) + if (!this.isEncryptionContainerCacheInitDone || shouldForceRefresh) { try { - await this.InitContainerCacheAsync(cancellationToken); - await this.EncryptionProcessor.InitEncryptionSettingsIfNotInitializedAsync(); + if (shouldForceRefresh && this.isEncryptionContainerCacheInitDone) + { + this.isEncryptionContainerCacheInitDone = false; + } + + // if force refreshed, results in the Client Keys and Policies to be refreshed in client cache. + await this.InitContainerCacheAsync(cancellationToken: cancellationToken, shouldForceRefresh: shouldForceRefresh); + + // a forceRefresh here results in refreshing the Encryption Processor EncryptionSetting cache not the Cosmos Client Cache which is done earlier. + await this.EncryptionProcessor.InitEncryptionSettingsIfNotInitializedAsync(shouldForceRefresh: shouldForceRefresh); + + // update the Rid cache. + await this.GetorUpdateDatabaseAndContainerRidFromCacheAsync( + cancellationToken: cancellationToken, + shouldForceRefresh: shouldForceRefresh); + this.isEncryptionContainerCacheInitDone = true; } finally @@ -88,7 +124,8 @@ internal async Task InitEncryptionContainerCacheIfNotInitAsync(CancellationToken } internal async Task InitContainerCacheAsync( - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + bool shouldForceRefresh = false) { cancellationToken.ThrowIfCancellationRequested(); @@ -96,7 +133,7 @@ internal async Task InitContainerCacheAsync( ClientEncryptionPolicy clientEncryptionPolicy = await encryptionCosmosClient.GetClientEncryptionPolicyAsync( container: this, cancellationToken: cancellationToken, - shouldForceRefresh: false); + shouldForceRefresh: shouldForceRefresh); if (clientEncryptionPolicy != null) { @@ -106,11 +143,38 @@ await this.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: clientEncryptionKeyId, container: this, cancellationToken: cancellationToken, - shouldForceRefresh: false); + shouldForceRefresh: shouldForceRefresh); } } } + internal void AddHeaders(Headers header) + { + header.Add("x-ms-cosmos-is-client-encrypted", bool.TrueString); + + (_, string ridValue) = this.GetorUpdateDatabaseAndContainerRidFromCacheAsync(cancellationToken: default) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + header.Add("x-ms-cosmos-intended-collection-rid", ridValue); + } + + internal ItemRequestOptions GetClonedItemRequestOptions(ItemRequestOptions itemRequestOptions) + { + ItemRequestOptions clonedRequestOptions; + + if (itemRequestOptions != null) + { + clonedRequestOptions = (ItemRequestOptions)itemRequestOptions.ShallowCopy(); + } + else + { + clonedRequestOptions = new ItemRequestOptions(); + } + + return clonedRequestOptions; + } + public override async Task> CreateItemAsync( T item, PartitionKey? partitionKey = null, @@ -130,7 +194,7 @@ public override async Task> CreateItemAsync( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("CreateItem")) { - ResponseMessage responseMessage; + ResponseMessage responseMessage = null; using (Stream itemStream = this.CosmosSerializer.ToStream(item)) { @@ -176,17 +240,54 @@ private async Task CreateItemHelperAsync( CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { - streamPayload = await this.EncryptionProcessor.EncryptAsync( - streamPayload, - diagnosticsContext, - cancellationToken); + ItemRequestOptions clonedRequestOptions = requestOptions; + + JObject plainItemJobject = null; + bool isEncryptionSuccessful; + (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + streamPayload, + diagnosticsContext, + cancellationToken); + + // could also mean there was no encryption policy configured. + if (isEncryptionSuccessful) + { + clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + } ResponseMessage responseMessage = await this.Container.CreateItemStreamAsync( streamPayload, partitionKey, - requestOptions, + clonedRequestOptions, cancellationToken); + if (responseMessage.StatusCode != System.Net.HttpStatusCode.Created && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + // set it back upon successful encryption. + clonedRequestOptions.AddRequestHeaders = null; + + // get the latest policy and re-encrypt. + await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + + streamPayload = this.CosmosSerializer.ToStream(plainItemJobject); + (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + streamPayload, + diagnosticsContext, + cancellationToken); + + if (isEncryptionSuccessful) + { + clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + } + + responseMessage = await this.Container.CreateItemStreamAsync( + streamPayload, + partitionKey, + clonedRequestOptions, + cancellationToken); + } + responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( responseMessage.Content, diagnosticsContext, @@ -268,16 +369,31 @@ private async Task ReadItemHelperAsync( CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + + clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + ResponseMessage responseMessage = await this.Container.ReadItemStreamAsync( id, partitionKey, - requestOptions, + clonedRequestOptions, cancellationToken); - responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( - responseMessage.Content, - diagnosticsContext, + if (responseMessage.StatusCode != System.Net.HttpStatusCode.OK && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + + responseMessage = await this.Container.ReadItemStreamAsync( + id, + partitionKey, + clonedRequestOptions, cancellationToken); + } + + responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( + responseMessage.Content, + diagnosticsContext, + cancellationToken); return responseMessage; } @@ -362,28 +478,62 @@ private async Task ReplaceItemHelperAsync( CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { + ItemRequestOptions clonedRequestOptions = requestOptions; + if (partitionKey == null) { throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); } - streamPayload = await this.EncryptionProcessor.EncryptAsync( + JObject plainItemJobject = null; + bool isEncryptionSuccessful; + (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( streamPayload, diagnosticsContext, cancellationToken); + if (isEncryptionSuccessful) + { + clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + } + ResponseMessage responseMessage = await this.Container.ReplaceItemStreamAsync( streamPayload, id, partitionKey, - requestOptions, + clonedRequestOptions, cancellationToken); - responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( - responseMessage.Content, + if (responseMessage.StatusCode != System.Net.HttpStatusCode.Created && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + clonedRequestOptions.AddRequestHeaders = null; + await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + + streamPayload = this.CosmosSerializer.ToStream(plainItemJobject); + (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + streamPayload, diagnosticsContext, cancellationToken); + if (isEncryptionSuccessful) + { + clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + } + + responseMessage = await this.Container.ReplaceItemStreamAsync( + streamPayload, + id, + partitionKey, + clonedRequestOptions, + cancellationToken); + } + + responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( + responseMessage.Content, + diagnosticsContext, + cancellationToken); + return responseMessage; } @@ -457,17 +607,50 @@ private async Task UpsertItemHelperAsync( throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); } - streamPayload = await this.EncryptionProcessor.EncryptAsync( + ItemRequestOptions clonedRequestOptions = requestOptions; + + bool isEncryptionSuccessful; + JObject plainItemJobject = null; + (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( streamPayload, diagnosticsContext, cancellationToken); + if (isEncryptionSuccessful) + { + clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + } + ResponseMessage responseMessage = await this.Container.UpsertItemStreamAsync( streamPayload, partitionKey, - requestOptions, + clonedRequestOptions, cancellationToken); + if (responseMessage.StatusCode != System.Net.HttpStatusCode.Created && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + clonedRequestOptions.AddRequestHeaders = null; + await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + + streamPayload = this.CosmosSerializer.ToStream(plainItemJobject); + (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + streamPayload, + diagnosticsContext, + cancellationToken); + + if (isEncryptionSuccessful) + { + clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + } + + responseMessage = await this.Container.UpsertItemStreamAsync( + streamPayload, + partitionKey, + clonedRequestOptions, + cancellationToken); + } + responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( responseMessage.Content, diagnosticsContext, @@ -481,7 +664,7 @@ public override TransactionalBatch CreateTransactionalBatch( { return new EncryptionTransactionalBatch( this.Container.CreateTransactionalBatch(partitionKey), - this.EncryptionProcessor, + this, this.CosmosSerializer); } @@ -545,11 +728,23 @@ public override FeedIterator GetItemQueryIterator( string continuationToken = null, QueryRequestOptions requestOptions = null) { + QueryRequestOptions clonedRequestOptions; + if (requestOptions != null) + { + clonedRequestOptions = (QueryRequestOptions)requestOptions.ShallowCopy(); + } + else + { + clonedRequestOptions = new QueryRequestOptions(); + } + + clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + return new EncryptionFeedIterator( (EncryptionFeedIterator)this.GetItemQueryStreamIterator( queryText, continuationToken, - requestOptions), + clonedRequestOptions), this.ResponseFactory); } @@ -629,7 +824,7 @@ public override FeedIterator GetItemQueryStreamIterator( queryDefinition, continuationToken, requestOptions), - this.EncryptionProcessor); + this); } public override FeedIterator GetItemQueryStreamIterator( @@ -642,7 +837,7 @@ public override FeedIterator GetItemQueryStreamIterator( queryText, continuationToken, requestOptions), - this.EncryptionProcessor); + this); } public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( @@ -652,7 +847,7 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(null); using (diagnosticsContext.CreateScope("GetChangeFeedProcessorBuilder")) { - return this.Container.GetChangeFeedProcessorBuilder( + ChangeFeedProcessorBuilder changeFeedProcessorBuilder = this.Container.GetChangeFeedProcessorBuilder( processorName, async (IReadOnlyCollection documents, CancellationToken cancellationToken) => { @@ -671,6 +866,8 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( // Call the original passed in delegate await onChangesDelegate(decryptedItems, cancellationToken); }); + + return changeFeedProcessorBuilder; } } @@ -710,7 +907,7 @@ public override FeedIterator GetItemQueryStreamIterator( queryDefinition, continuationToken, requestOptions), - this.EncryptionProcessor); + this); } public override FeedIterator GetItemQueryIterator( @@ -745,7 +942,7 @@ public override FeedIterator GetChangeFeedStreamIterator( changeFeedStartFrom, changeFeedMode, changeFeedRequestOptions), - this.EncryptionProcessor); + this); } public override FeedIterator GetChangeFeedIterator( @@ -780,5 +977,33 @@ public override Task PatchItemStreamAsync( { throw new NotImplementedException(); } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( + string processorName, + ChangeFeedHandler onChangesDelegate) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint( + string processorName, + ChangeFeedHandlerWithManualCheckpoint onChangesDelegate) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( + string processorName, + ChangeFeedStreamHandler onChangesDelegate) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint( + string processorName, + ChangeFeedStreamHandlerWithManualCheckpoint onChangesDelegate) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs index 247f7e93b7..5c032adab1 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs @@ -109,7 +109,7 @@ public static FeedIterator ToEncryptionStreamIterator( return new EncryptionFeedIterator( query.ToStreamIterator(), - encryptionContainer.EncryptionProcessor); + encryptionContainer); } /// diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs index d089d64a83..85be88f530 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs @@ -49,8 +49,13 @@ internal async Task GetClientEncryptionPolicyAsync( throw new ArgumentNullException(nameof(container)); } + EncryptionContainer encryptionContainer = (EncryptionContainer)container; + + (string databaseRidvalue, string containerRidvalue) = await encryptionContainer.GetorUpdateDatabaseAndContainerRidFromCacheAsync( + cancellationToken: cancellationToken); + // container Id is unique within a Database. - string cacheKey = container.Database.Id + "/" + container.Id; + string cacheKey = databaseRidvalue + "|" + containerRidvalue + container.Database.Id + "/" + container.Id; // cache it against Database and Container ID key. return await this.clientEncryptionPolicyCacheByContainerId.GetAsync( @@ -77,8 +82,13 @@ internal async Task GetClientEncryptionKeyPropert throw new ArgumentNullException(nameof(container)); } + EncryptionContainer encryptionContainer = (EncryptionContainer)container; + + (string databaseRidvalue, string containerRidvalue) = await encryptionContainer.GetorUpdateDatabaseAndContainerRidFromCacheAsync( + cancellationToken: cancellationToken); + // Client Encryption key Id is unique within a Database. - string cacheKey = container.Database.Id + "/" + clientEncryptionKeyId; + string cacheKey = databaseRidvalue + "|" + containerRidvalue + container.Database.Id + "/" + clientEncryptionKeyId; return await this.clientEncryptionKeyPropertiesCacheByKeyId.GetAsync( cacheKey, @@ -207,9 +217,11 @@ public override Database GetDatabase(string id) public override Container GetContainer(string databaseId, string containerId) { - return new EncryptionContainer( + EncryptionContainer encryptionContainer = new EncryptionContainer( this.cosmosClient.GetContainer(databaseId, containerId), this); + + return encryptionContainer; } public override FeedIterator GetDatabaseQueryIterator( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs index a8bfab2454..caa0279139 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs @@ -14,13 +14,15 @@ internal sealed class EncryptionFeedIterator : FeedIterator { private readonly FeedIterator feedIterator; private readonly EncryptionProcessor encryptionProcessor; + private readonly EncryptionContainer encryptionContainer; public EncryptionFeedIterator( FeedIterator feedIterator, - EncryptionProcessor encryptionProcessor) + EncryptionContainer encryptionContainer) { this.feedIterator = feedIterator ?? throw new ArgumentNullException(nameof(feedIterator)); - this.encryptionProcessor = encryptionProcessor ?? throw new ArgumentNullException(nameof(encryptionProcessor)); + this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); + this.encryptionProcessor = encryptionContainer.EncryptionProcessor; } public override bool HasMoreResults => this.feedIterator.HasMoreResults; @@ -32,6 +34,21 @@ public override async Task ReadNextAsync(CancellationToken canc { ResponseMessage responseMessage = await this.feedIterator.ReadNextAsync(cancellationToken); + // check for Bad Request and Wrong RID intended and update the cached RID and Client Encryption Policy. + if (responseMessage.StatusCode != System.Net.HttpStatusCode.OK + && responseMessage.StatusCode != System.Net.HttpStatusCode.NotModified + && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + + throw new CosmosException( + "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + responseMessage.ErrorMessage, + responseMessage.StatusCode, + 1024, + responseMessage.Headers.ActivityId, + responseMessage.Headers.RequestCharge); + } + if (responseMessage.IsSuccessStatusCode && responseMessage.Content != null) { Stream decryptedContent = await this.DeserializeAndDecryptResponseAsync( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs index 383a19e87a..a76989834f 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs @@ -5,7 +5,6 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; - using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -61,11 +60,11 @@ public EncryptionProcessor( /// /// cancellation token /// Task - internal async Task InitializeEncryptionSettingsAsync(CancellationToken cancellationToken = default) + internal async Task InitializeEncryptionSettingsAsync(CancellationToken cancellationToken = default, bool shouldForceRefresh = false) { cancellationToken.ThrowIfCancellationRequested(); - if (this.isEncryptionSettingsInitDone) + if (this.isEncryptionSettingsInitDone && !shouldForceRefresh) { throw new InvalidOperationException("The Encrypton Processor has already been initialized. "); } @@ -95,9 +94,10 @@ internal async Task InitializeEncryptionSettingsAsync(CancellationToken cancella string propertyName = propertyToEncrypt.Path.Substring(1); - this.EncryptionSettings.SetEncryptionSettingForProperty( + await this.EncryptionSettings.SetEncryptionSettingForPropertyAsync( propertyName, - encryptionSettingsForProperty); + encryptionSettingsForProperty, + cancellationToken); } this.isEncryptionSettingsInitDone = true; @@ -108,20 +108,20 @@ internal async Task InitializeEncryptionSettingsAsync(CancellationToken cancella /// /// (Optional) Token to cancel the operation. /// Task to await. - internal async Task InitEncryptionSettingsIfNotInitializedAsync(CancellationToken cancellationToken = default) + internal async Task InitEncryptionSettingsIfNotInitializedAsync(CancellationToken cancellationToken = default, bool shouldForceRefresh = false) { - if (this.isEncryptionSettingsInitDone) + if (this.isEncryptionSettingsInitDone && !shouldForceRefresh) { return; } if (await CacheInitSema.WaitAsync(-1)) { - if (!this.isEncryptionSettingsInitDone) + if (!this.isEncryptionSettingsInitDone || shouldForceRefresh) { try { - await this.InitializeEncryptionSettingsAsync(cancellationToken); + await this.InitializeEncryptionSettingsAsync(cancellationToken, shouldForceRefresh); } finally { @@ -260,7 +260,7 @@ private JToken SerializeAndEncryptValue( /// 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. /// - public async Task EncryptAsync( + public async Task<(Stream encryptedStream, bool isEncryptionSuccessful, JObject plainItemJobject)> EncryptAsync( Stream input, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) @@ -276,11 +276,13 @@ public async Task EncryptAsync( if (this.ClientEncryptionPolicy == null) { - return input; + return (input, false, null); } JObject itemJObj = EncryptionProcessor.BaseSerializer.FromStream(input); + JObject plainItemObj = (JObject)itemJObj.DeepClone(); + foreach (ClientEncryptionIncludedPath pathToEncrypt in this.ClientEncryptionPolicy.IncludedPaths) { string propertyName = pathToEncrypt.Path.Substring(1); @@ -291,7 +293,7 @@ public async Task EncryptAsync( continue; } - EncryptionSettingForProperty settingforProperty = await this.EncryptionSettings.GetEncryptionSettingForPropertyAsync(propertyName,cancellationToken); + EncryptionSettingForProperty settingforProperty = await this.EncryptionSettings.GetEncryptionSettingForPropertyAsync(propertyName, cancellationToken); if (settingforProperty == null) { @@ -307,7 +309,7 @@ public async Task EncryptAsync( } input.Dispose(); - return EncryptionProcessor.BaseSerializer.ToStream(itemJObj); + return (EncryptionProcessor.BaseSerializer.ToStream(itemJObj), true, plainItemObj); } private JToken DecryptAndDeserializeValue( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index 6922be863e..eb8eac83ff 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -11,6 +11,8 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionSettings { + // The cacheKey used here is combination of Database Rid, Container Rid and property name. + // This allows to access the exact version of setting incase the containers are created with same name. internal AsyncCache EncryptionSettingCacheByPropertyName { get; } = new AsyncCache(); public EncryptionProcessor EncryptionProcessor { get; } @@ -24,8 +26,15 @@ internal async Task GetEncryptionSettingForPropert string propertyName, CancellationToken cancellationToken) { + EncryptionContainer encryptionContainer = (EncryptionContainer)this.EncryptionProcessor.Container; + + (string databaseRid, string containerRid) = await encryptionContainer.GetorUpdateDatabaseAndContainerRidFromCacheAsync( + cancellationToken: cancellationToken); + + string cacheKey = databaseRid + "|" + containerRid + "/" + propertyName; + EncryptionSettingForProperty encryptionSettingsForProperty = await this.EncryptionSettingCacheByPropertyName.GetAsync( - propertyName, + cacheKey, obsoleteValue: null, async () => await this.FetchEncryptionSettingForPropertyAsync(propertyName, cancellationToken), cancellationToken); @@ -63,6 +72,7 @@ private async Task FetchEncryptionSettingForProper } } + Console.WriteLine("Could not find for property: {0} \n", propertyName); return null; } @@ -81,11 +91,18 @@ internal EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPat } } - internal void SetEncryptionSettingForProperty( + internal async Task SetEncryptionSettingForPropertyAsync( string propertyName, - EncryptionSettingForProperty encryptionSettingsForProperty) + EncryptionSettingForProperty encryptionSettingsForProperty, + CancellationToken cancellationToken) { - this.EncryptionSettingCacheByPropertyName.Set(propertyName, encryptionSettingsForProperty); + EncryptionContainer encryptionContainer = (EncryptionContainer)this.EncryptionProcessor.Container; + + (string databaseRid, string containerRid) = await encryptionContainer.GetorUpdateDatabaseAndContainerRidFromCacheAsync( + cancellationToken: cancellationToken); + + string cacheKey = databaseRid + "|" + containerRid + "/" + propertyName; + this.EncryptionSettingCacheByPropertyName.Set(cacheKey, encryptionSettingsForProperty); } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index e4839b7078..451f91a2f9 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -16,15 +16,17 @@ internal sealed class EncryptionTransactionalBatch : TransactionalBatch { private readonly CosmosSerializer cosmosSerializer; private readonly EncryptionProcessor encryptionProcessor; + private readonly EncryptionContainer encryptionContainer; private TransactionalBatch transactionalBatch; public EncryptionTransactionalBatch( TransactionalBatch transactionalBatch, - EncryptionProcessor encryptionProcessor, + EncryptionContainer encryptionContainer, CosmosSerializer cosmosSerializer) { this.transactionalBatch = transactionalBatch ?? throw new ArgumentNullException(nameof(transactionalBatch)); - this.encryptionProcessor = encryptionProcessor ?? throw new ArgumentNullException(nameof(encryptionProcessor)); + this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); + this.encryptionProcessor = encryptionContainer.EncryptionProcessor; this.cosmosSerializer = cosmosSerializer ?? throw new ArgumentNullException(nameof(cosmosSerializer)); } @@ -45,7 +47,7 @@ public override TransactionalBatch CreateItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - streamPayload = this.encryptionProcessor.EncryptAsync( + (streamPayload, _, _ ) = this.encryptionProcessor.EncryptAsync( streamPayload, diagnosticsContext, cancellationToken: default).Result; @@ -100,7 +102,7 @@ public override TransactionalBatch ReplaceItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - streamPayload = this.encryptionProcessor.EncryptAsync( + (streamPayload, _, _) = this.encryptionProcessor.EncryptAsync( streamPayload, diagnosticsContext, default).Result; @@ -131,7 +133,7 @@ public override TransactionalBatch UpsertItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - streamPayload = this.encryptionProcessor.EncryptAsync( + (streamPayload, _, _) = this.encryptionProcessor.EncryptAsync( streamPayload, diagnosticsContext, cancellationToken: default).Result; @@ -150,7 +152,30 @@ public override async Task ExecuteAsync( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(options: null); using (diagnosticsContext.CreateScope("TransactionalBatch.ExecuteAsync")) { - TransactionalBatchResponse response = await this.transactionalBatch.ExecuteAsync(cancellationToken); + TransactionalBatchRequestOptions requestOptions = new TransactionalBatchRequestOptions + { + AddRequestHeaders = this.encryptionContainer.AddHeaders, + }; + + TransactionalBatchResponse response = await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); + + foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) + { + if (transactionalBatchOperationResult.StatusCode != System.Net.HttpStatusCode.Created + && transactionalBatchOperationResult.StatusCode != System.Net.HttpStatusCode.OK + && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) + { + await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + + throw new CosmosException( + "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, + response.StatusCode, + 1024, + response.Headers.ActivityId, + response.Headers.RequestCharge); + } + } + return await this.DecryptTransactionalBatchResponseAsync( response, diagnosticsContext, @@ -165,7 +190,37 @@ public override async Task ExecuteAsync( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(options: null); using (diagnosticsContext.CreateScope("TransactionalBatch.ExecuteAsync.WithRequestOptions")) { - TransactionalBatchResponse response = await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); + TransactionalBatchRequestOptions clonedRequestOptions; + if (requestOptions != null) + { + clonedRequestOptions = (TransactionalBatchRequestOptions)requestOptions.ShallowCopy(); + } + else + { + clonedRequestOptions = new TransactionalBatchRequestOptions(); + } + + clonedRequestOptions.AddRequestHeaders = this.encryptionContainer.AddHeaders; + + TransactionalBatchResponse response = await this.transactionalBatch.ExecuteAsync(clonedRequestOptions, cancellationToken); + + foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) + { + if (transactionalBatchOperationResult.StatusCode != System.Net.HttpStatusCode.Created + && transactionalBatchOperationResult.StatusCode != System.Net.HttpStatusCode.OK + && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) + { + await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + + throw new CosmosException( + "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, + response.StatusCode, + 1024, + response.Headers.ActivityId, + response.Headers.RequestCharge); + } + } + return await this.DecryptTransactionalBatchResponseAsync( response, diagnosticsContext, diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index 0f6b30f0d4..ca34a8b5c6 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -25,8 +25,8 @@ namespace Microsoft.Azure.Cosmos.Encryption.EmulatorTests [TestClass] public class MdeEncryptionTests { - private static readonly EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata("key1", "tempmetadata1"); - private static readonly EncryptionKeyWrapMetadata metadata2 = new EncryptionKeyWrapMetadata("key2", "tempmetadata2"); + private static readonly EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", "key1", "tempmetadata1"); + private static readonly EncryptionKeyWrapMetadata metadata2 = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", "key2", "tempmetadata2"); private static CosmosClient client; private static CosmosClient encryptionCosmosClient; @@ -233,17 +233,17 @@ public async Task EncryptionBulkCrud() public async Task EncryptionCreateClientEncryptionKey() { string cekId = "anotherCek"; - EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata(cekId, "testmetadata1"); + EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", cekId, "testmetadata1"); ClientEncryptionKeyProperties clientEncryptionKeyProperties = await MdeEncryptionTests.CreateClientEncryptionKeyAsync( cekId, metadata1); Assert.AreEqual( - new EncryptionKeyWrapMetadata(name: cekId, value: metadata1.Value), + new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", name: cekId, value: metadata1.Value), clientEncryptionKeyProperties.EncryptionKeyWrapMetadata); // creating another key with same id should fail - metadata1 = new EncryptionKeyWrapMetadata(cekId, "testmetadata2"); + metadata1 = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", cekId, "testmetadata2"); try { @@ -264,22 +264,22 @@ await MdeEncryptionTests.CreateClientEncryptionKeyAsync( public async Task EncryptionRewrapClientEncryptionKey() { string cekId = "rewrapkeytest"; - EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata(cekId, "testmetadata1"); + EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", cekId, "testmetadata1"); ClientEncryptionKeyProperties clientEncryptionKeyProperties = await MdeEncryptionTests.CreateClientEncryptionKeyAsync( cekId, metadata1); Assert.AreEqual( - new EncryptionKeyWrapMetadata(name: cekId, value: metadata1.Value), + new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", name: cekId, value: metadata1.Value), clientEncryptionKeyProperties.EncryptionKeyWrapMetadata); - EncryptionKeyWrapMetadata updatedMetaData = new EncryptionKeyWrapMetadata(cekId, metadata1 + "updatedmetadata"); + EncryptionKeyWrapMetadata updatedMetaData = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", cekId, metadata1 + "updatedmetadata"); clientEncryptionKeyProperties = await MdeEncryptionTests.RewarpClientEncryptionKeyAsync( cekId, updatedMetaData); Assert.AreEqual( - new EncryptionKeyWrapMetadata(name: cekId, value: updatedMetaData.Value), + new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", name: cekId, value: updatedMetaData.Value), clientEncryptionKeyProperties.EncryptionKeyWrapMetadata); } @@ -339,7 +339,7 @@ public async Task EncryptionResourceTokenAuthRestricted() try { string cekId = "testingcekID"; - EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata(cekId, "testmetadata1"); + EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", cekId, "testmetadata1"); ClientEncryptionKeyResponse clientEncrytionKeyResponse = await databaseForRestrictedUser.CreateClientEncryptionKeyAsync( cekId, @@ -354,7 +354,7 @@ public async Task EncryptionResourceTokenAuthRestricted() try { string cekId = "testingcekID"; - EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata(cekId, "testmetadata1" + "updated"); + EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", cekId, "testmetadata1" + "updated"); ClientEncryptionKeyResponse clientEncrytionKeyResponse = await databaseForRestrictedUser.RewrapClientEncryptionKeyAsync( cekId, @@ -382,17 +382,17 @@ public async Task EncryptionFailsWithUnknownClientEncryptionKey() ContainerProperties containerProperties = new ContainerProperties(Guid.NewGuid().ToString(), "/PK") { ClientEncryptionPolicy = clientEncryptionPolicyId }; - Container encryptionContainer = await database.CreateContainerAsync(containerProperties, 400); - try { - await encryptionContainer.InitializeEncryptionAsync(); - await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); - Assert.Fail("Expected item creation should fail since client encryption policy is configured with unknown key."); + Container encryptionContainer = await database.CreateContainerAsync(containerProperties, 400); + Assert.Fail("Creating container with unknownkey policy should have failed. "); } - catch (Exception ex) + catch(CosmosException ex) { - Assert.IsTrue(ex is InvalidOperationException); + if(ex.StatusCode != HttpStatusCode.BadRequest) + { + Assert.Fail("CreateContainerAsync expected to fail with BadRequest statusCode. "); + } } } @@ -827,6 +827,519 @@ public async Task EncryptionRestrictedProperties() } } + [TestMethod] + public async Task EncryptionValidatePolicyRefreshPostContainerDeleteWithBulk() + { + Collection paths = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_IntArray", + ClientEncryptionKeyId = "key1", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_ArrayFormat", + ClientEncryptionKeyId = "key2", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_NestedObjectFormatL1", + ClientEncryptionKeyId = "key1", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + + ContainerProperties containerProperties = new ContainerProperties(Guid.NewGuid().ToString(), "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; + + Container encryptionContainerToDelete = await database.CreateContainerAsync(containerProperties, 400); + await encryptionContainerToDelete.InitializeEncryptionAsync(); + + CosmosClient otherClient = TestCommon.CreateCosmosClient(builder => builder + .WithBulkExecution(true) + .Build()); + + CosmosClient otherEncryptionClient = otherClient.WithEncryption(new TestEncryptionKeyStoreProvider()); + Database otherDatabase = otherEncryptionClient.GetDatabase(MdeEncryptionTests.database.Id); + + Container otherEncryptionContainer = otherDatabase.GetContainer(encryptionContainerToDelete.Id); + + await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer); + + // Client 1 Deletes the Container referenced in Client 2 and Recreate with different policy + using (await database.GetContainer(encryptionContainerToDelete.Id).DeleteContainerStreamAsync()) + { } + + paths = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_StringFormat", + ClientEncryptionKeyId = "key1", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_DateFormat", + ClientEncryptionKeyId = "key1", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_BoolFormat", + ClientEncryptionKeyId = "key2", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + + containerProperties = new ContainerProperties(encryptionContainerToDelete.Id, "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; + + ContainerResponse containerResponse = await database.CreateContainerAsync(containerProperties, 400); + + TestDoc docToReplace = await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete); + + docToReplace.Sensitive_StringFormat = "docTobeReplace"; + + TestDoc docToUpsert = await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete); + docToUpsert.Sensitive_StringFormat = "docTobeUpserted"; + + List tasks = new List() + { + MdeEncryptionTests.MdeUpsertItemAsync(otherEncryptionContainer, docToUpsert, HttpStatusCode.OK), + MdeEncryptionTests.MdeReplaceItemAsync(otherEncryptionContainer, docToReplace), + }; + + await Task.WhenAll(tasks); + + tasks = new List() + { + MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainer, docToReplace), + MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainer, docToUpsert), + MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer), + MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer), + }; + + await Task.WhenAll(tasks); + + tasks = new List() + { + MdeEncryptionTests.VerifyItemByReadAsync(encryptionContainerToDelete, docToReplace), + MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete), + MdeEncryptionTests.VerifyItemByReadAsync(encryptionContainerToDelete, docToUpsert), + }; + + await Task.WhenAll(tasks); + } + + [TestMethod] + public async Task EncryptionValidatePolicyRefreshPostContainerDeleteTransactionBatch() + { + Collection paths = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_StringFormat", + ClientEncryptionKeyId = "key1", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_DateFormat", + ClientEncryptionKeyId = "key1", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + + ContainerProperties containerProperties = new ContainerProperties(Guid.NewGuid().ToString(), "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; + + Container encryptionContainerToDelete = await database.CreateContainerAsync(containerProperties, 400); + await encryptionContainerToDelete.InitializeEncryptionAsync(); + + CosmosClient otherClient = TestCommon.CreateCosmosClient(builder => builder + .Build()); + + CosmosClient otherEncryptionClient = otherClient.WithEncryption(new TestEncryptionKeyStoreProvider()); + Database otherDatabase = otherEncryptionClient.GetDatabase(MdeEncryptionTests.database.Id); + + Container otherEncryptionContainer = otherDatabase.GetContainer(encryptionContainerToDelete.Id); + + await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer); + + // Client 1 Deletes the Container referenced in Client 2 and Recreate with different policy + using (await database.GetContainer(encryptionContainerToDelete.Id).DeleteContainerStreamAsync()) + { } + + paths = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_IntArray", + ClientEncryptionKeyId = "key1", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_NestedObjectFormatL1", + ClientEncryptionKeyId = "key1", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + + containerProperties = new ContainerProperties(encryptionContainerToDelete.Id, "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; + + ContainerResponse containerResponse = await database.CreateContainerAsync(containerProperties, 400); + + TestDoc testDoc = await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete); + + string partitionKey = "thePK"; + + TestDoc doc1ToCreate = TestDoc.Create(partitionKey); + TestDoc doc2ToCreate = TestDoc.Create(partitionKey); + + // check w.r.t to Batch if we are able to fail and update the policy. + TransactionalBatchResponse batchResponse = null; + try + { + batchResponse = await otherEncryptionContainer.CreateTransactionalBatch(new Cosmos.PartitionKey(partitionKey)) + .CreateItem(doc1ToCreate) + .CreateItemStream(doc2ToCreate.ToStream()) + .ExecuteAsync(); + + Assert.Fail("CreateTransactionalBatch should have failed. "); + } + catch(CosmosException ex) + { + Assert.AreEqual("Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. ", ex.Message); + } + + // the previous failure would have updated the policy in the cache. + batchResponse = await otherEncryptionContainer.CreateTransactionalBatch(new Cosmos.PartitionKey(partitionKey)) + .CreateItem(doc1ToCreate) + .CreateItemStream(doc2ToCreate.ToStream()) + .ExecuteAsync(); + + Assert.AreEqual(HttpStatusCode.OK, batchResponse.StatusCode); + + TransactionalBatchOperationResult doc1 = batchResponse.GetOperationResultAtIndex(0); + VerifyExpectedDocResponse(doc1ToCreate, doc1.Resource); + + TransactionalBatchOperationResult doc2 = batchResponse.GetOperationResultAtIndex(1); + VerifyExpectedDocResponse(doc2ToCreate, doc2.Resource); + } + + [TestMethod] + public async Task EncryptionValidatePolicyRefreshPostContainerDeleteQuery() + { + Collection paths = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_StringFormat", + ClientEncryptionKeyId = "key1", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_NestedObjectFormatL1", + ClientEncryptionKeyId = "key1", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + + ContainerProperties containerProperties = new ContainerProperties(Guid.NewGuid().ToString(), "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; + + Container encryptionContainerToDelete = await database.CreateContainerAsync(containerProperties, 400); + await encryptionContainerToDelete.InitializeEncryptionAsync(); + + CosmosClient otherClient = TestCommon.CreateCosmosClient(builder => builder + .Build()); + + CosmosClient otherEncryptionClient = otherClient.WithEncryption(new TestEncryptionKeyStoreProvider()); + Database otherDatabase = otherEncryptionClient.GetDatabase(MdeEncryptionTests.database.Id); + + Container otherEncryptionContainer = otherDatabase.GetContainer(encryptionContainerToDelete.Id); + + await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer); + + // Client 1 Deletes the Container referenced in Client 2 and Recreate with different policy + using (await database.GetContainer(encryptionContainerToDelete.Id).DeleteContainerStreamAsync()) + { } + + paths = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_IntArray", + ClientEncryptionKeyId = "key1", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_ArrayFormat", + ClientEncryptionKeyId = "key2", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + + containerProperties = new ContainerProperties(encryptionContainerToDelete.Id, "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; + + ContainerResponse containerResponse = await database.CreateContainerAsync(containerProperties, 400); + + TestDoc testDoc = await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete); + + // check w.r.t to query if we are able to fail and update the policy + try + { + await MdeEncryptionTests.ValidateQueryResultsAsync( + otherEncryptionContainer, + "SELECT * FROM c", + testDoc); + + Assert.Fail("ValidateQueryResultAsync should have failed. "); + } + catch(CosmosException ex) + { + if (ex.SubStatusCode != 1024) + { + Assert.Fail("Query should have failed. "); + } + + Assert.IsTrue(ex.Message.Contains("Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. ")); + } + + // previous failure would have updated the policy in the cache. + await MdeEncryptionTests.ValidateQueryResultsAsync( + otherEncryptionContainer, + "SELECT * FROM c", + testDoc); + } + + [TestMethod] + public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() + { + CosmosClient mainClient = TestCommon.CreateCosmosClient(builder => builder + .Build()); + + EncryptionKeyWrapMetadata keyWrapMetadata = new EncryptionKeyWrapMetadata("custom", "key", "mymetadata1"); + + TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider + { + DataEncryptionKeyCacheTimeToLive = TimeSpan.FromMinutes(30), + }; + + CosmosClient encryptionCosmosClient = mainClient.WithEncryption(testEncryptionKeyStoreProvider); + Database mainDatabase = await encryptionCosmosClient.CreateDatabaseAsync("databaseToBeDeleted"); + + ClientEncryptionKeyResponse clientEncrytionKeyResponse = await mainDatabase.CreateClientEncryptionKeyAsync( + keyWrapMetadata.Name, + DataEncryptionKeyAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256, + keyWrapMetadata); + + Collection originalPaths = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_StringFormat", + ClientEncryptionKeyId = "key", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_ArrayFormat", + ClientEncryptionKeyId = "key", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_NestedObjectFormatL1", + ClientEncryptionKeyId = "key", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(originalPaths); + + ContainerProperties containerProperties = new ContainerProperties("containerToBeDeleted", "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; + + Container encryptionContainerToDelete = await mainDatabase.CreateContainerAsync(containerProperties, 400); + await encryptionContainerToDelete.InitializeEncryptionAsync(); + + await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete); + + CosmosClient otherClient1 = TestCommon.CreateCosmosClient(builder => builder + .Build()); + + TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider2 = new TestEncryptionKeyStoreProvider + { + DataEncryptionKeyCacheTimeToLive = TimeSpan.Zero, + }; + + CosmosClient otherEncryptionClient = otherClient1.WithEncryption(testEncryptionKeyStoreProvider2); + Database otherDatabase = otherEncryptionClient.GetDatabase(mainDatabase.Id); + + Container otherEncryptionContainer = otherDatabase.GetContainer(encryptionContainerToDelete.Id); + + await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer); + + //----------------------- + // Client 1 Deletes the Database and Container referenced in Client 2 and Recreate with different policy + // delete database and recreate with same key name + await mainClient.GetDatabase("databaseToBeDeleted").DeleteStreamAsync(); + + mainDatabase = await encryptionCosmosClient.CreateDatabaseAsync("databaseToBeDeleted"); + + keyWrapMetadata = new EncryptionKeyWrapMetadata("custom", "key", "mymetadata2"); + clientEncrytionKeyResponse = await mainDatabase.CreateClientEncryptionKeyAsync( + keyWrapMetadata.Name, + DataEncryptionKeyAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256, + keyWrapMetadata); + + using (await mainDatabase.GetContainer(encryptionContainerToDelete.Id).DeleteContainerStreamAsync()) + { } + + Collection newModifiedPath = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_IntArray", + ClientEncryptionKeyId = "key", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_DateFormat", + ClientEncryptionKeyId = "key", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_BoolFormat", + ClientEncryptionKeyId = "key", + EncryptionType = "Deterministic", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + clientEncryptionPolicy = new ClientEncryptionPolicy(newModifiedPath); + + containerProperties = new ContainerProperties(encryptionContainerToDelete.Id, "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; + + ContainerResponse containerResponse = await mainDatabase.CreateContainerAsync(containerProperties, 400); + //encryptionContainerToDelete = containerResponse; + //----------------------- + + TestDoc testDoc = await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete); + + await MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainer, testDoc); + + // create new container in other client. + // The test basically validates if the new key created is referenced, Since the other client would have had the old key cached. + // and here we would not hit the incorrect container rid issue. + Collection newModifiedPath2 = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/Sensitive_StringFormat", + ClientEncryptionKeyId = "key", + EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }, + }; + + ClientEncryptionPolicy clientEncryptionPolicy2 = new ClientEncryptionPolicy(newModifiedPath2); + + ContainerProperties containerProperties2 = new ContainerProperties("otherContainer2", "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy2 }; + Container otherEncryptionContainer2 = await otherDatabase.CreateContainerAsync(containerProperties2, 400); + + // create an item + TestDoc newdoc = await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer2); + + CosmosClient otherClient2 = TestCommon.CreateCosmosClient(builder => builder + .WithBulkExecution(true) + .Build()); + + TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider3 = new TestEncryptionKeyStoreProvider + { + DataEncryptionKeyCacheTimeToLive = TimeSpan.FromMinutes(30), + }; + + CosmosClient otherEncryptionClient2 = otherClient2.WithEncryption(testEncryptionKeyStoreProvider3); + Database otherDatabase2 = otherEncryptionClient2.GetDatabase(mainDatabase.Id); + + Container otherEncryptionContainer3 = otherDatabase2.GetContainer(otherEncryptionContainer2.Id); + await MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainer3, newdoc); + + // validate from other client that we indeed are using the key with metadata 2 + Container otherEncryptionContainerFromClient2 = otherDatabase2.GetContainer(encryptionContainerToDelete.Id); + await MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainerFromClient2, testDoc); + + // create new container in other client. TEST END ----------------------------- + + // previous refrenced container. + await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer); + + List tasks = new List() + { + // readback item and create item from other Client. + MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer), + MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainer, testDoc), + }; + + await Task.WhenAll(tasks); + + testDoc = await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer); + + // to be sure if it was indeed encrypted with the new key. + await MdeEncryptionTests.VerifyItemByReadAsync(encryptionContainerToDelete, testDoc); + + await mainClient.GetDatabase("databaseToBeDeleted").DeleteStreamAsync(); + } + [TestMethod] public async Task VerifyKekRevokeHandling() { @@ -844,7 +1357,7 @@ public async Task VerifyKekRevokeHandling() // Once a Dek gets cached and the Kek is revoked, calls to unwrap/wrap keys would fail since KEK is revoked. // The Dek should be rewrapped if the KEK is revoked. // When an access to KeyVault fails, the Dek is fetched from the backend(force refresh to update the stale DEK) and cache is updated. - EncryptionKeyWrapMetadata revokedKekmetadata = new EncryptionKeyWrapMetadata("revokedKek", "revokedKek-metadata"); + EncryptionKeyWrapMetadata revokedKekmetadata = new EncryptionKeyWrapMetadata("TEST_KEYSTORE_PROVIDER", "revokedKek", "revokedKek-metadata"); await database.CreateClientEncryptionKeyAsync( "keywithRevokedKek", @@ -1246,6 +1759,11 @@ private async Task ValidateChangeFeedProcessorResponse( VerifyExpectedDocResponse(testDoc1, changeFeedReturnedDocs[changeFeedReturnedDocs.Count - 2]); VerifyExpectedDocResponse(testDoc2, changeFeedReturnedDocs[changeFeedReturnedDocs.Count - 1]); + + if (leaseDatabase != null) + { + using (await leaseDatabase.DeleteStreamAsync()) { } + } } // One of query or queryDefinition is to be passed in non-null From 0985baecc4aeb25d49403a6bbc0b271f76cbe7f7 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Tue, 20 Apr 2021 14:13:19 +0530 Subject: [PATCH 11/27] Update EncryptionContainer.cs --- Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index 8965e4582d..df29d70bd5 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -159,6 +159,11 @@ internal void AddHeaders(Headers header) header.Add("x-ms-cosmos-intended-collection-rid", ridValue); } + /// + /// Returns a copy of the passed RequestOptions if passed else creates a new ItemRequestOptions. + /// + /// Original ItemRequestOptions + /// ItemRequestOptions object. internal ItemRequestOptions GetClonedItemRequestOptions(ItemRequestOptions itemRequestOptions) { ItemRequestOptions clonedRequestOptions; From fe753440a641c65462abeb7dc9b50da003f861da Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Tue, 20 Apr 2021 14:50:44 +0530 Subject: [PATCH 12/27] Update EncryptionContainer.cs --- Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index df29d70bd5..1a31fde76a 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -160,10 +160,10 @@ internal void AddHeaders(Headers header) } /// - /// Returns a copy of the passed RequestOptions if passed else creates a new ItemRequestOptions. + /// Returns a cloned copy of the passed RequestOptions if passed else creates a new ItemRequestOptions. /// /// Original ItemRequestOptions - /// ItemRequestOptions object. + /// ItemRequestOptions. internal ItemRequestOptions GetClonedItemRequestOptions(ItemRequestOptions itemRequestOptions) { ItemRequestOptions clonedRequestOptions; From 1841a515fc9683bb17171e440dcd08fa45142c02 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Tue, 20 Apr 2021 15:15:18 +0530 Subject: [PATCH 13/27] Update EncryptionCosmosClient.cs --- .../src/EncryptionCosmosClient.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs index 85be88f530..5511f64140 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs @@ -217,11 +217,9 @@ public override Database GetDatabase(string id) public override Container GetContainer(string databaseId, string containerId) { - EncryptionContainer encryptionContainer = new EncryptionContainer( + return new EncryptionContainer( this.cosmosClient.GetContainer(databaseId, containerId), this); - - return encryptionContainer; } public override FeedIterator GetDatabaseQueryIterator( From 8456a919820ab79a5531d678e8dbbd52d7c0a16e Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Tue, 27 Apr 2021 14:45:52 +0530 Subject: [PATCH 14/27] Fixes. --- .../src/Custom/EncryptionContainer.cs | 16 ++ .../src/EncryptionContainer.cs | 268 ++++++++++-------- .../src/EncryptionCosmosClient.cs | 48 +--- .../src/EncryptionDatabase.cs | 6 +- .../src/EncryptionFeedIterator.cs | 16 +- .../src/EncryptionProcessor.cs | 222 +++------------ .../src/EncryptionSettingForProperty.cs | 18 +- .../src/EncryptionSettings.cs | 123 ++++---- .../src/EncryptionTransactionalBatch.cs | 115 +++++--- .../src/QueryDefinitionExtensions.cs | 9 +- .../tests/EmulatorTests/MdeEncryptionTests.cs | 74 +++-- 11 files changed, 429 insertions(+), 486 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Custom/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/Custom/EncryptionContainer.cs index 5d57e2dabe..4b788e4021 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Custom/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/Custom/EncryptionContainer.cs @@ -954,5 +954,21 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManu { throw new NotImplementedException(); } + + public override Task ReadManyItemsStreamAsync( + IReadOnlyList<(string id, PartitionKey partitionKey)> items, + ReadManyRequestOptions readManyRequestOptions = null, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ReadManyItemsAsync( + IReadOnlyList<(string id, PartitionKey partitionKey)> items, + ReadManyRequestOptions readManyRequestOptions = null, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index 1a31fde76a..6d392438aa 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -15,7 +15,11 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionContainer : Container { - public Container Container { get; private set; } + private const string IntendedCollectionHeader = "x-ms-cosmos-intended-collection-rid"; + + private const string IsClientEncryptedHeader = "x-ms-cosmos-is-client-encrypted"; + + public Container Container { get; } public CosmosSerializer CosmosSerializer { get; } @@ -23,10 +27,6 @@ internal sealed class EncryptionContainer : Container public EncryptionCosmosClient EncryptionCosmosClient { get; } - public EncryptionProcessor EncryptionProcessor => this.encryptionProcessorLazy.Value; - - private readonly Lazy encryptionProcessorLazy; - private bool isEncryptionContainerCacheInitDone; private static readonly SemaphoreSlim CacheInitSema = new SemaphoreSlim(1, 1); @@ -46,8 +46,7 @@ public EncryptionContainer( this.CosmosSerializer = this.Database.Client.ClientOptions.Serializer; this.isEncryptionContainerCacheInitDone = false; - this.DatabaseContainerRidCacheByContainerName = new AsyncCache>(); - this.encryptionProcessorLazy = new Lazy(() => new EncryptionProcessor(this, this.EncryptionCosmosClient)); + this.EncryptionSettingsByContainerName = new AsyncCache(); } public override string Id => this.Container.Id; @@ -58,24 +57,16 @@ public EncryptionContainer( public override Database Database => this.Container.Database; - internal AsyncCache> DatabaseContainerRidCacheByContainerName { get; set; } - - internal async Task> FetchDatabaseAndContainerRidAsync(Container container) - { - ContainerResponse resp = await container.ReadContainerAsync(); - string databaseRid = resp.Resource.SelfLink.Split('/').ElementAt(1); - string containerRid = resp.Resource.SelfLink.Split('/').ElementAt(3); - return new Tuple(databaseRid, containerRid); - } + public AsyncCache EncryptionSettingsByContainerName { get; } - internal async Task> GetorUpdateDatabaseAndContainerRidFromCacheAsync( + internal async Task GetorUpdateEncryptionSettingsFromCacheAsync( CancellationToken cancellationToken, bool shouldForceRefresh = false) { - return await this.DatabaseContainerRidCacheByContainerName.GetAsync( + return await this.EncryptionSettingsByContainerName.GetAsync( this.Id, - obsoleteValue: new Tuple(null, null), - singleValueInitFunc: async () => await this.FetchDatabaseAndContainerRidAsync(this.Container), + obsoleteValue: null, + singleValueInitFunc: async () => await EncryptionSettings.GetEncryptionSettingsAsync(this), cancellationToken: cancellationToken, forceRefresh: shouldForceRefresh); } @@ -87,25 +78,19 @@ internal async Task InitEncryptionContainerCacheIfNotInitAsync(CancellationToken return; } + // if we are likely here due to a force refresh, we might as well set it to false, and wait out if there is another thread refreshing the + // settings. When we do get the lock just check if it still needs initialization.This optimizes + // cases where there are several threads trying to force refresh the settings and the key cache. + // (however there could be cases where we might end up with multiple inits) + this.isEncryptionContainerCacheInitDone = false; if (await CacheInitSema.WaitAsync(-1)) { - if (!this.isEncryptionContainerCacheInitDone || shouldForceRefresh) + if (!this.isEncryptionContainerCacheInitDone) { try { - if (shouldForceRefresh && this.isEncryptionContainerCacheInitDone) - { - this.isEncryptionContainerCacheInitDone = false; - } - // if force refreshed, results in the Client Keys and Policies to be refreshed in client cache. - await this.InitContainerCacheAsync(cancellationToken: cancellationToken, shouldForceRefresh: shouldForceRefresh); - - // a forceRefresh here results in refreshing the Encryption Processor EncryptionSetting cache not the Cosmos Client Cache which is done earlier. - await this.EncryptionProcessor.InitEncryptionSettingsIfNotInitializedAsync(shouldForceRefresh: shouldForceRefresh); - - // update the Rid cache. - await this.GetorUpdateDatabaseAndContainerRidFromCacheAsync( + await this.InitContainerCacheAsync( cancellationToken: cancellationToken, shouldForceRefresh: shouldForceRefresh); @@ -123,24 +108,23 @@ await this.GetorUpdateDatabaseAndContainerRidFromCacheAsync( } } - internal async Task InitContainerCacheAsync( + private async Task InitContainerCacheAsync( CancellationToken cancellationToken = default, bool shouldForceRefresh = false) { cancellationToken.ThrowIfCancellationRequested(); - EncryptionCosmosClient encryptionCosmosClient = this.EncryptionCosmosClient; - ClientEncryptionPolicy clientEncryptionPolicy = await encryptionCosmosClient.GetClientEncryptionPolicyAsync( - container: this, + EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, shouldForceRefresh: shouldForceRefresh); - if (clientEncryptionPolicy != null) + if (encryptionSettings.GetClientEncryptionPolicyPaths.Any()) { - foreach (string clientEncryptionKeyId in clientEncryptionPolicy.IncludedPaths.Select(p => p.ClientEncryptionKeyId).Distinct()) + foreach (string propertyName in encryptionSettings.GetClientEncryptionPolicyPaths) { + EncryptionSettingForProperty settingforProperty = encryptionSettings.GetEncryptionSettingForProperty(propertyName); await this.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: clientEncryptionKeyId, + clientEncryptionKeyId: settingforProperty.ClientEncryptionKeyId, container: this, cancellationToken: cancellationToken, shouldForceRefresh: shouldForceRefresh); @@ -148,15 +132,13 @@ await this.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( } } - internal void AddHeaders(Headers header) + internal void SetRequestHeaders(RequestOptions requestOptions, EncryptionSettings encryptionSettings) { - header.Add("x-ms-cosmos-is-client-encrypted", bool.TrueString); - - (_, string ridValue) = this.GetorUpdateDatabaseAndContainerRidFromCacheAsync(cancellationToken: default) - .ConfigureAwait(false) - .GetAwaiter() - .GetResult(); - header.Add("x-ms-cosmos-intended-collection-rid", ridValue); + requestOptions.AddRequestHeaders = (headers) => + { + headers.Add(IsClientEncryptedHeader, bool.TrueString); + headers.Add(IntendedCollectionHeader, encryptionSettings.ContainerRidValue); + }; } /// @@ -245,47 +227,50 @@ private async Task CreateItemHelperAsync( CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { - ItemRequestOptions clonedRequestOptions = requestOptions; + EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + return await this.Container.CreateItemStreamAsync( + streamPayload, + partitionKey, + requestOptions, + cancellationToken); + } - JObject plainItemJobject = null; - bool isEncryptionSuccessful; - (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + streamPayload = await EncryptionProcessor.EncryptAsync( streamPayload, + encryptionSettings, diagnosticsContext, cancellationToken); - // could also mean there was no encryption policy configured. - if (isEncryptionSuccessful) - { - clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - clonedRequestOptions.AddRequestHeaders = this.AddHeaders; - } - ResponseMessage responseMessage = await this.Container.CreateItemStreamAsync( streamPayload, partitionKey, clonedRequestOptions, cancellationToken); - if (responseMessage.StatusCode != System.Net.HttpStatusCode.Created && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { - // set it back upon successful encryption. - clonedRequestOptions.AddRequestHeaders = null; + streamPayload = await EncryptionProcessor.DecryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); // get the latest policy and re-encrypt. await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - streamPayload = this.CosmosSerializer.ToStream(plainItemJobject); - (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + streamPayload = await EncryptionProcessor.EncryptAsync( streamPayload, + encryptionSettings, diagnosticsContext, cancellationToken); - if (isEncryptionSuccessful) - { - clonedRequestOptions.AddRequestHeaders = this.AddHeaders; - } - responseMessage = await this.Container.CreateItemStreamAsync( streamPayload, partitionKey, @@ -293,8 +278,9 @@ private async Task CreateItemHelperAsync( cancellationToken); } - responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( + responseMessage.Content = await EncryptionProcessor.DecryptAsync( responseMessage.Content, + encryptionSettings, diagnosticsContext, cancellationToken); @@ -374,9 +360,18 @@ private async Task ReadItemHelperAsync( CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); + if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + return await this.Container.ReadItemStreamAsync( + id, + partitionKey, + requestOptions, + cancellationToken); + } - clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); ResponseMessage responseMessage = await this.Container.ReadItemStreamAsync( id, @@ -384,10 +379,11 @@ private async Task ReadItemHelperAsync( clonedRequestOptions, cancellationToken); - if (responseMessage.StatusCode != System.Net.HttpStatusCode.OK && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); - + encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); responseMessage = await this.Container.ReadItemStreamAsync( id, partitionKey, @@ -395,8 +391,9 @@ private async Task ReadItemHelperAsync( cancellationToken); } - responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( + responseMessage.Content = await EncryptionProcessor.DecryptAsync( responseMessage.Content, + encryptionSettings, diagnosticsContext, cancellationToken); @@ -483,25 +480,30 @@ private async Task ReplaceItemHelperAsync( CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { - ItemRequestOptions clonedRequestOptions = requestOptions; - if (partitionKey == null) { throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); } - JObject plainItemJobject = null; - bool isEncryptionSuccessful; - (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + return await this.Container.ReplaceItemStreamAsync( + streamPayload, + id, + partitionKey, + requestOptions, + cancellationToken); + } + + streamPayload = await EncryptionProcessor.EncryptAsync( streamPayload, + encryptionSettings, diagnosticsContext, cancellationToken); - if (isEncryptionSuccessful) - { - clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - clonedRequestOptions.AddRequestHeaders = this.AddHeaders; - } + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); ResponseMessage responseMessage = await this.Container.ReplaceItemStreamAsync( streamPayload, @@ -510,22 +512,24 @@ private async Task ReplaceItemHelperAsync( clonedRequestOptions, cancellationToken); - if (responseMessage.StatusCode != System.Net.HttpStatusCode.Created && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { - clonedRequestOptions.AddRequestHeaders = null; + streamPayload = await EncryptionProcessor.DecryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); - streamPayload = this.CosmosSerializer.ToStream(plainItemJobject); - (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + streamPayload = await EncryptionProcessor.EncryptAsync( streamPayload, + encryptionSettings, diagnosticsContext, cancellationToken); - if (isEncryptionSuccessful) - { - clonedRequestOptions.AddRequestHeaders = this.AddHeaders; - } - responseMessage = await this.Container.ReplaceItemStreamAsync( streamPayload, id, @@ -534,8 +538,9 @@ private async Task ReplaceItemHelperAsync( cancellationToken); } - responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( + responseMessage.Content = await EncryptionProcessor.DecryptAsync( responseMessage.Content, + encryptionSettings, diagnosticsContext, cancellationToken); @@ -612,20 +617,24 @@ private async Task UpsertItemHelperAsync( throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); } - ItemRequestOptions clonedRequestOptions = requestOptions; + EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + return await this.Container.UpsertItemStreamAsync( + streamPayload, + partitionKey, + requestOptions, + cancellationToken); + } - bool isEncryptionSuccessful; - JObject plainItemJobject = null; - (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + streamPayload = await EncryptionProcessor.EncryptAsync( streamPayload, + encryptionSettings, diagnosticsContext, cancellationToken); - if (isEncryptionSuccessful) - { - clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - clonedRequestOptions.AddRequestHeaders = this.AddHeaders; - } + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); ResponseMessage responseMessage = await this.Container.UpsertItemStreamAsync( streamPayload, @@ -633,22 +642,25 @@ private async Task UpsertItemHelperAsync( clonedRequestOptions, cancellationToken); - if (responseMessage.StatusCode != System.Net.HttpStatusCode.Created && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { - clonedRequestOptions.AddRequestHeaders = null; + streamPayload = await EncryptionProcessor.DecryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); - streamPayload = this.CosmosSerializer.ToStream(plainItemJobject); - (streamPayload, isEncryptionSuccessful, plainItemJobject) = await this.EncryptionProcessor.EncryptAsync( + encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + streamPayload = await EncryptionProcessor.EncryptAsync( streamPayload, + encryptionSettings, diagnosticsContext, cancellationToken); - if (isEncryptionSuccessful) - { - clonedRequestOptions.AddRequestHeaders = this.AddHeaders; - } - responseMessage = await this.Container.UpsertItemStreamAsync( streamPayload, partitionKey, @@ -656,8 +668,9 @@ private async Task UpsertItemHelperAsync( cancellationToken); } - responseMessage.Content = await this.EncryptionProcessor.DecryptAsync( + responseMessage.Content = await EncryptionProcessor.DecryptAsync( responseMessage.Content, + encryptionSettings, diagnosticsContext, cancellationToken); @@ -743,7 +756,12 @@ public override FeedIterator GetItemQueryIterator( clonedRequestOptions = new QueryRequestOptions(); } - clonedRequestOptions.AddRequestHeaders = this.AddHeaders; + EncryptionSettings encryptionSettings = this.GetorUpdateEncryptionSettingsFromCacheAsync(default) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); return new EncryptionFeedIterator( (EncryptionFeedIterator)this.GetItemQueryStreamIterator( @@ -860,8 +878,10 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( foreach (JObject document in documents) { - JObject decryptedDocument = await this.EncryptionProcessor.DecryptAsync( + EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( document, + encryptionSettings, diagnosticsContext, cancellationToken); @@ -1010,5 +1030,21 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManu { throw new NotImplementedException(); } + + public override Task ReadManyItemsStreamAsync( + IReadOnlyList<(string id, PartitionKey partitionKey)> items, + ReadManyRequestOptions readManyRequestOptions = null, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ReadManyItemsAsync( + IReadOnlyList<(string id, PartitionKey partitionKey)> items, + ReadManyRequestOptions readManyRequestOptions = null, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs index 5511f64140..be6511abb8 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs @@ -17,60 +17,17 @@ internal sealed class EncryptionCosmosClient : CosmosClient { private readonly CosmosClient cosmosClient; - private readonly AsyncCache clientEncryptionPolicyCacheByContainerId; - private readonly AsyncCache clientEncryptionKeyPropertiesCacheByKeyId; public EncryptionCosmosClient(CosmosClient cosmosClient, EncryptionKeyStoreProvider encryptionKeyStoreProvider) { this.cosmosClient = cosmosClient ?? throw new ArgumentNullException(nameof(cosmosClient)); this.EncryptionKeyStoreProvider = encryptionKeyStoreProvider ?? throw new ArgumentNullException(nameof(encryptionKeyStoreProvider)); - this.clientEncryptionPolicyCacheByContainerId = new AsyncCache(); this.clientEncryptionKeyPropertiesCacheByKeyId = new AsyncCache(); } public EncryptionKeyStoreProvider EncryptionKeyStoreProvider { get; } - /// - /// Gets or Adds ClientEncryptionPolicy. The Cache gets seeded initially either via InitializeEncryptionAsync call on the container, - /// or during the the first request to create an item. - /// - /// The container handler to read the policies from. - /// cancellation token - /// force refresh the cache - /// task result - internal async Task GetClientEncryptionPolicyAsync( - Container container, - CancellationToken cancellationToken = default, - bool shouldForceRefresh = false) - { - if (container == null) - { - throw new ArgumentNullException(nameof(container)); - } - - EncryptionContainer encryptionContainer = (EncryptionContainer)container; - - (string databaseRidvalue, string containerRidvalue) = await encryptionContainer.GetorUpdateDatabaseAndContainerRidFromCacheAsync( - cancellationToken: cancellationToken); - - // container Id is unique within a Database. - string cacheKey = databaseRidvalue + "|" + containerRidvalue + container.Database.Id + "/" + container.Id; - - // cache it against Database and Container ID key. - return await this.clientEncryptionPolicyCacheByContainerId.GetAsync( - cacheKey, - obsoleteValue: null, - async () => - { - ContainerResponse containerResponse = await container.ReadContainerAsync(); - ClientEncryptionPolicy clientEncryptionPolicy = containerResponse.Resource.ClientEncryptionPolicy; - return clientEncryptionPolicy; - }, - cancellationToken, - forceRefresh: shouldForceRefresh); - } - internal async Task GetClientEncryptionKeyPropertiesAsync( string clientEncryptionKeyId, Container container, @@ -84,11 +41,8 @@ internal async Task GetClientEncryptionKeyPropert EncryptionContainer encryptionContainer = (EncryptionContainer)container; - (string databaseRidvalue, string containerRidvalue) = await encryptionContainer.GetorUpdateDatabaseAndContainerRidFromCacheAsync( - cancellationToken: cancellationToken); - // Client Encryption key Id is unique within a Database. - string cacheKey = databaseRidvalue + "|" + containerRidvalue + container.Database.Id + "/" + clientEncryptionKeyId; + string cacheKey = container.Database.Id + "/" + clientEncryptionKeyId; return await this.clientEncryptionKeyPropertiesCacheByKeyId.GetAsync( cacheKey, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabase.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabase.cs index 27b80cd76a..77940bfeba 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabase.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabase.cs @@ -198,8 +198,10 @@ public override async Task CreateUserAsync( public override ContainerBuilder DefineContainer(string name, string partitionKeyPath) { - ContainerBuilder containerBuilder = this.database.DefineContainer(name, partitionKeyPath); - return containerBuilder; + return new ContainerBuilder( + this, + name, + partitionKeyPath); } public override async Task DeleteAsync( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs index caa0279139..f75c9eb40a 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs @@ -6,6 +6,8 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; using System.IO; + using System.Linq; + using System.Net; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -13,7 +15,6 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionFeedIterator : FeedIterator { private readonly FeedIterator feedIterator; - private readonly EncryptionProcessor encryptionProcessor; private readonly EncryptionContainer encryptionContainer; public EncryptionFeedIterator( @@ -22,7 +23,6 @@ public EncryptionFeedIterator( { this.feedIterator = feedIterator ?? throw new ArgumentNullException(nameof(feedIterator)); this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); - this.encryptionProcessor = encryptionContainer.EncryptionProcessor; } public override bool HasMoreResults => this.feedIterator.HasMoreResults; @@ -35,8 +35,7 @@ public override async Task ReadNextAsync(CancellationToken canc ResponseMessage responseMessage = await this.feedIterator.ReadNextAsync(cancellationToken); // check for Bad Request and Wrong RID intended and update the cached RID and Client Encryption Policy. - if (responseMessage.StatusCode != System.Net.HttpStatusCode.OK - && responseMessage.StatusCode != System.Net.HttpStatusCode.NotModified + if (responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); @@ -68,6 +67,12 @@ private async Task DeserializeAndDecryptResponseAsync( CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + return content; + } + JObject contentJObj = EncryptionProcessor.BaseSerializer.FromStream(content); JArray results = new JArray(); @@ -84,8 +89,9 @@ private async Task DeserializeAndDecryptResponseAsync( continue; } - JObject decryptedDocument = await this.encryptionProcessor.DecryptAsync( + JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( document, + encryptionSettings, diagnosticsContext, cancellationToken); diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs index a76989834f..500d9619f1 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs @@ -16,126 +16,15 @@ namespace Microsoft.Azure.Cosmos.Encryption using Newtonsoft.Json; using Newtonsoft.Json.Linq; - internal sealed class EncryptionProcessor + internal static class EncryptionProcessor { - private bool isEncryptionSettingsInitDone; - - private static readonly SemaphoreSlim CacheInitSema = new SemaphoreSlim(1, 1); - - /// - /// Gets the container that has items which are to be encrypted. - /// - public Container Container { get; } - - /// - /// Gets the provider that allows interaction with the master keys. - /// - public EncryptionKeyStoreProvider EncryptionKeyStoreProvider => this.EncryptionCosmosClient.EncryptionKeyStoreProvider; - - public ClientEncryptionPolicy ClientEncryptionPolicy { get; private set; } - - public EncryptionCosmosClient EncryptionCosmosClient { get; } - internal static readonly CosmosJsonDotNetSerializer BaseSerializer = new CosmosJsonDotNetSerializer( new JsonSerializerSettings() { DateParseHandling = DateParseHandling.None, }); - internal EncryptionSettings EncryptionSettings { get; } - - public EncryptionProcessor( - Container container, - EncryptionCosmosClient encryptionCosmosClient) - { - this.Container = container ?? throw new ArgumentNullException(nameof(container)); - this.EncryptionCosmosClient = encryptionCosmosClient ?? throw new ArgumentNullException(nameof(encryptionCosmosClient)); - this.isEncryptionSettingsInitDone = false; - this.EncryptionSettings = new EncryptionSettings(this); - } - - /// - /// Builds up and caches the Encryption Setting by getting the cached entries of Client Encryption Policy and the corresponding keys. - /// Sets up the MDE Algorithm for encryption and decryption by initializing the KeyEncryptionKey and ProtectedDataEncryptionKey. - /// - /// cancellation token - /// Task - internal async Task InitializeEncryptionSettingsAsync(CancellationToken cancellationToken = default, bool shouldForceRefresh = false) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (this.isEncryptionSettingsInitDone && !shouldForceRefresh) - { - throw new InvalidOperationException("The Encrypton Processor has already been initialized. "); - } - - // fetch the cached policy. - this.ClientEncryptionPolicy = await this.EncryptionCosmosClient.GetClientEncryptionPolicyAsync( - container: this.Container, - cancellationToken: cancellationToken, - shouldForceRefresh: false); - - // no policy was configured. - if (this.ClientEncryptionPolicy == null) - { - this.isEncryptionSettingsInitDone = true; - return; - } - - // update the property level setting. - foreach (ClientEncryptionIncludedPath propertyToEncrypt in this.ClientEncryptionPolicy.IncludedPaths) - { - EncryptionType encryptionType = this.EncryptionSettings.GetEncryptionTypeForProperty(propertyToEncrypt); - - EncryptionSettingForProperty encryptionSettingsForProperty = new EncryptionSettingForProperty( - propertyToEncrypt.ClientEncryptionKeyId, - encryptionType, - this); - - string propertyName = propertyToEncrypt.Path.Substring(1); - - await this.EncryptionSettings.SetEncryptionSettingForPropertyAsync( - propertyName, - encryptionSettingsForProperty, - cancellationToken); - } - - this.isEncryptionSettingsInitDone = true; - } - - /// - /// Initializes the Encryption Setting for the processor if not initialized or if shouldForceRefresh is true. - /// - /// (Optional) Token to cancel the operation. - /// Task to await. - internal async Task InitEncryptionSettingsIfNotInitializedAsync(CancellationToken cancellationToken = default, bool shouldForceRefresh = false) - { - if (this.isEncryptionSettingsInitDone && !shouldForceRefresh) - { - return; - } - - if (await CacheInitSema.WaitAsync(-1)) - { - if (!this.isEncryptionSettingsInitDone || shouldForceRefresh) - { - try - { - await this.InitializeEncryptionSettingsAsync(cancellationToken, shouldForceRefresh); - } - finally - { - CacheInitSema.Release(1); - } - } - else - { - CacheInitSema.Release(1); - } - } - } - - private void EncryptProperty( + private static void EncryptProperty( JObject itemJObj, JToken propertyValue, AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm) @@ -147,14 +36,14 @@ private void EncryptProperty( { if (jProperty.Value.Type == JTokenType.Object || jProperty.Value.Type == JTokenType.Array) { - this.EncryptProperty( + EncryptProperty( itemJObj, jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); } else { - jProperty.Value = this.SerializeAndEncryptValue(jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); + jProperty.Value = SerializeAndEncryptValue(jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -171,7 +60,7 @@ private void EncryptProperty( { if (jProperty.Value.Type == JTokenType.Object || jProperty.Value.Type == JTokenType.Array) { - this.EncryptProperty( + EncryptProperty( itemJObj, jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); @@ -180,7 +69,7 @@ private void EncryptProperty( // primitive type else { - jProperty.Value = this.SerializeAndEncryptValue(jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); + jProperty.Value = SerializeAndEncryptValue(jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -196,7 +85,7 @@ private void EncryptProperty( // iterates over individual elements if (jArray[i].Type == JTokenType.Object || jArray[i].Type == JTokenType.Array) { - this.EncryptProperty( + EncryptProperty( itemJObj, jArray[i], aeadAes256CbcHmac256EncryptionAlgorithm); @@ -205,7 +94,7 @@ private void EncryptProperty( // primitive type else { - jArray[i] = this.SerializeAndEncryptValue(jArray[i], aeadAes256CbcHmac256EncryptionAlgorithm); + jArray[i] = SerializeAndEncryptValue(jArray[i], aeadAes256CbcHmac256EncryptionAlgorithm); } } } @@ -216,20 +105,20 @@ private void EncryptProperty( { for (int i = 0; i < propertyValue.Count(); i++) { - propertyValue[i] = this.SerializeAndEncryptValue(propertyValue[i], aeadAes256CbcHmac256EncryptionAlgorithm); + propertyValue[i] = SerializeAndEncryptValue(propertyValue[i], aeadAes256CbcHmac256EncryptionAlgorithm); } } } } else { - itemJObj.Property(propertyValue.Path).Value = this.SerializeAndEncryptValue( + itemJObj.Property(propertyValue.Path).Value = SerializeAndEncryptValue( itemJObj.Property(propertyValue.Path).Value, aeadAes256CbcHmac256EncryptionAlgorithm); } } - private JToken SerializeAndEncryptValue( + private static JToken SerializeAndEncryptValue( JToken jToken, AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm) { @@ -246,7 +135,7 @@ private JToken SerializeAndEncryptValue( if (cipherText == null) { - throw new InvalidOperationException($"{nameof(this.SerializeAndEncryptValue)} returned null cipherText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. "); + throw new InvalidOperationException($"{nameof(SerializeAndEncryptValue)} returned null cipherText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. "); } byte[] cipherTextWithTypeMarker = new byte[cipherText.Length + 1]; @@ -260,8 +149,9 @@ private JToken SerializeAndEncryptValue( /// 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. /// - public async Task<(Stream encryptedStream, bool isEncryptionSuccessful, JObject plainItemJobject)> EncryptAsync( + public static async Task EncryptAsync( Stream input, + EncryptionSettings encryptionSettings, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { @@ -272,28 +162,17 @@ private JToken SerializeAndEncryptValue( Debug.Assert(diagnosticsContext != null); - await this.InitEncryptionSettingsIfNotInitializedAsync(cancellationToken); - - if (this.ClientEncryptionPolicy == null) - { - return (input, false, null); - } - JObject itemJObj = EncryptionProcessor.BaseSerializer.FromStream(input); - JObject plainItemObj = (JObject)itemJObj.DeepClone(); - - foreach (ClientEncryptionIncludedPath pathToEncrypt in this.ClientEncryptionPolicy.IncludedPaths) + foreach (string propertyName in encryptionSettings.GetClientEncryptionPolicyPaths) { - string propertyName = pathToEncrypt.Path.Substring(1); - // possibly a wrong path configured in the Client Encryption Policy, ignore. if (!itemJObj.TryGetValue(propertyName, out JToken propertyValue)) { continue; } - EncryptionSettingForProperty settingforProperty = await this.EncryptionSettings.GetEncryptionSettingForPropertyAsync(propertyName, cancellationToken); + EncryptionSettingForProperty settingforProperty = encryptionSettings.GetEncryptionSettingForProperty(propertyName); if (settingforProperty == null) { @@ -302,17 +181,17 @@ private JToken SerializeAndEncryptValue( AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingforProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); - this.EncryptProperty( + EncryptProperty( itemJObj, propertyValue, aeadAes256CbcHmac256EncryptionAlgorithm); } input.Dispose(); - return (EncryptionProcessor.BaseSerializer.ToStream(itemJObj), true, plainItemObj); + return EncryptionProcessor.BaseSerializer.ToStream(itemJObj); } - private JToken DecryptAndDeserializeValue( + private static JToken DecryptAndDeserializeValue( JToken jToken, AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm) { @@ -330,7 +209,7 @@ private JToken DecryptAndDeserializeValue( if (plainText == null) { - throw new InvalidOperationException($"{nameof(this.DecryptAndDeserializeValue)} returned null plainText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Decrypt)}. "); + throw new InvalidOperationException($"{nameof(DecryptAndDeserializeValue)} returned null plainText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Decrypt)}. "); } return DeserializeAndAddProperty( @@ -338,7 +217,7 @@ private JToken DecryptAndDeserializeValue( (TypeMarker)cipherTextWithTypeMarker[0]); } - private void DecryptProperty( + private static void DecryptProperty( JObject itemJObj, AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm, string propertyName, @@ -350,7 +229,7 @@ private void DecryptProperty( { if (jProperty.Value.Type == JTokenType.Object || jProperty.Value.Type == JTokenType.Array) { - this.DecryptProperty( + DecryptProperty( itemJObj, aeadAes256CbcHmac256EncryptionAlgorithm, jProperty.Name, @@ -358,7 +237,7 @@ private void DecryptProperty( } else { - jProperty.Value = this.DecryptAndDeserializeValue( + jProperty.Value = DecryptAndDeserializeValue( jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); } @@ -376,7 +255,7 @@ private void DecryptProperty( { if (jProperty.Value.Type == JTokenType.Object || jProperty.Value.Type == JTokenType.Array) { - this.DecryptProperty( + DecryptProperty( itemJObj, aeadAes256CbcHmac256EncryptionAlgorithm, jProperty.Name, @@ -384,7 +263,7 @@ private void DecryptProperty( } else { - jProperty.Value = this.DecryptAndDeserializeValue( + jProperty.Value = DecryptAndDeserializeValue( jProperty.Value, aeadAes256CbcHmac256EncryptionAlgorithm); } @@ -400,7 +279,7 @@ private void DecryptProperty( // iterates over individual elements if (jArray[i].Type == JTokenType.Object || jArray[i].Type == JTokenType.Array) { - this.DecryptProperty( + DecryptProperty( itemJObj, aeadAes256CbcHmac256EncryptionAlgorithm, jArray[i].Path, @@ -408,7 +287,7 @@ private void DecryptProperty( } else { - jArray[i] = this.DecryptAndDeserializeValue( + jArray[i] = DecryptAndDeserializeValue( jArray[i], aeadAes256CbcHmac256EncryptionAlgorithm); } @@ -421,7 +300,7 @@ private void DecryptProperty( { for (int i = 0; i < propertyValue.Count(); i++) { - propertyValue[i] = this.DecryptAndDeserializeValue( + propertyValue[i] = DecryptAndDeserializeValue( propertyValue[i], aeadAes256CbcHmac256EncryptionAlgorithm); } @@ -430,25 +309,25 @@ private void DecryptProperty( } else { - itemJObj.Property(propertyName).Value = this.DecryptAndDeserializeValue( + itemJObj.Property(propertyName).Value = DecryptAndDeserializeValue( itemJObj.Property(propertyName).Value, aeadAes256CbcHmac256EncryptionAlgorithm); } } - private async Task DecryptObjectAsync( + private static async Task DecryptObjectAsync( JObject document, + EncryptionSettings encryptionSettings, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { Debug.Assert(diagnosticsContext != null); - foreach (ClientEncryptionIncludedPath path in this.ClientEncryptionPolicy.IncludedPaths) + foreach (string propertyName in encryptionSettings.GetClientEncryptionPolicyPaths) { - if (document.TryGetValue(path.Path.Substring(1), out JToken propertyValue)) + if (document.TryGetValue(propertyName, out JToken propertyValue)) { - string propertyName = path.Path.Substring(1); - EncryptionSettingForProperty settingsForProperty = await this.EncryptionSettings.GetEncryptionSettingForPropertyAsync(propertyName, cancellationToken); + EncryptionSettingForProperty settingsForProperty = encryptionSettings.GetEncryptionSettingForProperty(propertyName); if (settingsForProperty == null) { @@ -457,7 +336,7 @@ private async Task DecryptObjectAsync( AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingsForProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); - this.DecryptProperty( + DecryptProperty( document, aeadAes256CbcHmac256EncryptionAlgorithm, propertyName, @@ -473,8 +352,9 @@ private async Task DecryptObjectAsync( /// 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. /// - public async Task DecryptAsync( + public static async Task DecryptAsync( Stream input, + EncryptionSettings encryptionSettings, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { @@ -486,18 +366,11 @@ public async Task DecryptAsync( Debug.Assert(input.CanSeek); Debug.Assert(diagnosticsContext != null); - await this.InitEncryptionSettingsIfNotInitializedAsync(cancellationToken); - - if (this.ClientEncryptionPolicy == null) - { - input.Position = 0; - return input; - } - - JObject itemJObj = this.RetrieveItem(input); + JObject itemJObj = RetrieveItem(input); - await this.DecryptObjectAsync( + await DecryptObjectAsync( itemJObj, + encryptionSettings, diagnosticsContext, cancellationToken); @@ -505,29 +378,24 @@ await this.DecryptObjectAsync( return EncryptionProcessor.BaseSerializer.ToStream(itemJObj); } - public async Task DecryptAsync( + public static async Task DecryptAsync( JObject document, + EncryptionSettings encryptionSettings, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { Debug.Assert(document != null); - await this.InitEncryptionSettingsIfNotInitializedAsync(cancellationToken); - - if (this.ClientEncryptionPolicy == null) - { - return document; - } - - await this.DecryptObjectAsync( + await DecryptObjectAsync( document, + encryptionSettings, diagnosticsContext, cancellationToken); return document; } - private JObject RetrieveItem( + private static JObject RetrieveItem( Stream input) { Debug.Assert(input != null); diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs index 2aed12a903..3ecbd47f9d 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs @@ -17,20 +17,20 @@ internal sealed class EncryptionSettingForProperty public EncryptionType EncryptionType { get; } - public EncryptionProcessor EncryptionProcessor { get; } + private readonly EncryptionContainer encryptionContainer; - public EncryptionSettingForProperty(string clientEncryptionKeyId, EncryptionType encryptionType, EncryptionProcessor encryptionProcessor) + public EncryptionSettingForProperty(string clientEncryptionKeyId, EncryptionType encryptionType, EncryptionContainer encryptionContainer) { this.ClientEncryptionKeyId = clientEncryptionKeyId ?? throw new ArgumentNullException(nameof(clientEncryptionKeyId)); this.EncryptionType = encryptionType; - this.EncryptionProcessor = encryptionProcessor ?? throw new ArgumentNullException(nameof(encryptionProcessor)); + this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); } internal async Task BuildEncryptionAlgorithmForSettingAsync(CancellationToken cancellationToken) { - ClientEncryptionKeyProperties clientEncryptionKeyProperties = await this.EncryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( + ClientEncryptionKeyProperties clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: this.ClientEncryptionKeyId, - container: this.EncryptionProcessor.Container, + container: this.encryptionContainer, cancellationToken: cancellationToken, shouldForceRefresh: false); @@ -42,7 +42,7 @@ internal async Task BuildEncryptionAlgo // Here a request is sent out to unwrap using the Master Key configured via the Key Encryption Key. protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( clientEncryptionKeyProperties, - this.EncryptionProcessor.EncryptionKeyStoreProvider, + this.encryptionContainer.EncryptionCosmosClient.EncryptionKeyStoreProvider, this.ClientEncryptionKeyId); } catch (RequestFailedException ex) @@ -52,16 +52,16 @@ internal async Task BuildEncryptionAlgo // This is based on the AKV provider implementaion so we expect a RequestFailedException in case other providers are used in unwrap implementation. if (ex.Status == (int)HttpStatusCode.Forbidden) { - clientEncryptionKeyProperties = await this.EncryptionProcessor.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( + clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: this.ClientEncryptionKeyId, - container: this.EncryptionProcessor.Container, + container: this.encryptionContainer, cancellationToken: cancellationToken, shouldForceRefresh: true); // just bail out if this fails. protectedDataEncryptionKey = this.BuildProtectedDataEncryptionKey( clientEncryptionKeyProperties, - this.EncryptionProcessor.EncryptionKeyStoreProvider, + this.encryptionContainer.EncryptionCosmosClient.EncryptionKeyStoreProvider, this.ClientEncryptionKeyId); } else diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index eb8eac83ff..4edc0f16f1 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -5,78 +5,32 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; + using System.Collections.Concurrent; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Encryption.Cryptography; internal sealed class EncryptionSettings { - // The cacheKey used here is combination of Database Rid, Container Rid and property name. - // This allows to access the exact version of setting incase the containers are created with same name. - internal AsyncCache EncryptionSettingCacheByPropertyName { get; } = new AsyncCache(); + private readonly ConcurrentDictionary encryptionSettingsDictByPropertyName = new ConcurrentDictionary(); - public EncryptionProcessor EncryptionProcessor { get; } + private EncryptionContainer encryptionContainer; - public EncryptionSettings(EncryptionProcessor encryptionProcessor) - { - this.EncryptionProcessor = encryptionProcessor ?? throw new ArgumentNullException(nameof(encryptionProcessor)); - } - - internal async Task GetEncryptionSettingForPropertyAsync( - string propertyName, - CancellationToken cancellationToken) - { - EncryptionContainer encryptionContainer = (EncryptionContainer)this.EncryptionProcessor.Container; + private ClientEncryptionPolicy clientEncryptionPolicy; - (string databaseRid, string containerRid) = await encryptionContainer.GetorUpdateDatabaseAndContainerRidFromCacheAsync( - cancellationToken: cancellationToken); - - string cacheKey = databaseRid + "|" + containerRid + "/" + propertyName; - - EncryptionSettingForProperty encryptionSettingsForProperty = await this.EncryptionSettingCacheByPropertyName.GetAsync( - cacheKey, - obsoleteValue: null, - async () => await this.FetchEncryptionSettingForPropertyAsync(propertyName, cancellationToken), - cancellationToken); - - if (encryptionSettingsForProperty == null) - { - return null; - } + public string ContainerRidValue { get; private set; } - return encryptionSettingsForProperty; - } + internal System.Collections.Generic.ICollection GetClientEncryptionPolicyPaths => this.encryptionSettingsDictByPropertyName.Keys; - private async Task FetchEncryptionSettingForPropertyAsync( - string propertyName, - CancellationToken cancellationToken) + internal EncryptionSettingForProperty GetEncryptionSettingForProperty(string propertyName) { - ClientEncryptionPolicy clientEncryptionPolicy = await this.EncryptionProcessor.EncryptionCosmosClient.GetClientEncryptionPolicyAsync( - this.EncryptionProcessor.Container, - cancellationToken: cancellationToken, - shouldForceRefresh: false); + this.encryptionSettingsDictByPropertyName.TryGetValue(propertyName, out EncryptionSettingForProperty encryptionSettingsForProperty); - if (clientEncryptionPolicy != null) - { - foreach (ClientEncryptionIncludedPath propertyToEncrypt in clientEncryptionPolicy.IncludedPaths) - { - if (string.Equals(propertyToEncrypt.Path.Substring(1), propertyName)) - { - EncryptionType encryptionType = this.GetEncryptionTypeForProperty(propertyToEncrypt); - - return new EncryptionSettingForProperty( - propertyToEncrypt.ClientEncryptionKeyId, - encryptionType, - this.EncryptionProcessor); - } - } - } - - Console.WriteLine("Could not find for property: {0} \n", propertyName); - return null; + return encryptionSettingsForProperty; } - internal EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPath clientEncryptionIncludedPath) + private EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPath clientEncryptionIncludedPath) { switch (clientEncryptionIncludedPath.EncryptionType) { @@ -91,18 +45,57 @@ internal EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPat } } - internal async Task SetEncryptionSettingForPropertyAsync( + private async Task InitializeEncryptionSettingsAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ContainerResponse containerResponse = await this.encryptionContainer.ReadContainerAsync(); + + // also set the Container Rid. + this.ContainerRidValue = containerResponse.Resource.SelfLink.Split('/').ElementAt(3); + + // set the ClientEncryptionPolicy for the Settings. + this.clientEncryptionPolicy = containerResponse.Resource.ClientEncryptionPolicy; + if (this.clientEncryptionPolicy == null) + { + return this; + } + + // update the property level setting. + foreach (ClientEncryptionIncludedPath propertyToEncrypt in this.clientEncryptionPolicy.IncludedPaths) + { + EncryptionType encryptionType = this.GetEncryptionTypeForProperty(propertyToEncrypt); + + EncryptionSettingForProperty encryptionSettingsForProperty = new EncryptionSettingForProperty( + propertyToEncrypt.ClientEncryptionKeyId, + encryptionType, + this.encryptionContainer); + + string propertyName = propertyToEncrypt.Path.Substring(1); + + this.SetEncryptionSettingForProperty( + propertyName, + encryptionSettingsForProperty); + } + + return this; + } + + private void SetEncryptionSettingForProperty( string propertyName, - EncryptionSettingForProperty encryptionSettingsForProperty, - CancellationToken cancellationToken) + EncryptionSettingForProperty encryptionSettingsForProperty) { - EncryptionContainer encryptionContainer = (EncryptionContainer)this.EncryptionProcessor.Container; + this.encryptionSettingsDictByPropertyName[propertyName] = encryptionSettingsForProperty; + } - (string databaseRid, string containerRid) = await encryptionContainer.GetorUpdateDatabaseAndContainerRidFromCacheAsync( - cancellationToken: cancellationToken); + public static async Task GetEncryptionSettingsAsync(EncryptionContainer encryptionContainer) + { + EncryptionSettings encryptionSettings = new EncryptionSettings + { + encryptionContainer = encryptionContainer, + }; - string cacheKey = databaseRid + "|" + containerRid + "/" + propertyName; - this.EncryptionSettingCacheByPropertyName.Set(cacheKey, encryptionSettingsForProperty); + return await encryptionSettings.InitializeEncryptionSettingsAsync(); } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index 451f91a2f9..12efa876bb 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -7,6 +7,7 @@ namespace Microsoft.Azure.Cosmos.Encryption using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos; @@ -15,7 +16,6 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionTransactionalBatch : TransactionalBatch { private readonly CosmosSerializer cosmosSerializer; - private readonly EncryptionProcessor encryptionProcessor; private readonly EncryptionContainer encryptionContainer; private TransactionalBatch transactionalBatch; @@ -26,7 +26,6 @@ public EncryptionTransactionalBatch( { this.transactionalBatch = transactionalBatch ?? throw new ArgumentNullException(nameof(transactionalBatch)); this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); - this.encryptionProcessor = encryptionContainer.EncryptionProcessor; this.cosmosSerializer = cosmosSerializer ?? throw new ArgumentNullException(nameof(cosmosSerializer)); } @@ -47,10 +46,19 @@ public override TransactionalBatch CreateItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - (streamPayload, _, _ ) = this.encryptionProcessor.EncryptAsync( - streamPayload, - diagnosticsContext, - cancellationToken: default).Result; + EncryptionSettings encryptionSettings = this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(default) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + if (encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + streamPayload = EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken: default).Result; + } } this.transactionalBatch = this.transactionalBatch.CreateItemStream( @@ -102,10 +110,19 @@ public override TransactionalBatch ReplaceItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - (streamPayload, _, _) = this.encryptionProcessor.EncryptAsync( - streamPayload, - diagnosticsContext, - default).Result; + EncryptionSettings encryptionSettings = this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(default) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + if (encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + streamPayload = EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + default).Result; + } } this.transactionalBatch = this.transactionalBatch.ReplaceItemStream( @@ -133,10 +150,19 @@ public override TransactionalBatch UpsertItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - (streamPayload, _, _) = this.encryptionProcessor.EncryptAsync( - streamPayload, - diagnosticsContext, - cancellationToken: default).Result; + EncryptionSettings encryptionSettings = this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(default) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + if (encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + streamPayload = EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken: default).Result; + } } this.transactionalBatch = this.transactionalBatch.UpsertItemStream( @@ -152,17 +178,24 @@ public override async Task ExecuteAsync( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(options: null); using (diagnosticsContext.CreateScope("TransactionalBatch.ExecuteAsync")) { - TransactionalBatchRequestOptions requestOptions = new TransactionalBatchRequestOptions - { - AddRequestHeaders = this.encryptionContainer.AddHeaders, - }; + TransactionalBatchResponse response = null; - TransactionalBatchResponse response = await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + return await this.transactionalBatch.ExecuteAsync(cancellationToken); + } + else + { + TransactionalBatchRequestOptions requestOptions = new TransactionalBatchRequestOptions(); + this.encryptionContainer.SetRequestHeaders(requestOptions, encryptionSettings); + response = await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); + } foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) { - if (transactionalBatchOperationResult.StatusCode != System.Net.HttpStatusCode.Created - && transactionalBatchOperationResult.StatusCode != System.Net.HttpStatusCode.OK + // FIXME this should return BadRequest and not (-1), requires a backend fix. + if (transactionalBatchOperationResult.StatusCode == (System.Net.HttpStatusCode)(-1) && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) { await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); @@ -178,6 +211,7 @@ public override async Task ExecuteAsync( return await this.DecryptTransactionalBatchResponseAsync( response, + encryptionSettings, diagnosticsContext, cancellationToken); } @@ -190,24 +224,33 @@ public override async Task ExecuteAsync( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(options: null); using (diagnosticsContext.CreateScope("TransactionalBatch.ExecuteAsync.WithRequestOptions")) { - TransactionalBatchRequestOptions clonedRequestOptions; - if (requestOptions != null) + TransactionalBatchResponse response = null; + + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) { - clonedRequestOptions = (TransactionalBatchRequestOptions)requestOptions.ShallowCopy(); + return await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); } else { - clonedRequestOptions = new TransactionalBatchRequestOptions(); - } - - clonedRequestOptions.AddRequestHeaders = this.encryptionContainer.AddHeaders; + TransactionalBatchRequestOptions clonedRequestOptions; + if (requestOptions != null) + { + clonedRequestOptions = (TransactionalBatchRequestOptions)requestOptions.ShallowCopy(); + } + else + { + clonedRequestOptions = new TransactionalBatchRequestOptions(); + } - TransactionalBatchResponse response = await this.transactionalBatch.ExecuteAsync(clonedRequestOptions, cancellationToken); + this.encryptionContainer.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + response = await this.transactionalBatch.ExecuteAsync(clonedRequestOptions, cancellationToken); + } foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) { - if (transactionalBatchOperationResult.StatusCode != System.Net.HttpStatusCode.Created - && transactionalBatchOperationResult.StatusCode != System.Net.HttpStatusCode.OK + // FIXME this should return BadRequest and not (-1), requires a backend fix. + if (transactionalBatchOperationResult.StatusCode == (System.Net.HttpStatusCode)(-1) && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) { await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); @@ -223,6 +266,7 @@ public override async Task ExecuteAsync( return await this.DecryptTransactionalBatchResponseAsync( response, + encryptionSettings, diagnosticsContext, cancellationToken); } @@ -230,9 +274,15 @@ public override async Task ExecuteAsync( private async Task DecryptTransactionalBatchResponseAsync( TransactionalBatchResponse response, + EncryptionSettings encryptionSettings, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { + if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + { + return response; + } + List decryptedTransactionalBatchOperationResults = new List(); for (int index = 0; index < response.Count; index++) @@ -241,8 +291,9 @@ private async Task DecryptTransactionalBatchResponse if (response.IsSuccessStatusCode && result.ResourceStream != null) { - Stream decryptedStream = await this.encryptionProcessor.DecryptAsync( + Stream decryptedStream = await EncryptionProcessor.DecryptAsync( result.ResourceStream, + encryptionSettings, diagnosticsContext, cancellationToken); diff --git a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs index d7ffb1c761..de12b5b190 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs @@ -77,14 +77,9 @@ public static async Task AddParameterAsync( EncryptionContainer encryptionContainer = (EncryptionContainer)encryptionQueryDefinition.Container; Stream valueStream = encryptionContainer.CosmosSerializer.ToStream(value); - // not really required, but will have things setup for subsequent queries or operations on this Container if the Container was never init - // or if this was the first operation carried out on this container. - await encryptionContainer.EncryptionProcessor.InitEncryptionSettingsIfNotInitializedAsync(cancellationToken); - // get the path's encryption setting. - EncryptionSettingForProperty settingsForProperty = await encryptionContainer.EncryptionProcessor.EncryptionSettings.GetEncryptionSettingForPropertyAsync( - path.Substring(1), - cancellationToken); + EncryptionSettings encryptionSettings = await encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettingForProperty settingsForProperty = encryptionSettings.GetEncryptionSettingForProperty(path.Substring(1)); if (settingsForProperty == null) { diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index ca34a8b5c6..c83ddbfaf2 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -836,7 +836,7 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteWithBulk() { Path = "/Sensitive_IntArray", ClientEncryptionKeyId = "key1", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -864,8 +864,9 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteWithBulk() Container encryptionContainerToDelete = await database.CreateContainerAsync(containerProperties, 400); await encryptionContainerToDelete.InitializeEncryptionAsync(); + // FIXME Set WithBulkExecution to true post SDK/Backend fix. CosmosClient otherClient = TestCommon.CreateCosmosClient(builder => builder - .WithBulkExecution(true) + .WithBulkExecution(false) .Build()); CosmosClient otherEncryptionClient = otherClient.WithEncryption(new TestEncryptionKeyStoreProvider()); @@ -885,7 +886,7 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteWithBulk() { Path = "/Sensitive_StringFormat", ClientEncryptionKeyId = "key1", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -920,22 +921,13 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteWithBulk() docToUpsert.Sensitive_StringFormat = "docTobeUpserted"; List tasks = new List() - { + { MdeEncryptionTests.MdeUpsertItemAsync(otherEncryptionContainer, docToUpsert, HttpStatusCode.OK), - MdeEncryptionTests.MdeReplaceItemAsync(otherEncryptionContainer, docToReplace), - }; - - await Task.WhenAll(tasks); - - tasks = new List() - { - MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainer, docToReplace), - MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainer, docToUpsert), - MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer), + MdeEncryptionTests.MdeReplaceItemAsync(otherEncryptionContainer, docToReplace), MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer), }; - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); tasks = new List() { @@ -945,6 +937,14 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteWithBulk() }; await Task.WhenAll(tasks); + + // validate if the right policy was used, by reading them all back. + FeedIterator queryResponseIterator = otherEncryptionContainer.GetItemQueryIterator("select * from c"); + + while (queryResponseIterator.HasMoreResults) + { + FeedResponse readDocs = await queryResponseIterator.ReadNextAsync(); + } } [TestMethod] @@ -956,7 +956,7 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteTransactionB { Path = "/Sensitive_StringFormat", ClientEncryptionKeyId = "key1", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -996,7 +996,7 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteTransactionB { Path = "/Sensitive_IntArray", ClientEncryptionKeyId = "key1", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1029,6 +1029,8 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteTransactionB batchResponse = await otherEncryptionContainer.CreateTransactionalBatch(new Cosmos.PartitionKey(partitionKey)) .CreateItem(doc1ToCreate) .CreateItemStream(doc2ToCreate.ToStream()) + .ReadItem(doc1ToCreate.Id) + .DeleteItem(doc2ToCreate.Id) .ExecuteAsync(); Assert.Fail("CreateTransactionalBatch should have failed. "); @@ -1042,6 +1044,8 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteTransactionB batchResponse = await otherEncryptionContainer.CreateTransactionalBatch(new Cosmos.PartitionKey(partitionKey)) .CreateItem(doc1ToCreate) .CreateItemStream(doc2ToCreate.ToStream()) + .ReadItem(doc1ToCreate.Id) + .DeleteItem(doc2ToCreate.Id) .ExecuteAsync(); Assert.AreEqual(HttpStatusCode.OK, batchResponse.StatusCode); @@ -1062,7 +1066,7 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteQuery() { Path = "/Sensitive_StringFormat", ClientEncryptionKeyId = "key1", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1102,7 +1106,7 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteQuery() { Path = "/Sensitive_IntArray", ClientEncryptionKeyId = "key1", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1177,7 +1181,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() { Path = "/Sensitive_StringFormat", ClientEncryptionKeyId = "key", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1222,7 +1226,6 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer); - //----------------------- // Client 1 Deletes the Database and Container referenced in Client 2 and Recreate with different policy // delete database and recreate with same key name await mainClient.GetDatabase("databaseToBeDeleted").DeleteStreamAsync(); @@ -1244,7 +1247,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() { Path = "/Sensitive_IntArray", ClientEncryptionKeyId = "key", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1269,9 +1272,8 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() containerProperties = new ContainerProperties(encryptionContainerToDelete.Id, "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy }; - ContainerResponse containerResponse = await mainDatabase.CreateContainerAsync(containerProperties, 400); + ContainerResponse containerResponse = await mainDatabase.CreateContainerAsync(containerProperties, 400); //encryptionContainerToDelete = containerResponse; - //----------------------- TestDoc testDoc = await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete); @@ -1286,7 +1288,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() { Path = "/Sensitive_StringFormat", ClientEncryptionKeyId = "key", - EncryptionType = CosmosEncryptionType.Deterministic, + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, }; @@ -1337,6 +1339,22 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() // to be sure if it was indeed encrypted with the new key. await MdeEncryptionTests.VerifyItemByReadAsync(encryptionContainerToDelete, testDoc); + + // validate if the right policy was used, by reading them all back. + FeedIterator queryResponseIterator = encryptionContainerToDelete.GetItemQueryIterator("select * from c"); + + while (queryResponseIterator.HasMoreResults) + { + await queryResponseIterator.ReadNextAsync(); + } + + queryResponseIterator = otherEncryptionContainer.GetItemQueryIterator("select * from c"); + + while (queryResponseIterator.HasMoreResults) + { + await queryResponseIterator.ReadNextAsync(); + } + await mainClient.GetDatabase("databaseToBeDeleted").DeleteStreamAsync(); } @@ -1456,13 +1474,17 @@ public async Task CreateAndDeleteDatabaseWithoutKeys() await encryptionContainer.InitializeEncryptionAsync(); TestDoc testDoc = TestDoc.Create(); - ItemResponse createResponse = await encryptionContainer.CreateItemAsync( testDoc, new PartitionKey(testDoc.PK)); Assert.AreEqual(HttpStatusCode.Created, createResponse.StatusCode); VerifyExpectedDocResponse(testDoc, createResponse.Resource); + await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); + await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); + await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainer); + + await database.DeleteStreamAsync(); } From 2f08396ff94514caa328441b2cc1f838b6bc85b9 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Tue, 27 Apr 2021 17:51:58 +0530 Subject: [PATCH 15/27] Fixes. --- .../src/EncryptionContainer.cs | 67 ++++++++----------- .../src/EncryptionFeedIterator.cs | 10 ++- .../src/EncryptionSettings.cs | 10 +++ .../src/EncryptionTransactionalBatch.cs | 10 ++- .../tests/EmulatorTests/MdeEncryptionTests.cs | 24 +++---- 5 files changed, 66 insertions(+), 55 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index 6d392438aa..286b1f7382 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -61,38 +61,39 @@ public EncryptionContainer( internal async Task GetorUpdateEncryptionSettingsFromCacheAsync( CancellationToken cancellationToken, + EncryptionSettings obsoleteEncryptionSettings = null, bool shouldForceRefresh = false) { return await this.EncryptionSettingsByContainerName.GetAsync( this.Id, - obsoleteValue: null, + obsoleteValue: obsoleteEncryptionSettings, singleValueInitFunc: async () => await EncryptionSettings.GetEncryptionSettingsAsync(this), cancellationToken: cancellationToken, forceRefresh: shouldForceRefresh); } - internal async Task InitEncryptionContainerCacheIfNotInitAsync(CancellationToken cancellationToken, bool shouldForceRefresh = false) + internal async Task InitEncryptionContainerCacheIfNotInitAsync( + CancellationToken cancellationToken, + EncryptionSettings obsoleteEncryptionSettings = null, + bool shouldForceRefresh = false) { if (this.isEncryptionContainerCacheInitDone && !shouldForceRefresh) { return; } - // if we are likely here due to a force refresh, we might as well set it to false, and wait out if there is another thread refreshing the - // settings. When we do get the lock just check if it still needs initialization.This optimizes - // cases where there are several threads trying to force refresh the settings and the key cache. - // (however there could be cases where we might end up with multiple inits) - this.isEncryptionContainerCacheInitDone = false; if (await CacheInitSema.WaitAsync(-1)) { - if (!this.isEncryptionContainerCacheInitDone) + if (!this.isEncryptionContainerCacheInitDone || shouldForceRefresh) { try { + this.isEncryptionContainerCacheInitDone = false; + // if force refreshed, results in the Client Keys and Policies to be refreshed in client cache. - await this.InitContainerCacheAsync( + await this.GetorUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, - shouldForceRefresh: shouldForceRefresh); + obsoleteEncryptionSettings: obsoleteEncryptionSettings); this.isEncryptionContainerCacheInitDone = true; } @@ -108,30 +109,6 @@ await this.InitContainerCacheAsync( } } - private async Task InitContainerCacheAsync( - CancellationToken cancellationToken = default, - bool shouldForceRefresh = false) - { - cancellationToken.ThrowIfCancellationRequested(); - - EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - shouldForceRefresh: shouldForceRefresh); - - if (encryptionSettings.GetClientEncryptionPolicyPaths.Any()) - { - foreach (string propertyName in encryptionSettings.GetClientEncryptionPolicyPaths) - { - EncryptionSettingForProperty settingforProperty = encryptionSettings.GetEncryptionSettingForProperty(propertyName); - await this.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: settingforProperty.ClientEncryptionKeyId, - container: this, - cancellationToken: cancellationToken, - shouldForceRefresh: shouldForceRefresh); - } - } - } - internal void SetRequestHeaders(RequestOptions requestOptions, EncryptionSettings encryptionSettings) { requestOptions.AddRequestHeaders = (headers) => @@ -261,7 +238,11 @@ private async Task CreateItemHelperAsync( cancellationToken); // get the latest policy and re-encrypt. - await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + await this.InitEncryptionContainerCacheIfNotInitAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings, + shouldForceRefresh: true); + encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); @@ -381,7 +362,11 @@ private async Task ReadItemHelperAsync( if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { - await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + await this.InitEncryptionContainerCacheIfNotInitAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings, + shouldForceRefresh: true); + encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); responseMessage = await this.Container.ReadItemStreamAsync( @@ -520,7 +505,10 @@ private async Task ReplaceItemHelperAsync( diagnosticsContext, cancellationToken); - await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + await this.InitEncryptionContainerCacheIfNotInitAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings, + shouldForceRefresh: true); encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); @@ -650,7 +638,10 @@ private async Task UpsertItemHelperAsync( diagnosticsContext, cancellationToken); - await this.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + await this.InitEncryptionContainerCacheIfNotInitAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings, + shouldForceRefresh: true); encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs index f75c9eb40a..7364bfe8ef 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs @@ -34,11 +34,16 @@ public override async Task ReadNextAsync(CancellationToken canc { ResponseMessage responseMessage = await this.feedIterator.ReadNextAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + // check for Bad Request and Wrong RID intended and update the cached RID and Client Encryption Policy. if (responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { - await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings, + shouldForceRefresh: true); throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + responseMessage.ErrorMessage, @@ -52,6 +57,7 @@ public override async Task ReadNextAsync(CancellationToken canc { Stream decryptedContent = await this.DeserializeAndDecryptResponseAsync( responseMessage.Content, + encryptionSettings, diagnosticsContext, cancellationToken); @@ -64,10 +70,10 @@ public override async Task ReadNextAsync(CancellationToken canc private async Task DeserializeAndDecryptResponseAsync( Stream content, + EncryptionSettings encryptionSettings, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) { return content; diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index 4edc0f16f1..c7fb8a2a37 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -61,6 +61,16 @@ private async Task InitializeEncryptionSettingsAsync(Cancell return this; } + // for each of the unique keys in the policy Add it in /Update the cache. + foreach (string clientEncryptionKeyId in this.clientEncryptionPolicy.IncludedPaths.Select(x => x.ClientEncryptionKeyId).Distinct()) + { + await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( + clientEncryptionKeyId: clientEncryptionKeyId, + container: this.encryptionContainer, + cancellationToken: cancellationToken, + shouldForceRefresh: true); + } + // update the property level setting. foreach (ClientEncryptionIncludedPath propertyToEncrypt in this.clientEncryptionPolicy.IncludedPaths) { diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index 12efa876bb..3f8e566e2a 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -198,7 +198,10 @@ public override async Task ExecuteAsync( if (transactionalBatchOperationResult.StatusCode == (System.Net.HttpStatusCode)(-1) && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) { - await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings, + shouldForceRefresh: true); throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, @@ -253,7 +256,10 @@ public override async Task ExecuteAsync( if (transactionalBatchOperationResult.StatusCode == (System.Net.HttpStatusCode)(-1) && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) { - await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken, shouldForceRefresh: true); + await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings, + shouldForceRefresh: true); throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, diff --git a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs index 6b9d02877e..b1a85b2702 100644 --- a/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs +++ b/Microsoft.Azure.Cosmos.Encryption/tests/EmulatorTests/MdeEncryptionTests.cs @@ -891,13 +891,13 @@ public async Task EncryptionValidatePolicyRefreshPostContainerDeleteWithBulk() docToUpsert.Sensitive_StringFormat = "docTobeUpserted"; List tasks = new List() - { + { MdeEncryptionTests.MdeUpsertItemAsync(otherEncryptionContainer, docToUpsert, HttpStatusCode.OK), MdeEncryptionTests.MdeReplaceItemAsync(otherEncryptionContainer, docToReplace), MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer), }; - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); tasks = new List() { @@ -1130,7 +1130,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() CosmosClient mainClient = TestCommon.CreateCosmosClient(builder => builder .Build()); - EncryptionKeyWrapMetadata keyWrapMetadata = new EncryptionKeyWrapMetadata("custom", "key", "mymetadata1"); + EncryptionKeyWrapMetadata keyWrapMetadata = new EncryptionKeyWrapMetadata("custom", "myCek", "mymetadata1"); TestEncryptionKeyStoreProvider testEncryptionKeyStoreProvider = new TestEncryptionKeyStoreProvider { @@ -1150,7 +1150,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() new ClientEncryptionIncludedPath() { Path = "/Sensitive_StringFormat", - ClientEncryptionKeyId = "key", + ClientEncryptionKeyId = "myCek", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1158,7 +1158,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() new ClientEncryptionIncludedPath() { Path = "/Sensitive_ArrayFormat", - ClientEncryptionKeyId = "key", + ClientEncryptionKeyId = "myCek", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1166,7 +1166,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() new ClientEncryptionIncludedPath() { Path = "/Sensitive_NestedObjectFormatL1", - ClientEncryptionKeyId = "key", + ClientEncryptionKeyId = "myCek", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1202,7 +1202,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() mainDatabase = await encryptionCosmosClient.CreateDatabaseAsync("databaseToBeDeleted"); - keyWrapMetadata = new EncryptionKeyWrapMetadata("custom", "key", "mymetadata2"); + keyWrapMetadata = new EncryptionKeyWrapMetadata("custom", "myCek", "mymetadata2"); clientEncrytionKeyResponse = await mainDatabase.CreateClientEncryptionKeyAsync( keyWrapMetadata.Name, DataEncryptionKeyAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256, @@ -1216,7 +1216,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() new ClientEncryptionIncludedPath() { Path = "/Sensitive_IntArray", - ClientEncryptionKeyId = "key", + ClientEncryptionKeyId = "myCek", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1224,7 +1224,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() new ClientEncryptionIncludedPath() { Path = "/Sensitive_DateFormat", - ClientEncryptionKeyId = "key", + ClientEncryptionKeyId = "myCek", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1232,7 +1232,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() new ClientEncryptionIncludedPath() { Path = "/Sensitive_BoolFormat", - ClientEncryptionKeyId = "key", + ClientEncryptionKeyId = "myCek", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1257,7 +1257,7 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() new ClientEncryptionIncludedPath() { Path = "/Sensitive_StringFormat", - ClientEncryptionKeyId = "key", + ClientEncryptionKeyId = "myCek", EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }, @@ -1290,8 +1290,6 @@ public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete() Container otherEncryptionContainerFromClient2 = otherDatabase2.GetContainer(encryptionContainerToDelete.Id); await MdeEncryptionTests.VerifyItemByReadAsync(otherEncryptionContainerFromClient2, testDoc); - // create new container in other client. TEST END ----------------------------- - // previous refrenced container. await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer); From e3c2e2daf3df9aa7d463e351cdd0785fd5b609b1 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Wed, 28 Apr 2021 16:18:55 +0530 Subject: [PATCH 16/27] Fixes as per review comments and Refactoring. --- .../src/EncryptionContainer.cs | 844 +++++++++--------- .../src/EncryptionContainerExtensions.cs | 5 +- .../src/EncryptionCosmosClient.cs | 96 +- .../src/EncryptionDatabase.cs | 2 +- .../src/EncryptionFeedIterator.cs | 17 +- .../src/EncryptionProcessor.cs | 282 +++--- .../src/EncryptionSettingForProperty.cs | 8 +- .../src/EncryptionSettings.cs | 31 +- .../src/EncryptionTransactionalBatch.cs | 63 +- .../Microsoft.Azure.Cosmos.Encryption.csproj | 2 +- .../src/QueryDefinitionExtensions.cs | 2 +- 11 files changed, 672 insertions(+), 680 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index 286b1f7382..9dceab124a 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -15,11 +15,12 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionContainer : Container { + // TODO: Good to have constants available in the Cosmos SDK. Tracked via https://github.com/Azure/azure-cosmos-dotnet-v3/issues/2431 private const string IntendedCollectionHeader = "x-ms-cosmos-intended-collection-rid"; private const string IsClientEncryptedHeader = "x-ms-cosmos-is-client-encrypted"; - public Container Container { get; } + private readonly Container container; public CosmosSerializer CosmosSerializer { get; } @@ -27,10 +28,6 @@ internal sealed class EncryptionContainer : Container public EncryptionCosmosClient EncryptionCosmosClient { get; } - private bool isEncryptionContainerCacheInitDone; - - private static readonly SemaphoreSlim CacheInitSema = new SemaphoreSlim(1, 1); - /// /// All the operations / requests for exercising client-side encryption functionality need to be made using this EncryptionContainer instance. /// @@ -40,104 +37,22 @@ public EncryptionContainer( Container container, EncryptionCosmosClient encryptionCosmosClient) { - this.Container = container ?? throw new ArgumentNullException(nameof(container)); + this.container = container ?? throw new ArgumentNullException(nameof(container)); this.EncryptionCosmosClient = encryptionCosmosClient ?? throw new ArgumentNullException(nameof(container)); this.ResponseFactory = this.Database.Client.ResponseFactory; this.CosmosSerializer = this.Database.Client.ClientOptions.Serializer; - - this.isEncryptionContainerCacheInitDone = false; - this.EncryptionSettingsByContainerName = new AsyncCache(); - } - - public override string Id => this.Container.Id; - - public override Conflicts Conflicts => this.Container.Conflicts; - - public override Scripts.Scripts Scripts => this.Container.Scripts; - - public override Database Database => this.Container.Database; - - public AsyncCache EncryptionSettingsByContainerName { get; } - - internal async Task GetorUpdateEncryptionSettingsFromCacheAsync( - CancellationToken cancellationToken, - EncryptionSettings obsoleteEncryptionSettings = null, - bool shouldForceRefresh = false) - { - return await this.EncryptionSettingsByContainerName.GetAsync( - this.Id, - obsoleteValue: obsoleteEncryptionSettings, - singleValueInitFunc: async () => await EncryptionSettings.GetEncryptionSettingsAsync(this), - cancellationToken: cancellationToken, - forceRefresh: shouldForceRefresh); + this.encryptionSettingsByContainerName = new AsyncCache(); } - internal async Task InitEncryptionContainerCacheIfNotInitAsync( - CancellationToken cancellationToken, - EncryptionSettings obsoleteEncryptionSettings = null, - bool shouldForceRefresh = false) - { - if (this.isEncryptionContainerCacheInitDone && !shouldForceRefresh) - { - return; - } - - if (await CacheInitSema.WaitAsync(-1)) - { - if (!this.isEncryptionContainerCacheInitDone || shouldForceRefresh) - { - try - { - this.isEncryptionContainerCacheInitDone = false; - - // if force refreshed, results in the Client Keys and Policies to be refreshed in client cache. - await this.GetorUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: obsoleteEncryptionSettings); - - this.isEncryptionContainerCacheInitDone = true; - } - finally - { - CacheInitSema.Release(1); - } - } - else - { - CacheInitSema.Release(1); - } - } - } + public override string Id => this.container.Id; - internal void SetRequestHeaders(RequestOptions requestOptions, EncryptionSettings encryptionSettings) - { - requestOptions.AddRequestHeaders = (headers) => - { - headers.Add(IsClientEncryptedHeader, bool.TrueString); - headers.Add(IntendedCollectionHeader, encryptionSettings.ContainerRidValue); - }; - } + public override Conflicts Conflicts => this.container.Conflicts; - /// - /// Returns a cloned copy of the passed RequestOptions if passed else creates a new ItemRequestOptions. - /// - /// Original ItemRequestOptions - /// ItemRequestOptions. - internal ItemRequestOptions GetClonedItemRequestOptions(ItemRequestOptions itemRequestOptions) - { - ItemRequestOptions clonedRequestOptions; + public override Scripts.Scripts Scripts => this.container.Scripts; - if (itemRequestOptions != null) - { - clonedRequestOptions = (ItemRequestOptions)itemRequestOptions.ShallowCopy(); - } - else - { - clonedRequestOptions = new ItemRequestOptions(); - } + public override Database Database => this.container.Database; - return clonedRequestOptions; - } + private readonly AsyncCache encryptionSettingsByContainerName; public override async Task> CreateItemAsync( T item, @@ -197,84 +112,13 @@ public override async Task CreateItemStreamAsync( } } - private async Task CreateItemHelperAsync( - Stream streamPayload, - PartitionKey partitionKey, - ItemRequestOptions requestOptions, - CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) - { - EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) - { - return await this.Container.CreateItemStreamAsync( - streamPayload, - partitionKey, - requestOptions, - cancellationToken); - } - - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - streamPayload = await EncryptionProcessor.EncryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - ResponseMessage responseMessage = await this.Container.CreateItemStreamAsync( - streamPayload, - partitionKey, - clonedRequestOptions, - cancellationToken); - - if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) - { - streamPayload = await EncryptionProcessor.DecryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - // get the latest policy and re-encrypt. - await this.InitEncryptionContainerCacheIfNotInitAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings, - shouldForceRefresh: true); - - encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - streamPayload = await EncryptionProcessor.EncryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - responseMessage = await this.Container.CreateItemStreamAsync( - streamPayload, - partitionKey, - clonedRequestOptions, - cancellationToken); - } - - responseMessage.Content = await EncryptionProcessor.DecryptAsync( - responseMessage.Content, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - return responseMessage; - } - public override Task> DeleteItemAsync( string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.DeleteItemAsync( + return this.container.DeleteItemAsync( id, partitionKey, requestOptions, @@ -287,7 +131,7 @@ public override Task DeleteItemStreamAsync( ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.DeleteItemStreamAsync( + return this.container.DeleteItemStreamAsync( id, partitionKey, requestOptions, @@ -334,57 +178,6 @@ public override async Task ReadItemStreamAsync( } } - private async Task ReadItemHelperAsync( - string id, - PartitionKey partitionKey, - ItemRequestOptions requestOptions, - CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) - { - EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); - if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) - { - return await this.Container.ReadItemStreamAsync( - id, - partitionKey, - requestOptions, - cancellationToken); - } - - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - ResponseMessage responseMessage = await this.Container.ReadItemStreamAsync( - id, - partitionKey, - clonedRequestOptions, - cancellationToken); - - if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) - { - await this.InitEncryptionContainerCacheIfNotInitAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings, - shouldForceRefresh: true); - - encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - responseMessage = await this.Container.ReadItemStreamAsync( - id, - partitionKey, - clonedRequestOptions, - cancellationToken); - } - - responseMessage.Content = await EncryptionProcessor.DecryptAsync( - responseMessage.Content, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - return responseMessage; - } - public override async Task> ReplaceItemAsync( T item, string id, @@ -457,84 +250,6 @@ public override async Task ReplaceItemStreamAsync( } } - private async Task ReplaceItemHelperAsync( - Stream streamPayload, - string id, - PartitionKey partitionKey, - ItemRequestOptions requestOptions, - CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) - { - if (partitionKey == null) - { - throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); - } - - EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) - { - return await this.Container.ReplaceItemStreamAsync( - streamPayload, - id, - partitionKey, - requestOptions, - cancellationToken); - } - - streamPayload = await EncryptionProcessor.EncryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - ResponseMessage responseMessage = await this.Container.ReplaceItemStreamAsync( - streamPayload, - id, - partitionKey, - clonedRequestOptions, - cancellationToken); - - if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) - { - streamPayload = await EncryptionProcessor.DecryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - await this.InitEncryptionContainerCacheIfNotInitAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings, - shouldForceRefresh: true); - - encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - streamPayload = await EncryptionProcessor.EncryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - responseMessage = await this.Container.ReplaceItemStreamAsync( - streamPayload, - id, - partitionKey, - clonedRequestOptions, - cancellationToken); - } - - responseMessage.Content = await EncryptionProcessor.DecryptAsync( - responseMessage.Content, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - return responseMessage; - } - public override async Task> UpsertItemAsync( T item, PartitionKey? partitionKey = null, @@ -593,104 +308,29 @@ public override async Task UpsertItemStreamAsync( } } - private async Task UpsertItemHelperAsync( - Stream streamPayload, - PartitionKey partitionKey, - ItemRequestOptions requestOptions, - CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) + public override TransactionalBatch CreateTransactionalBatch( + PartitionKey partitionKey) { - if (partitionKey == null) - { - throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); - } + return new EncryptionTransactionalBatch( + this.container.CreateTransactionalBatch(partitionKey), + this, + this.CosmosSerializer); + } - EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) - { - return await this.Container.UpsertItemStreamAsync( - streamPayload, - partitionKey, - requestOptions, - cancellationToken); - } - - streamPayload = await EncryptionProcessor.EncryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - ResponseMessage responseMessage = await this.Container.UpsertItemStreamAsync( - streamPayload, - partitionKey, - clonedRequestOptions, - cancellationToken); - - if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) - { - streamPayload = await EncryptionProcessor.DecryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - await this.InitEncryptionContainerCacheIfNotInitAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings, - shouldForceRefresh: true); - - encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - streamPayload = await EncryptionProcessor.EncryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - responseMessage = await this.Container.UpsertItemStreamAsync( - streamPayload, - partitionKey, - clonedRequestOptions, - cancellationToken); - } - - responseMessage.Content = await EncryptionProcessor.DecryptAsync( - responseMessage.Content, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - return responseMessage; - } - - public override TransactionalBatch CreateTransactionalBatch( - PartitionKey partitionKey) - { - return new EncryptionTransactionalBatch( - this.Container.CreateTransactionalBatch(partitionKey), - this, - this.CosmosSerializer); - } - - public override Task DeleteContainerAsync( - ContainerRequestOptions requestOptions = null, - CancellationToken cancellationToken = default) - { - return this.Container.DeleteContainerAsync( - requestOptions, - cancellationToken); - } + public override Task DeleteContainerAsync( + ContainerRequestOptions requestOptions = null, + CancellationToken cancellationToken = default) + { + return this.container.DeleteContainerAsync( + requestOptions, + cancellationToken); + } public override Task DeleteContainerStreamAsync( ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.DeleteContainerStreamAsync( + return this.container.DeleteContainerStreamAsync( requestOptions, cancellationToken); } @@ -700,7 +340,7 @@ public override ChangeFeedProcessorBuilder GetChangeFeedEstimatorBuilder( ChangesEstimationHandler estimationDelegate, TimeSpan? estimationPeriod = null) { - return this.Container.GetChangeFeedEstimatorBuilder( + return this.container.GetChangeFeedEstimatorBuilder( processorName, estimationDelegate, estimationPeriod); @@ -712,7 +352,7 @@ public override IOrderedQueryable GetItemLinqQueryable( QueryRequestOptions requestOptions = null, CosmosLinqSerializerOptions linqSerializerOptions = null) { - return this.Container.GetItemLinqQueryable( + return this.container.GetItemLinqQueryable( allowSynchronousQueryExecution, continuationToken, requestOptions, @@ -737,28 +377,11 @@ public override FeedIterator GetItemQueryIterator( string continuationToken = null, QueryRequestOptions requestOptions = null) { - QueryRequestOptions clonedRequestOptions; - if (requestOptions != null) - { - clonedRequestOptions = (QueryRequestOptions)requestOptions.ShallowCopy(); - } - else - { - clonedRequestOptions = new QueryRequestOptions(); - } - - EncryptionSettings encryptionSettings = this.GetorUpdateEncryptionSettingsFromCacheAsync(default) - .ConfigureAwait(false) - .GetAwaiter() - .GetResult(); - - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - return new EncryptionFeedIterator( (EncryptionFeedIterator)this.GetItemQueryStreamIterator( queryText, continuationToken, - clonedRequestOptions), + requestOptions), this.ResponseFactory); } @@ -766,7 +389,7 @@ public override Task ReadContainerAsync( ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.ReadContainerAsync( + return this.container.ReadContainerAsync( requestOptions, cancellationToken); } @@ -775,7 +398,7 @@ public override Task ReadContainerStreamAsync( ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.ReadContainerStreamAsync( + return this.container.ReadContainerStreamAsync( requestOptions, cancellationToken); } @@ -783,14 +406,14 @@ public override Task ReadContainerStreamAsync( public override Task ReadThroughputAsync( CancellationToken cancellationToken = default) { - return this.Container.ReadThroughputAsync(cancellationToken); + return this.container.ReadThroughputAsync(cancellationToken); } public override Task ReadThroughputAsync( RequestOptions requestOptions, CancellationToken cancellationToken = default) { - return this.Container.ReadThroughputAsync( + return this.container.ReadThroughputAsync( requestOptions, cancellationToken); } @@ -800,7 +423,7 @@ public override Task ReplaceContainerAsync( ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.ReplaceContainerAsync( + return this.container.ReplaceContainerAsync( containerProperties, requestOptions, cancellationToken); @@ -811,7 +434,7 @@ public override Task ReplaceContainerStreamAsync( ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.ReplaceContainerStreamAsync( + return this.container.ReplaceContainerStreamAsync( containerProperties, requestOptions, cancellationToken); @@ -822,7 +445,7 @@ public override Task ReplaceThroughputAsync( RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.ReplaceThroughputAsync( + return this.container.ReplaceThroughputAsync( throughput, requestOptions, cancellationToken); @@ -833,12 +456,23 @@ public override FeedIterator GetItemQueryStreamIterator( string continuationToken = null, QueryRequestOptions requestOptions = null) { + QueryRequestOptions clonedRequestOptions; + if (requestOptions != null) + { + clonedRequestOptions = (QueryRequestOptions)requestOptions.ShallowCopy(); + } + else + { + clonedRequestOptions = new QueryRequestOptions(); + } + return new EncryptionFeedIterator( - this.Container.GetItemQueryStreamIterator( + this.container.GetItemQueryStreamIterator( queryDefinition, continuationToken, - requestOptions), - this); + clonedRequestOptions), + this, + clonedRequestOptions); } public override FeedIterator GetItemQueryStreamIterator( @@ -846,12 +480,23 @@ public override FeedIterator GetItemQueryStreamIterator( string continuationToken = null, QueryRequestOptions requestOptions = null) { + QueryRequestOptions clonedRequestOptions; + if (requestOptions != null) + { + clonedRequestOptions = (QueryRequestOptions)requestOptions.ShallowCopy(); + } + else + { + clonedRequestOptions = new QueryRequestOptions(); + } + return new EncryptionFeedIterator( - this.Container.GetItemQueryStreamIterator( + this.container.GetItemQueryStreamIterator( queryText, continuationToken, - requestOptions), - this); + clonedRequestOptions), + this, + clonedRequestOptions); } public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( @@ -861,7 +506,7 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(null); using (diagnosticsContext.CreateScope("GetChangeFeedProcessorBuilder")) { - ChangeFeedProcessorBuilder changeFeedProcessorBuilder = this.Container.GetChangeFeedProcessorBuilder( + ChangeFeedProcessorBuilder changeFeedProcessorBuilder = this.container.GetChangeFeedProcessorBuilder( processorName, async (IReadOnlyCollection documents, CancellationToken cancellationToken) => { @@ -869,7 +514,7 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( foreach (JObject document in documents) { - EncryptionSettings encryptionSettings = await this.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( document, encryptionSettings, @@ -892,7 +537,7 @@ public override Task ReplaceThroughputAsync( RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - return this.Container.ReplaceThroughputAsync( + return this.container.ReplaceThroughputAsync( throughputProperties, requestOptions, cancellationToken); @@ -901,14 +546,14 @@ public override Task ReplaceThroughputAsync( public override Task> GetFeedRangesAsync( CancellationToken cancellationToken = default) { - return this.Container.GetFeedRangesAsync(cancellationToken); + return this.container.GetFeedRangesAsync(cancellationToken); } public override Task> GetPartitionKeyRangesAsync( FeedRange feedRange, CancellationToken cancellationToken = default) { - return this.Container.GetPartitionKeyRangesAsync(feedRange, cancellationToken); + return this.container.GetPartitionKeyRangesAsync(feedRange, cancellationToken); } public override FeedIterator GetItemQueryStreamIterator( @@ -917,13 +562,24 @@ public override FeedIterator GetItemQueryStreamIterator( string continuationToken, QueryRequestOptions requestOptions = null) { + QueryRequestOptions clonedRequestOptions; + if (requestOptions != null) + { + clonedRequestOptions = (QueryRequestOptions)requestOptions.ShallowCopy(); + } + else + { + clonedRequestOptions = new QueryRequestOptions(); + } + return new EncryptionFeedIterator( - this.Container.GetItemQueryStreamIterator( + this.container.GetItemQueryStreamIterator( feedRange, queryDefinition, continuationToken, - requestOptions), - this); + clonedRequestOptions), + this, + clonedRequestOptions); } public override FeedIterator GetItemQueryIterator( @@ -945,7 +601,7 @@ public override ChangeFeedEstimator GetChangeFeedEstimator( string processorName, Container leaseContainer) { - return this.Container.GetChangeFeedEstimator(processorName, leaseContainer); + return this.container.GetChangeFeedEstimator(processorName, leaseContainer); } public override FeedIterator GetChangeFeedStreamIterator( @@ -953,12 +609,23 @@ public override FeedIterator GetChangeFeedStreamIterator( ChangeFeedMode changeFeedMode, ChangeFeedRequestOptions changeFeedRequestOptions = null) { + ChangeFeedRequestOptions clonedchangeFeedRequestOptions; + if (changeFeedRequestOptions != null) + { + clonedchangeFeedRequestOptions = (ChangeFeedRequestOptions)changeFeedRequestOptions.ShallowCopy(); + } + else + { + clonedchangeFeedRequestOptions = new ChangeFeedRequestOptions(); + } + return new EncryptionFeedIterator( - this.Container.GetChangeFeedStreamIterator( + this.container.GetChangeFeedStreamIterator( changeFeedStartFrom, changeFeedMode, - changeFeedRequestOptions), - this); + clonedchangeFeedRequestOptions), + this, + clonedchangeFeedRequestOptions); } public override FeedIterator GetChangeFeedIterator( @@ -1037,5 +704,318 @@ public override Task> ReadManyItemsAsync( { throw new NotImplementedException(); } + + public async Task GetOrUpdateEncryptionSettingsFromCacheAsync( + CancellationToken cancellationToken, + EncryptionSettings obsoleteEncryptionSettings = null) + { + return await this.encryptionSettingsByContainerName.GetAsync( + this.Id, + obsoleteValue: obsoleteEncryptionSettings, + singleValueInitFunc: async () => await EncryptionSettings.CreateAsync(this), + cancellationToken: cancellationToken); + } + + public void SetRequestHeaders(RequestOptions requestOptions, EncryptionSettings encryptionSettings) + { + requestOptions.AddRequestHeaders = (headers) => + { + headers.Add(IsClientEncryptedHeader, bool.TrueString); + headers.Add(IntendedCollectionHeader, encryptionSettings.ContainerRidValue); + }; + } + + /// + /// Returns a cloned copy of the passed RequestOptions if passed else creates a new ItemRequestOptions. + /// + /// Original ItemRequestOptions + /// ItemRequestOptions. + public ItemRequestOptions GetClonedItemRequestOptions(ItemRequestOptions itemRequestOptions) + { + ItemRequestOptions clonedRequestOptions; + + if (itemRequestOptions != null) + { + clonedRequestOptions = (ItemRequestOptions)itemRequestOptions.ShallowCopy(); + } + else + { + clonedRequestOptions = new ItemRequestOptions(); + } + + return clonedRequestOptions; + } + + private async Task CreateItemHelperAsync( + Stream streamPayload, + PartitionKey partitionKey, + ItemRequestOptions requestOptions, + CosmosDiagnosticsContext diagnosticsContext, + CancellationToken cancellationToken) + { + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.PropertiesToEncrypt.Any()) + { + return await this.container.CreateItemStreamAsync( + streamPayload, + partitionKey, + requestOptions, + cancellationToken); + } + + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + streamPayload = await EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + ResponseMessage responseMessage = await this.container.CreateItemStreamAsync( + streamPayload, + partitionKey, + clonedRequestOptions, + cancellationToken); + + if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + streamPayload = await EncryptionProcessor.DecryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + // get the latest encryption settings and re-encrypt. + encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings); + + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + streamPayload = await EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + responseMessage = await this.container.CreateItemStreamAsync( + streamPayload, + partitionKey, + clonedRequestOptions, + cancellationToken); + } + + responseMessage.Content = await EncryptionProcessor.DecryptAsync( + responseMessage.Content, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + return responseMessage; + } + + private async Task ReadItemHelperAsync( + string id, + PartitionKey partitionKey, + ItemRequestOptions requestOptions, + CosmosDiagnosticsContext diagnosticsContext, + CancellationToken cancellationToken) + { + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); + if (!encryptionSettings.PropertiesToEncrypt.Any()) + { + return await this.container.ReadItemStreamAsync( + id, + partitionKey, + requestOptions, + cancellationToken); + } + + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + ResponseMessage responseMessage = await this.container.ReadItemStreamAsync( + id, + partitionKey, + clonedRequestOptions, + cancellationToken); + + if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + // get the latest encryption settings. + encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings); + + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + responseMessage = await this.container.ReadItemStreamAsync( + id, + partitionKey, + clonedRequestOptions, + cancellationToken); + } + + responseMessage.Content = await EncryptionProcessor.DecryptAsync( + responseMessage.Content, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + return responseMessage; + } + + private async Task ReplaceItemHelperAsync( + Stream streamPayload, + string id, + PartitionKey partitionKey, + ItemRequestOptions requestOptions, + CosmosDiagnosticsContext diagnosticsContext, + CancellationToken cancellationToken) + { + if (partitionKey == null) + { + throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); + } + + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.PropertiesToEncrypt.Any()) + { + return await this.container.ReplaceItemStreamAsync( + streamPayload, + id, + partitionKey, + requestOptions, + cancellationToken); + } + + streamPayload = await EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + ResponseMessage responseMessage = await this.container.ReplaceItemStreamAsync( + streamPayload, + id, + partitionKey, + clonedRequestOptions, + cancellationToken); + + if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + streamPayload = await EncryptionProcessor.DecryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + // get the latest encryption settings. + encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings); + + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + streamPayload = await EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + responseMessage = await this.container.ReplaceItemStreamAsync( + streamPayload, + id, + partitionKey, + clonedRequestOptions, + cancellationToken); + } + + responseMessage.Content = await EncryptionProcessor.DecryptAsync( + responseMessage.Content, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + return responseMessage; + } + + private async Task UpsertItemHelperAsync( + Stream streamPayload, + PartitionKey partitionKey, + ItemRequestOptions requestOptions, + CosmosDiagnosticsContext diagnosticsContext, + CancellationToken cancellationToken) + { + if (partitionKey == null) + { + throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); + } + + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.PropertiesToEncrypt.Any()) + { + return await this.container.UpsertItemStreamAsync( + streamPayload, + partitionKey, + requestOptions, + cancellationToken); + } + + streamPayload = await EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + ResponseMessage responseMessage = await this.container.UpsertItemStreamAsync( + streamPayload, + partitionKey, + clonedRequestOptions, + cancellationToken); + + if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + { + streamPayload = await EncryptionProcessor.DecryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + // get the latest encryption settings. + encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings); + + this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + + streamPayload = await EncryptionProcessor.EncryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + responseMessage = await this.container.UpsertItemStreamAsync( + streamPayload, + partitionKey, + clonedRequestOptions, + cancellationToken); + } + + responseMessage.Content = await EncryptionProcessor.DecryptAsync( + responseMessage.Content, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + return responseMessage; + } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs index 5c032adab1..ac4fc9bac0 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs @@ -42,7 +42,7 @@ public static async Task InitializeEncryptionAsync( if (container is EncryptionContainer encryptionContainer) { - await encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync(cancellationToken); + await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); } return container; @@ -109,7 +109,8 @@ public static FeedIterator ToEncryptionStreamIterator( return new EncryptionFeedIterator( query.ToStreamIterator(), - encryptionContainer); + encryptionContainer, + new RequestOptions()); } /// diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs index be6511abb8..21d2a1a937 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs @@ -28,55 +28,6 @@ public EncryptionCosmosClient(CosmosClient cosmosClient, EncryptionKeyStoreProvi public EncryptionKeyStoreProvider EncryptionKeyStoreProvider { get; } - internal async Task GetClientEncryptionKeyPropertiesAsync( - string clientEncryptionKeyId, - Container container, - CancellationToken cancellationToken = default, - bool shouldForceRefresh = false) - { - if (container == null) - { - throw new ArgumentNullException(nameof(container)); - } - - EncryptionContainer encryptionContainer = (EncryptionContainer)container; - - // Client Encryption key Id is unique within a Database. - string cacheKey = container.Database.Id + "/" + clientEncryptionKeyId; - - return await this.clientEncryptionKeyPropertiesCacheByKeyId.GetAsync( - cacheKey, - obsoleteValue: null, - async () => await this.FetchClientEncryptionKeyPropertiesAsync(container, clientEncryptionKeyId, cancellationToken), - cancellationToken, - forceRefresh: shouldForceRefresh); - } - - internal async Task FetchClientEncryptionKeyPropertiesAsync( - Container container, - string clientEncryptionKeyId, - CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - ClientEncryptionKey clientEncryptionKey = container.Database.GetClientEncryptionKey(clientEncryptionKeyId); - try - { - return await clientEncryptionKey.ReadAsync(cancellationToken: cancellationToken); - } - catch (CosmosException ex) - { - if (ex.StatusCode == HttpStatusCode.NotFound) - { - throw new InvalidOperationException($"Encryption Based Container without Data Encryption Keys. Please make sure you have created the Client Encryption Keys:{ex.Message}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); - } - else - { - throw; - } - } - } - public override CosmosClientOptions ClientOptions => this.cosmosClient.ClientOptions; public override CosmosResponseFactory ResponseFactory => this.cosmosClient.ResponseFactory; @@ -218,5 +169,52 @@ protected override void Dispose(bool disposing) { this.cosmosClient.Dispose(); } + + public async Task GetClientEncryptionKeyPropertiesAsync( + string clientEncryptionKeyId, + EncryptionContainer encryptionContainer, + CancellationToken cancellationToken = default, + bool shouldForceRefresh = false) + { + if (encryptionContainer == null) + { + throw new ArgumentNullException(nameof(encryptionContainer)); + } + + // Client Encryption key Id is unique within a Database. + string cacheKey = encryptionContainer.Database.Id + "/" + clientEncryptionKeyId; + + return await this.clientEncryptionKeyPropertiesCacheByKeyId.GetAsync( + cacheKey, + obsoleteValue: null, + async () => await this.FetchClientEncryptionKeyPropertiesAsync(encryptionContainer, clientEncryptionKeyId, cancellationToken), + cancellationToken, + forceRefresh: shouldForceRefresh); + } + + private async Task FetchClientEncryptionKeyPropertiesAsync( + EncryptionContainer encryptionContainer, + string clientEncryptionKeyId, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ClientEncryptionKey clientEncryptionKey = encryptionContainer.Database.GetClientEncryptionKey(clientEncryptionKeyId); + try + { + return await clientEncryptionKey.ReadAsync(cancellationToken: cancellationToken); + } + catch (CosmosException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + throw new InvalidOperationException($"Encryption Based Container without Data Encryption Keys. Please make sure you have created the Client Encryption Keys:{ex.Message}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); + } + else + { + throw; + } + } + } } } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabase.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabase.cs index 77940bfeba..610fab3f18 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabase.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionDatabase.cs @@ -21,7 +21,7 @@ public EncryptionDatabase(Database database, EncryptionCosmosClient encryptionCo this.EncryptionCosmosClient = encryptionCosmosClient ?? throw new ArgumentNullException(nameof(encryptionCosmosClient)); } - internal EncryptionCosmosClient EncryptionCosmosClient { get; } + public EncryptionCosmosClient EncryptionCosmosClient { get; } public override string Id => this.database.Id; diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs index 7364bfe8ef..280f430878 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs @@ -16,13 +16,16 @@ internal sealed class EncryptionFeedIterator : FeedIterator { private readonly FeedIterator feedIterator; private readonly EncryptionContainer encryptionContainer; + private readonly RequestOptions requestOptions; public EncryptionFeedIterator( FeedIterator feedIterator, - EncryptionContainer encryptionContainer) + EncryptionContainer encryptionContainer, + RequestOptions requestOptions) { this.feedIterator = feedIterator ?? throw new ArgumentNullException(nameof(feedIterator)); this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); + this.requestOptions = requestOptions ?? throw new ArgumentNullException(nameof(requestOptions)); } public override bool HasMoreResults => this.feedIterator.HasMoreResults; @@ -32,18 +35,18 @@ public override async Task ReadNextAsync(CancellationToken canc CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(options: null); using (diagnosticsContext.CreateScope("FeedIterator.ReadNext")) { - ResponseMessage responseMessage = await this.feedIterator.ReadNextAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + this.encryptionContainer.SetRequestHeaders(this.requestOptions, encryptionSettings); - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + ResponseMessage responseMessage = await this.feedIterator.ReadNextAsync(cancellationToken); // check for Bad Request and Wrong RID intended and update the cached RID and Client Encryption Policy. if (responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { - await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync( + await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings, - shouldForceRefresh: true); + obsoleteEncryptionSettings: encryptionSettings); throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + responseMessage.ErrorMessage, @@ -74,7 +77,7 @@ private async Task DeserializeAndDecryptResponseAsync( CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { - if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + if (!encryptionSettings.PropertiesToEncrypt.Any()) { return content; } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs index 500d9619f1..2de96f9551 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs @@ -24,6 +24,146 @@ internal static class EncryptionProcessor DateParseHandling = DateParseHandling.None, }); + internal enum TypeMarker : byte + { + Null = 1, // not used + Boolean = 2, + Double = 3, + Long = 4, + String = 5, + } + + /// + /// 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. + /// + public static async Task EncryptAsync( + Stream input, + EncryptionSettings encryptionSettings, + CosmosDiagnosticsContext diagnosticsContext, + CancellationToken cancellationToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + Debug.Assert(diagnosticsContext != null); + + JObject itemJObj = EncryptionProcessor.BaseSerializer.FromStream(input); + + foreach (string propertyName in encryptionSettings.PropertiesToEncrypt) + { + // possibly a wrong path configured in the Client Encryption Policy, ignore. + if (!itemJObj.TryGetValue(propertyName, out JToken propertyValue)) + { + continue; + } + + EncryptionSettingForProperty settingforProperty = encryptionSettings.GetEncryptionSettingForProperty(propertyName); + + if (settingforProperty == null) + { + throw new ArgumentException($"Invalid Encryption Setting for the Property:{propertyName}. "); + } + + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingforProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); + + EncryptProperty( + itemJObj, + propertyValue, + aeadAes256CbcHmac256EncryptionAlgorithm); + } + + input.Dispose(); + return EncryptionProcessor.BaseSerializer.ToStream(itemJObj); + } + + /// + /// If there isn't any data that needs to be decrypted, 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. + /// + public static async Task DecryptAsync( + Stream input, + EncryptionSettings encryptionSettings, + CosmosDiagnosticsContext diagnosticsContext, + CancellationToken cancellationToken) + { + if (input == null) + { + return input; + } + + Debug.Assert(input.CanSeek); + Debug.Assert(diagnosticsContext != null); + + JObject itemJObj = RetrieveItem(input); + + await DecryptObjectAsync( + itemJObj, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + input.Dispose(); + return EncryptionProcessor.BaseSerializer.ToStream(itemJObj); + } + + public static async Task DecryptAsync( + JObject document, + EncryptionSettings encryptionSettings, + CosmosDiagnosticsContext diagnosticsContext, + CancellationToken cancellationToken) + { + Debug.Assert(document != null); + + await DecryptObjectAsync( + document, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + return document; + } + + internal static (TypeMarker, byte[]) Serialize(JToken propertyValue) + { + SqlSerializerFactory sqlSerializerFactory = new SqlSerializerFactory(); + + // UTF-8 Encoding + SqlVarCharSerializer sqlVarcharSerializer = new SqlVarCharSerializer(size: -1, codePageCharacterEncoding: 65001); + + return propertyValue.Type switch + { + JTokenType.Boolean => (TypeMarker.Boolean, sqlSerializerFactory.GetDefaultSerializer().Serialize(propertyValue.ToObject())), + JTokenType.Float => (TypeMarker.Double, sqlSerializerFactory.GetDefaultSerializer().Serialize(propertyValue.ToObject())), + JTokenType.Integer => (TypeMarker.Long, sqlSerializerFactory.GetDefaultSerializer().Serialize(propertyValue.ToObject())), + JTokenType.String => (TypeMarker.String, sqlVarcharSerializer.Serialize(propertyValue.ToObject())), + _ => throw new InvalidOperationException($"Invalid or Unsupported Data Type Passed : {propertyValue.Type}. "), + }; + } + + internal static JToken DeserializeAndAddProperty( + byte[] serializedBytes, + TypeMarker typeMarker) + { + SqlSerializerFactory sqlSerializerFactory = new SqlSerializerFactory(); + + // UTF-8 Encoding + SqlVarCharSerializer sqlVarcharSerializer = new SqlVarCharSerializer(size: -1, codePageCharacterEncoding: 65001); + + return typeMarker switch + { + TypeMarker.Boolean => sqlSerializerFactory.GetDefaultSerializer().Deserialize(serializedBytes), + TypeMarker.Double => sqlSerializerFactory.GetDefaultSerializer().Deserialize(serializedBytes), + TypeMarker.Long => sqlSerializerFactory.GetDefaultSerializer().Deserialize(serializedBytes), + TypeMarker.String => sqlVarcharSerializer.Deserialize(serializedBytes), + _ => throw new InvalidOperationException($"Invalid or Unsupported Data Type Passed : {typeMarker}. "), + }; + } + private static void EncryptProperty( JObject itemJObj, JToken propertyValue, @@ -144,53 +284,6 @@ private static JToken SerializeAndEncryptValue( return cipherTextWithTypeMarker; } - /// - /// 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. - /// - public static async Task EncryptAsync( - Stream input, - EncryptionSettings encryptionSettings, - CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) - { - if (input == null) - { - throw new ArgumentNullException(nameof(input)); - } - - Debug.Assert(diagnosticsContext != null); - - JObject itemJObj = EncryptionProcessor.BaseSerializer.FromStream(input); - - foreach (string propertyName in encryptionSettings.GetClientEncryptionPolicyPaths) - { - // possibly a wrong path configured in the Client Encryption Policy, ignore. - if (!itemJObj.TryGetValue(propertyName, out JToken propertyValue)) - { - continue; - } - - EncryptionSettingForProperty settingforProperty = encryptionSettings.GetEncryptionSettingForProperty(propertyName); - - if (settingforProperty == null) - { - throw new ArgumentException($"Invalid Encryption Setting for the Property:{propertyName}. "); - } - - AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingforProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); - - EncryptProperty( - itemJObj, - propertyValue, - aeadAes256CbcHmac256EncryptionAlgorithm); - } - - input.Dispose(); - return EncryptionProcessor.BaseSerializer.ToStream(itemJObj); - } - private static JToken DecryptAndDeserializeValue( JToken jToken, AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm) @@ -323,7 +416,7 @@ private static async Task DecryptObjectAsync( { Debug.Assert(diagnosticsContext != null); - foreach (string propertyName in encryptionSettings.GetClientEncryptionPolicyPaths) + foreach (string propertyName in encryptionSettings.PropertiesToEncrypt) { if (document.TryGetValue(propertyName, out JToken propertyValue)) { @@ -347,54 +440,6 @@ private static async Task DecryptObjectAsync( return; } - /// - /// If there isn't any data that needs to be decrypted, 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. - /// - public static async Task DecryptAsync( - Stream input, - EncryptionSettings encryptionSettings, - CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) - { - if (input == null) - { - return input; - } - - Debug.Assert(input.CanSeek); - Debug.Assert(diagnosticsContext != null); - - JObject itemJObj = RetrieveItem(input); - - await DecryptObjectAsync( - itemJObj, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - input.Dispose(); - return EncryptionProcessor.BaseSerializer.ToStream(itemJObj); - } - - public static async Task DecryptAsync( - JObject document, - EncryptionSettings encryptionSettings, - CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) - { - Debug.Assert(document != null); - - await DecryptObjectAsync( - document, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - return document; - } - private static JObject RetrieveItem( Stream input) { @@ -414,50 +459,5 @@ private static JObject RetrieveItem( return itemJObj; } - - internal static (TypeMarker, byte[]) Serialize(JToken propertyValue) - { - SqlSerializerFactory sqlSerializerFactory = new SqlSerializerFactory(); - - // UTF-8 Encoding - SqlVarCharSerializer sqlVarcharSerializer = new SqlVarCharSerializer(size: -1, codePageCharacterEncoding: 65001); - - return propertyValue.Type switch - { - JTokenType.Boolean => (TypeMarker.Boolean, sqlSerializerFactory.GetDefaultSerializer().Serialize(propertyValue.ToObject())), - JTokenType.Float => (TypeMarker.Double, sqlSerializerFactory.GetDefaultSerializer().Serialize(propertyValue.ToObject())), - JTokenType.Integer => (TypeMarker.Long, sqlSerializerFactory.GetDefaultSerializer().Serialize(propertyValue.ToObject())), - JTokenType.String => (TypeMarker.String, sqlVarcharSerializer.Serialize(propertyValue.ToObject())), - _ => throw new InvalidOperationException($"Invalid or Unsupported Data Type Passed : {propertyValue.Type}. "), - }; - } - - internal static JToken DeserializeAndAddProperty( - byte[] serializedBytes, - TypeMarker typeMarker) - { - SqlSerializerFactory sqlSerializerFactory = new SqlSerializerFactory(); - - // UTF-8 Encoding - SqlVarCharSerializer sqlVarcharSerializer = new SqlVarCharSerializer(size: -1, codePageCharacterEncoding: 65001); - - return typeMarker switch - { - TypeMarker.Boolean => sqlSerializerFactory.GetDefaultSerializer().Deserialize(serializedBytes), - TypeMarker.Double => sqlSerializerFactory.GetDefaultSerializer().Deserialize(serializedBytes), - TypeMarker.Long => sqlSerializerFactory.GetDefaultSerializer().Deserialize(serializedBytes), - TypeMarker.String => sqlVarcharSerializer.Deserialize(serializedBytes), - _ => throw new InvalidOperationException($"Invalid or Unsupported Data Type Passed : {typeMarker}. "), - }; - } - - internal enum TypeMarker : byte - { - Null = 1, // not used - Boolean = 2, - Double = 3, - Long = 4, - String = 5, - } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs index 3ecbd47f9d..25e08ff54a 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs @@ -26,11 +26,11 @@ public EncryptionSettingForProperty(string clientEncryptionKeyId, EncryptionType this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); } - internal async Task BuildEncryptionAlgorithmForSettingAsync(CancellationToken cancellationToken) + public async Task BuildEncryptionAlgorithmForSettingAsync(CancellationToken cancellationToken) { ClientEncryptionKeyProperties clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: this.ClientEncryptionKeyId, - container: this.encryptionContainer, + encryptionContainer: this.encryptionContainer, cancellationToken: cancellationToken, shouldForceRefresh: false); @@ -54,7 +54,7 @@ internal async Task BuildEncryptionAlgo { clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: this.ClientEncryptionKeyId, - container: this.encryptionContainer, + encryptionContainer: this.encryptionContainer, cancellationToken: cancellationToken, shouldForceRefresh: true); @@ -77,7 +77,7 @@ internal async Task BuildEncryptionAlgo return aeadAes256CbcHmac256EncryptionAlgorithm; } - internal ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( + public ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( ClientEncryptionKeyProperties clientEncryptionKeyProperties, EncryptionKeyStoreProvider encryptionKeyStoreProvider, string keyId) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index c7fb8a2a37..80161bc28b 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; using System.Collections.Concurrent; + using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,21 +16,33 @@ internal sealed class EncryptionSettings { private readonly ConcurrentDictionary encryptionSettingsDictByPropertyName = new ConcurrentDictionary(); - private EncryptionContainer encryptionContainer; + private readonly EncryptionContainer encryptionContainer; private ClientEncryptionPolicy clientEncryptionPolicy; public string ContainerRidValue { get; private set; } - internal System.Collections.Generic.ICollection GetClientEncryptionPolicyPaths => this.encryptionSettingsDictByPropertyName.Keys; + public ICollection PropertiesToEncrypt => this.encryptionSettingsDictByPropertyName.Keys; - internal EncryptionSettingForProperty GetEncryptionSettingForProperty(string propertyName) + public static Task CreateAsync(EncryptionContainer encryptionContainer) + { + EncryptionSettings encryptionSettings = new EncryptionSettings(encryptionContainer); + + return encryptionSettings.InitializeEncryptionSettingsAsync(); + } + + public EncryptionSettingForProperty GetEncryptionSettingForProperty(string propertyName) { this.encryptionSettingsDictByPropertyName.TryGetValue(propertyName, out EncryptionSettingForProperty encryptionSettingsForProperty); return encryptionSettingsForProperty; } + private EncryptionSettings(EncryptionContainer encryptionContainer) + { + this.encryptionContainer = encryptionContainer; + } + private EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPath clientEncryptionIncludedPath) { switch (clientEncryptionIncludedPath.EncryptionType) @@ -66,7 +79,7 @@ private async Task InitializeEncryptionSettingsAsync(Cancell { await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: clientEncryptionKeyId, - container: this.encryptionContainer, + encryptionContainer: this.encryptionContainer, cancellationToken: cancellationToken, shouldForceRefresh: true); } @@ -97,15 +110,5 @@ private void SetEncryptionSettingForProperty( { this.encryptionSettingsDictByPropertyName[propertyName] = encryptionSettingsForProperty; } - - public static async Task GetEncryptionSettingsAsync(EncryptionContainer encryptionContainer) - { - EncryptionSettings encryptionSettings = new EncryptionSettings - { - encryptionContainer = encryptionContainer, - }; - - return await encryptionSettings.InitializeEncryptionSettingsAsync(); - } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index 3f8e566e2a..8f1b1d8228 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -46,18 +46,21 @@ public override TransactionalBatch CreateItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); - if (encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + if (encryptionSettings.PropertiesToEncrypt.Any()) { streamPayload = EncryptionProcessor.EncryptAsync( streamPayload, encryptionSettings, diagnosticsContext, - cancellationToken: default).Result; + cancellationToken: default) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); } } @@ -110,18 +113,21 @@ public override TransactionalBatch ReplaceItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); - if (encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + if (encryptionSettings.PropertiesToEncrypt.Any()) { streamPayload = EncryptionProcessor.EncryptAsync( streamPayload, encryptionSettings, diagnosticsContext, - default).Result; + default) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); } } @@ -150,18 +156,21 @@ public override TransactionalBatch UpsertItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); - if (encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + if (encryptionSettings.PropertiesToEncrypt.Any()) { streamPayload = EncryptionProcessor.EncryptAsync( streamPayload, encryptionSettings, diagnosticsContext, - cancellationToken: default).Result; + cancellationToken: default) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); } } @@ -180,8 +189,8 @@ public override async Task ExecuteAsync( { TransactionalBatchResponse response = null; - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.transactionalBatch.ExecuteAsync(cancellationToken); } @@ -198,10 +207,9 @@ public override async Task ExecuteAsync( if (transactionalBatchOperationResult.StatusCode == (System.Net.HttpStatusCode)(-1) && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) { - await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync( + await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings, - shouldForceRefresh: true); + obsoleteEncryptionSettings: encryptionSettings); throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, @@ -229,8 +237,8 @@ public override async Task ExecuteAsync( { TransactionalBatchResponse response = null; - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); } @@ -256,10 +264,9 @@ public override async Task ExecuteAsync( if (transactionalBatchOperationResult.StatusCode == (System.Net.HttpStatusCode)(-1) && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) { - await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync( + await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings, - shouldForceRefresh: true); + obsoleteEncryptionSettings: encryptionSettings); throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, @@ -278,13 +285,21 @@ await this.encryptionContainer.InitEncryptionContainerCacheIfNotInitAsync( } } + public override TransactionalBatch PatchItem( + string id, + IReadOnlyList patchOperations, + TransactionalBatchPatchItemRequestOptions requestOptions = null) + { + throw new NotImplementedException(); + } + private async Task DecryptTransactionalBatchResponseAsync( TransactionalBatchResponse response, EncryptionSettings encryptionSettings, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken) { - if (!encryptionSettings.GetClientEncryptionPolicyPaths.Any()) + if (!encryptionSettings.PropertiesToEncrypt.Any()) { return response; } @@ -314,13 +329,5 @@ private async Task DecryptTransactionalBatchResponse response, this.cosmosSerializer); } - - public override TransactionalBatch PatchItem( - string id, - IReadOnlyList patchOperations, - TransactionalBatchPatchItemRequestOptions requestOptions = null) - { - throw new NotImplementedException(); - } } } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj b/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj index c2b23e8dfb..6d2a4e2208 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj +++ b/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj @@ -25,9 +25,9 @@ - + diff --git a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs index de12b5b190..373933b234 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs @@ -78,7 +78,7 @@ public static async Task AddParameterAsync( Stream valueStream = encryptionContainer.CosmosSerializer.ToStream(value); // get the path's encryption setting. - EncryptionSettings encryptionSettings = await encryptionContainer.GetorUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); EncryptionSettingForProperty settingsForProperty = encryptionSettings.GetEncryptionSettingForProperty(path.Substring(1)); if (settingsForProperty == null) From 69b361ad4fb20ddc5cc5712c97c903b835a22281 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Wed, 28 Apr 2021 20:24:53 +0530 Subject: [PATCH 17/27] Refactoring --- .../src/EncryptionContainer.cs | 9 ++++++--- .../src/EncryptionTransactionalBatch.cs | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index 9dceab124a..edfda58b4f 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -506,7 +506,7 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(null); using (diagnosticsContext.CreateScope("GetChangeFeedProcessorBuilder")) { - ChangeFeedProcessorBuilder changeFeedProcessorBuilder = this.container.GetChangeFeedProcessorBuilder( + return this.container.GetChangeFeedProcessorBuilder( processorName, async (IReadOnlyCollection documents, CancellationToken cancellationToken) => { @@ -527,8 +527,6 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( // Call the original passed in delegate await onChangesDelegate(decryptedItems, cancellationToken); }); - - return changeFeedProcessorBuilder; } } @@ -778,6 +776,10 @@ private async Task CreateItemHelperAsync( clonedRequestOptions, cancellationToken); + // This handles the scenario where a container is deleted(say from different Client) and recreated with same Id but with different client encryption policy. + // The idea is to have the container Rid cached and sent out as part of RequestOptions with Container Rid set in "x-ms-cosmos-intended-collection-rid" header. + // So when the container being referenced here gets recreated we would end up with a stale Container Rid and this would result in BadRequest( and a substatus 1024). + // This would allow us to refresh the encryption settings and Container Rid, on the premise that the container recreated could possibly be configured with a new encryption policy. if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { streamPayload = await EncryptionProcessor.DecryptAsync( @@ -791,6 +793,7 @@ private async Task CreateItemHelperAsync( cancellationToken: cancellationToken, obsoleteEncryptionSettings: encryptionSettings); + // updated Rid in the header. this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); streamPayload = await EncryptionProcessor.EncryptAsync( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index 8f1b1d8228..b407ff92a6 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -46,7 +46,7 @@ public override TransactionalBatch CreateItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); @@ -113,7 +113,7 @@ public override TransactionalBatch ReplaceItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); @@ -156,7 +156,7 @@ public override TransactionalBatch UpsertItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); @@ -189,7 +189,7 @@ public override async Task ExecuteAsync( { TransactionalBatchResponse response = null; - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default); if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.transactionalBatch.ExecuteAsync(cancellationToken); @@ -237,7 +237,7 @@ public override async Task ExecuteAsync( { TransactionalBatchResponse response = null; - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default); if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); From 1862729a02547a6ee77f438197a54bc5bf3bd0f8 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Wed, 28 Apr 2021 20:26:53 +0530 Subject: [PATCH 18/27] Update EncryptionContainer.cs --- Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index edfda58b4f..344f911de1 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -778,7 +778,7 @@ private async Task CreateItemHelperAsync( // This handles the scenario where a container is deleted(say from different Client) and recreated with same Id but with different client encryption policy. // The idea is to have the container Rid cached and sent out as part of RequestOptions with Container Rid set in "x-ms-cosmos-intended-collection-rid" header. - // So when the container being referenced here gets recreated we would end up with a stale Container Rid and this would result in BadRequest( and a substatus 1024). + // So when the container being referenced here gets recreated we would end up with a stale encryption settings and container Rid and this would result in BadRequest( and a substatus 1024). // This would allow us to refresh the encryption settings and Container Rid, on the premise that the container recreated could possibly be configured with a new encryption policy. if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) { From 5a5dfec55ac698ca32619f7e20be6ab7d480ddde Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Wed, 28 Apr 2021 20:35:24 +0530 Subject: [PATCH 19/27] Update EncryptionSettingForProperty.cs --- .../src/EncryptionSettingForProperty.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs index 25e08ff54a..b68b73c4f1 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs @@ -77,7 +77,7 @@ public async Task BuildEncryptionAlgori return aeadAes256CbcHmac256EncryptionAlgorithm; } - public ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( + private ProtectedDataEncryptionKey BuildProtectedDataEncryptionKey( ClientEncryptionKeyProperties clientEncryptionKeyProperties, EncryptionKeyStoreProvider encryptionKeyStoreProvider, string keyId) From 1a0b2d65d3bb58af841999ab6538d642bee9c740 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 30 Apr 2021 13:48:55 +0530 Subject: [PATCH 20/27] Fixes as per review comments. --- .../src/EncryptionContainer.cs | 262 +++++++++++------- .../src/EncryptionContainerExtensions.cs | 6 +- .../src/EncryptionCosmosClient.cs | 8 +- .../src/EncryptionFeedIterator.cs | 6 +- .../src/EncryptionProcessor.cs | 40 ++- .../src/EncryptionSettingForProperty.cs | 14 +- .../src/EncryptionSettings.cs | 28 +- .../src/EncryptionTransactionalBatch.cs | 52 +--- .../src/QueryDefinitionExtensions.cs | 20 +- 9 files changed, 255 insertions(+), 181 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index 344f911de1..86e0c65052 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -8,6 +8,7 @@ namespace Microsoft.Azure.Cosmos.Encryption using System.Collections.Generic; using System.IO; using System.Linq; + using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos; @@ -15,10 +16,9 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionContainer : Container { - // TODO: Good to have constants available in the Cosmos SDK. Tracked via https://github.com/Azure/azure-cosmos-dotnet-v3/issues/2431 - private const string IntendedCollectionHeader = "x-ms-cosmos-intended-collection-rid"; + public const string SubStatusHeader = "x-ms-substatus"; - private const string IsClientEncryptedHeader = "x-ms-cosmos-is-client-encrypted"; + public const string IncorrectContainerRidSubStatus = "1024"; private readonly Container container; @@ -73,7 +73,7 @@ public override async Task> CreateItemAsync( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("CreateItem")) { - ResponseMessage responseMessage = null; + ResponseMessage responseMessage; using (Stream itemStream = this.CosmosSerializer.ToStream(item)) { @@ -515,13 +515,34 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( foreach (JObject document in documents) { EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( - document, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - decryptedItems.Add(decryptedDocument.ToObject()); + try + { + JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( + document, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + decryptedItems.Add(decryptedDocument.ToObject()); + } + + // we cannot rely currently on a specific exception, this is due to the fact that the run time issue can be variable, we can hit issue with either Json + // serialization say an item was not encrypted but the policy shows it as encrypted, or we could hit a MicrosoftDataEncryptionException from MDE lib etc. + catch (Exception) + { + // most likely the encryption policy has changed. + encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings); + + JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( + document, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + decryptedItems.Add(decryptedDocument.ToObject()); + } } // Call the original passed in delegate @@ -714,21 +735,12 @@ public async Task GetOrUpdateEncryptionSettingsFromCacheAsyn cancellationToken: cancellationToken); } - public void SetRequestHeaders(RequestOptions requestOptions, EncryptionSettings encryptionSettings) - { - requestOptions.AddRequestHeaders = (headers) => - { - headers.Add(IsClientEncryptedHeader, bool.TrueString); - headers.Add(IntendedCollectionHeader, encryptionSettings.ContainerRidValue); - }; - } - /// /// Returns a cloned copy of the passed RequestOptions if passed else creates a new ItemRequestOptions. /// /// Original ItemRequestOptions /// ItemRequestOptions. - public ItemRequestOptions GetClonedItemRequestOptions(ItemRequestOptions itemRequestOptions) + private static ItemRequestOptions GetClonedItemRequestOptions(ItemRequestOptions itemRequestOptions) { ItemRequestOptions clonedRequestOptions; @@ -749,7 +761,8 @@ private async Task CreateItemHelperAsync( PartitionKey partitionKey, ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + bool shouldRetry = true) { EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) @@ -761,14 +774,21 @@ private async Task CreateItemHelperAsync( cancellationToken); } - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - streamPayload = await EncryptionProcessor.EncryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + ItemRequestOptions clonedRequestOptions = requestOptions; + + // only clone it on the first try. + if (shouldRetry) + { + clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); + } + + encryptionSettings.SetRequestHeaders(clonedRequestOptions); ResponseMessage responseMessage = await this.container.CreateItemStreamAsync( streamPayload, @@ -780,33 +800,30 @@ private async Task CreateItemHelperAsync( // The idea is to have the container Rid cached and sent out as part of RequestOptions with Container Rid set in "x-ms-cosmos-intended-collection-rid" header. // So when the container being referenced here gets recreated we would end up with a stale encryption settings and container Rid and this would result in BadRequest( and a substatus 1024). // This would allow us to refresh the encryption settings and Container Rid, on the premise that the container recreated could possibly be configured with a new encryption policy. - if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + if (shouldRetry && + responseMessage.StatusCode == HttpStatusCode.BadRequest && + string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { - streamPayload = await EncryptionProcessor.DecryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - // get the latest encryption settings and re-encrypt. - encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); - - // updated Rid in the header. - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + // Even though the streamPayload position is expected to be 0, resetting it 0 to be on a safer side. + streamPayload.Position = 0; - streamPayload = await EncryptionProcessor.EncryptAsync( + // Now the streamPayload itself is not disposed off(and hence safe to use it in the below call) since the stream that is passed to CreateItemStreamAsync is a MemoryStream and not the original Stream + // that the user has passed. The call to EncryptAsync reads out the stream(and processes it) and returns a MemoryStream which is eventually cloned in the + // Cosmos SDK and then used. This stream however is to be disposed off as part of ResponseMessage when this gets returned. + streamPayload = await this.DecryptStreamPayloadAndUpdateEncryptionSettingsAsync( streamPayload, encryptionSettings, diagnosticsContext, cancellationToken); - responseMessage = await this.container.CreateItemStreamAsync( - streamPayload, - partitionKey, - clonedRequestOptions, - cancellationToken); + // we try to recreate the item with the StreamPayload(to be encrypted) now that the encryptionSettings would have been updated with latest values if any. + return await this.CreateItemHelperAsync( + streamPayload, + partitionKey, + clonedRequestOptions, + diagnosticsContext, + cancellationToken, + shouldRetry: false); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -823,7 +840,8 @@ private async Task ReadItemHelperAsync( PartitionKey partitionKey, ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + bool shouldRetry = true) { EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) @@ -835,8 +853,15 @@ private async Task ReadItemHelperAsync( cancellationToken); } - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + ItemRequestOptions clonedRequestOptions = requestOptions; + + // only clone it on the first try. + if (shouldRetry) + { + clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); + } + + encryptionSettings.SetRequestHeaders(clonedRequestOptions); ResponseMessage responseMessage = await this.container.ReadItemStreamAsync( id, @@ -844,20 +869,22 @@ private async Task ReadItemHelperAsync( clonedRequestOptions, cancellationToken); - if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + if (shouldRetry && + responseMessage.StatusCode == HttpStatusCode.BadRequest && + string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { // get the latest encryption settings. - encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( + await this.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, obsoleteEncryptionSettings: encryptionSettings); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - responseMessage = await this.container.ReadItemStreamAsync( + return await this.ReadItemHelperAsync( id, partitionKey, clonedRequestOptions, - cancellationToken); + diagnosticsContext, + cancellationToken, + shouldRetry: false); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -875,7 +902,8 @@ private async Task ReplaceItemHelperAsync( PartitionKey partitionKey, ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + bool shouldRetry = true) { if (partitionKey == null) { @@ -899,8 +927,15 @@ private async Task ReplaceItemHelperAsync( diagnosticsContext, cancellationToken); - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + ItemRequestOptions clonedRequestOptions = requestOptions; + + // only clone it on the first try. + if (shouldRetry) + { + clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); + } + + encryptionSettings.SetRequestHeaders(clonedRequestOptions); ResponseMessage responseMessage = await this.container.ReplaceItemStreamAsync( streamPayload, @@ -909,33 +944,25 @@ private async Task ReplaceItemHelperAsync( clonedRequestOptions, cancellationToken); - if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + if (shouldRetry && + responseMessage.StatusCode == HttpStatusCode.BadRequest && + string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { - streamPayload = await EncryptionProcessor.DecryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - // get the latest encryption settings. - encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); - - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - streamPayload = await EncryptionProcessor.EncryptAsync( + streamPayload.Position = 0; + streamPayload = await this.DecryptStreamPayloadAndUpdateEncryptionSettingsAsync( streamPayload, encryptionSettings, diagnosticsContext, cancellationToken); - responseMessage = await this.container.ReplaceItemStreamAsync( + return await this.ReplaceItemHelperAsync( streamPayload, id, partitionKey, clonedRequestOptions, - cancellationToken); + diagnosticsContext, + cancellationToken, + shouldRetry: false); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -952,7 +979,8 @@ private async Task UpsertItemHelperAsync( PartitionKey partitionKey, ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + bool shouldRetry = true) { if (partitionKey == null) { @@ -975,8 +1003,15 @@ private async Task UpsertItemHelperAsync( diagnosticsContext, cancellationToken); - ItemRequestOptions clonedRequestOptions = this.GetClonedItemRequestOptions(requestOptions); - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + ItemRequestOptions clonedRequestOptions = requestOptions; + + // only clone it on the first try. + if (shouldRetry) + { + clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); + } + + encryptionSettings.SetRequestHeaders(clonedRequestOptions); ResponseMessage responseMessage = await this.container.UpsertItemStreamAsync( streamPayload, @@ -984,41 +1019,64 @@ private async Task UpsertItemHelperAsync( clonedRequestOptions, cancellationToken); - if (responseMessage.StatusCode == System.Net.HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + if (shouldRetry && + responseMessage.StatusCode == HttpStatusCode.BadRequest && + string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { - streamPayload = await EncryptionProcessor.DecryptAsync( - streamPayload, - encryptionSettings, - diagnosticsContext, - cancellationToken); - - // get the latest encryption settings. - encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); - - this.SetRequestHeaders(clonedRequestOptions, encryptionSettings); - - streamPayload = await EncryptionProcessor.EncryptAsync( + streamPayload.Position = 0; + streamPayload = await this.DecryptStreamPayloadAndUpdateEncryptionSettingsAsync( streamPayload, encryptionSettings, diagnosticsContext, cancellationToken); - responseMessage = await this.container.UpsertItemStreamAsync( + return await this.UpsertItemHelperAsync( streamPayload, partitionKey, clonedRequestOptions, - cancellationToken); + diagnosticsContext, + cancellationToken, + shouldRetry: false); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( - responseMessage.Content, - encryptionSettings, - diagnosticsContext, - cancellationToken); + responseMessage.Content, + encryptionSettings, + diagnosticsContext, + cancellationToken); return responseMessage; } + + /// + /// This method takes in an encrypted Stream payload. + /// The streamPayload is decrypted with the same policy which was used to encrypt and and then the original plain stream payload is + /// returned which can be used to re-encrypt after the latest encryption settings is retrieved. + /// The method also updates the cached Encryption Settings with the latest value if any. + /// + /// Data encrypted with wrong encryption policy. + /// EncryptionSettings which was used to encrypt the payload. + /// Diagnostics context. + /// Cancellation token. + /// Returns the decrypted stream payload. + private async Task DecryptStreamPayloadAndUpdateEncryptionSettingsAsync( + Stream streamPayload, + EncryptionSettings encryptionSettings, + CosmosDiagnosticsContext diagnosticsContext, + CancellationToken cancellationToken) + { + streamPayload = await EncryptionProcessor.DecryptAsync( + streamPayload, + encryptionSettings, + diagnosticsContext, + cancellationToken); + + // get the latest encryption settings. + await this.GetOrUpdateEncryptionSettingsFromCacheAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings); + + return streamPayload; + } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs index ac4fc9bac0..232c9f148f 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs @@ -40,11 +40,13 @@ public static async Task InitializeEncryptionAsync( { cancellationToken.ThrowIfCancellationRequested(); - if (container is EncryptionContainer encryptionContainer) + if (container is not EncryptionContainer encryptionContainer) { - await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); + throw new ArgumentOutOfRangeException($"{nameof(InitializeEncryptionAsync)} requires the use of an encryption - enabled client. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); } + await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); + return container; } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs index 21d2a1a937..08efaf2877 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs @@ -173,6 +173,7 @@ protected override void Dispose(bool disposing) public async Task GetClientEncryptionKeyPropertiesAsync( string clientEncryptionKeyId, EncryptionContainer encryptionContainer, + string databaseRid, CancellationToken cancellationToken = default, bool shouldForceRefresh = false) { @@ -181,8 +182,13 @@ public async Task GetClientEncryptionKeyPropertie throw new ArgumentNullException(nameof(encryptionContainer)); } + if (string.IsNullOrEmpty(databaseRid)) + { + throw new ArgumentNullException(nameof(databaseRid)); + } + // Client Encryption key Id is unique within a Database. - string cacheKey = encryptionContainer.Database.Id + "/" + clientEncryptionKeyId; + string cacheKey = databaseRid + "|" + clientEncryptionKeyId; return await this.clientEncryptionKeyPropertiesCacheByKeyId.GetAsync( cacheKey, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs index 280f430878..f6ece3b667 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs @@ -36,13 +36,13 @@ public override async Task ReadNextAsync(CancellationToken canc using (diagnosticsContext.CreateScope("FeedIterator.ReadNext")) { EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); - this.encryptionContainer.SetRequestHeaders(this.requestOptions, encryptionSettings); + encryptionSettings.SetRequestHeaders(this.requestOptions); ResponseMessage responseMessage = await this.feedIterator.ReadNextAsync(cancellationToken); // check for Bad Request and Wrong RID intended and update the cached RID and Client Encryption Policy. if (responseMessage.StatusCode == HttpStatusCode.BadRequest - && string.Equals(responseMessage.Headers.Get("x-ms-substatus"), "1024")) + && string.Equals(responseMessage.Headers.Get(EncryptionContainer.SubStatusHeader), EncryptionContainer.IncorrectContainerRidSubStatus)) { await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, @@ -51,7 +51,7 @@ await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + responseMessage.ErrorMessage, responseMessage.StatusCode, - 1024, + int.Parse(EncryptionContainer.IncorrectContainerRidSubStatus), responseMessage.Headers.ActivityId, responseMessage.Headers.RequestCharge); } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs index 2de96f9551..f732ba7e2c 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs @@ -24,7 +24,7 @@ internal static class EncryptionProcessor DateParseHandling = DateParseHandling.None, }); - internal enum TypeMarker : byte + private enum TypeMarker : byte { Null = 1, // not used Boolean = 2, @@ -128,7 +128,7 @@ await DecryptObjectAsync( return document; } - internal static (TypeMarker, byte[]) Serialize(JToken propertyValue) + private static (TypeMarker, byte[]) Serialize(JToken propertyValue) { SqlSerializerFactory sqlSerializerFactory = new SqlSerializerFactory(); @@ -145,7 +145,7 @@ internal static (TypeMarker, byte[]) Serialize(JToken propertyValue) }; } - internal static JToken DeserializeAndAddProperty( + private static JToken DeserializeAndAddProperty( byte[] serializedBytes, TypeMarker typeMarker) { @@ -164,6 +164,40 @@ internal static JToken DeserializeAndAddProperty( }; } + internal static async Task EncryptValueStreamAsync( + EncryptionSettingForProperty settingsForProperty, + Stream valueStream, + CancellationToken cancellationToken) + { + if (valueStream == null) + { + throw new ArgumentNullException(nameof(valueStream)); + } + + if (settingsForProperty == null) + { + throw new ArgumentNullException(nameof(settingsForProperty)); + } + + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingsForProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); + + JToken propertyValueToEncrypt = EncryptionProcessor.BaseSerializer.FromStream(valueStream); + (EncryptionProcessor.TypeMarker typeMarker, byte[] serializedData) = EncryptionProcessor.Serialize(propertyValueToEncrypt); + + byte[] cipherText = aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt(serializedData); + + if (cipherText == null) + { + throw new InvalidOperationException($"{nameof(EncryptValueStreamAsync)} returned null cipherText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); + } + + byte[] cipherTextWithTypeMarker = new byte[cipherText.Length + 1]; + cipherTextWithTypeMarker[0] = (byte)typeMarker; + Buffer.BlockCopy(cipherText, 0, cipherTextWithTypeMarker, 1, cipherText.Length); + + return EncryptionProcessor.BaseSerializer.ToStream(cipherTextWithTypeMarker); + } + private static void EncryptProperty( JObject itemJObj, JToken propertyValue, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs index b68b73c4f1..5c67b77de3 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs @@ -17,13 +17,20 @@ internal sealed class EncryptionSettingForProperty public EncryptionType EncryptionType { get; } + private readonly string databaseRid; + private readonly EncryptionContainer encryptionContainer; - public EncryptionSettingForProperty(string clientEncryptionKeyId, EncryptionType encryptionType, EncryptionContainer encryptionContainer) + public EncryptionSettingForProperty( + string clientEncryptionKeyId, + EncryptionType encryptionType, + EncryptionContainer encryptionContainer, + string databaseRid) { this.ClientEncryptionKeyId = clientEncryptionKeyId ?? throw new ArgumentNullException(nameof(clientEncryptionKeyId)); this.EncryptionType = encryptionType; this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); + this.databaseRid = databaseRid ?? throw new ArgumentNullException(nameof(databaseRid)); } public async Task BuildEncryptionAlgorithmForSettingAsync(CancellationToken cancellationToken) @@ -31,8 +38,8 @@ public async Task BuildEncryptionAlgori ClientEncryptionKeyProperties clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: this.ClientEncryptionKeyId, encryptionContainer: this.encryptionContainer, - cancellationToken: cancellationToken, - shouldForceRefresh: false); + databaseRid: this.databaseRid, + cancellationToken: cancellationToken); ProtectedDataEncryptionKey protectedDataEncryptionKey; @@ -55,6 +62,7 @@ public async Task BuildEncryptionAlgori clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: this.ClientEncryptionKeyId, encryptionContainer: this.encryptionContainer, + databaseRid: this.databaseRid, cancellationToken: cancellationToken, shouldForceRefresh: true); diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index 80161bc28b..3157d25a87 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -14,12 +14,19 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionSettings { + // TODO: Good to have constants available in the Cosmos SDK. Tracked via https://github.com/Azure/azure-cosmos-dotnet-v3/issues/2431 + private const string IntendedCollectionHeader = "x-ms-cosmos-intended-collection-rid"; + + private const string IsClientEncryptedHeader = "x-ms-cosmos-is-client-encrypted"; + private readonly ConcurrentDictionary encryptionSettingsDictByPropertyName = new ConcurrentDictionary(); private readonly EncryptionContainer encryptionContainer; private ClientEncryptionPolicy clientEncryptionPolicy; + private string databaseRidValue; + public string ContainerRidValue { get; private set; } public ICollection PropertiesToEncrypt => this.encryptionSettingsDictByPropertyName.Keys; @@ -38,6 +45,15 @@ public EncryptionSettingForProperty GetEncryptionSettingForProperty(string prope return encryptionSettingsForProperty; } + public void SetRequestHeaders(RequestOptions requestOptions) + { + requestOptions.AddRequestHeaders = (headers) => + { + headers.Add(IsClientEncryptedHeader, bool.TrueString); + headers.Add(IntendedCollectionHeader, this.ContainerRidValue); + }; + } + private EncryptionSettings(EncryptionContainer encryptionContainer) { this.encryptionContainer = encryptionContainer; @@ -64,7 +80,10 @@ private async Task InitializeEncryptionSettingsAsync(Cancell ContainerResponse containerResponse = await this.encryptionContainer.ReadContainerAsync(); - // also set the Container Rid. + // set the Database Rid. + this.databaseRidValue = containerResponse.Resource.SelfLink.Split('/').ElementAt(1); + + // set the Container Rid. this.ContainerRidValue = containerResponse.Resource.SelfLink.Split('/').ElementAt(3); // set the ClientEncryptionPolicy for the Settings. @@ -80,8 +99,8 @@ private async Task InitializeEncryptionSettingsAsync(Cancell await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( clientEncryptionKeyId: clientEncryptionKeyId, encryptionContainer: this.encryptionContainer, - cancellationToken: cancellationToken, - shouldForceRefresh: true); + databaseRid: this.databaseRidValue, + cancellationToken: cancellationToken); } // update the property level setting. @@ -92,7 +111,8 @@ await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyProp EncryptionSettingForProperty encryptionSettingsForProperty = new EncryptionSettingForProperty( propertyToEncrypt.ClientEncryptionKeyId, encryptionType, - this.encryptionContainer); + this.encryptionContainer, + this.databaseRidValue); string propertyName = propertyToEncrypt.Path.Substring(1); diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index b407ff92a6..d5298b3135 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -8,6 +8,7 @@ namespace Microsoft.Azure.Cosmos.Encryption using System.Collections.Generic; using System.IO; using System.Linq; + using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos; @@ -184,48 +185,7 @@ public override TransactionalBatch UpsertItemStream( public override async Task ExecuteAsync( CancellationToken cancellationToken = default) { - CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(options: null); - using (diagnosticsContext.CreateScope("TransactionalBatch.ExecuteAsync")) - { - TransactionalBatchResponse response = null; - - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default); - if (!encryptionSettings.PropertiesToEncrypt.Any()) - { - return await this.transactionalBatch.ExecuteAsync(cancellationToken); - } - else - { - TransactionalBatchRequestOptions requestOptions = new TransactionalBatchRequestOptions(); - this.encryptionContainer.SetRequestHeaders(requestOptions, encryptionSettings); - response = await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); - } - - foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) - { - // FIXME this should return BadRequest and not (-1), requires a backend fix. - if (transactionalBatchOperationResult.StatusCode == (System.Net.HttpStatusCode)(-1) - && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) - { - await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); - - throw new CosmosException( - "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, - response.StatusCode, - 1024, - response.Headers.ActivityId, - response.Headers.RequestCharge); - } - } - - return await this.DecryptTransactionalBatchResponseAsync( - response, - encryptionSettings, - diagnosticsContext, - cancellationToken); - } + return await this.ExecuteAsync(requestOptions: null, cancellationToken: cancellationToken); } public override async Task ExecuteAsync( @@ -254,15 +214,15 @@ public override async Task ExecuteAsync( clonedRequestOptions = new TransactionalBatchRequestOptions(); } - this.encryptionContainer.SetRequestHeaders(clonedRequestOptions, encryptionSettings); + encryptionSettings.SetRequestHeaders(clonedRequestOptions); response = await this.transactionalBatch.ExecuteAsync(clonedRequestOptions, cancellationToken); } foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) { // FIXME this should return BadRequest and not (-1), requires a backend fix. - if (transactionalBatchOperationResult.StatusCode == (System.Net.HttpStatusCode)(-1) - && string.Equals(response.Headers.Get("x-ms-substatus"), "1024")) + if (transactionalBatchOperationResult.StatusCode == (HttpStatusCode)(-1) + && string.Equals(response.Headers.Get(EncryptionContainer.SubStatusHeader), EncryptionContainer.IncorrectContainerRidSubStatus)) { await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, @@ -271,7 +231,7 @@ await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, response.StatusCode, - 1024, + int.Parse(EncryptionContainer.IncorrectContainerRidSubStatus), response.Headers.ActivityId, response.Headers.RequestCharge); } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs index 373933b234..9b8edc4fb8 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs @@ -75,7 +75,6 @@ public static async Task AddParameterAsync( if (queryDefinition is EncryptionQueryDefinition encryptionQueryDefinition) { EncryptionContainer encryptionContainer = (EncryptionContainer)encryptionQueryDefinition.Container; - Stream valueStream = encryptionContainer.CosmosSerializer.ToStream(value); // get the path's encryption setting. EncryptionSettings encryptionSettings = await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); @@ -93,22 +92,9 @@ public static async Task AddParameterAsync( throw new ArgumentException($"Unsupported argument with Path: {path} for query. For executing queries on encrypted path requires the use of deterministic encryption type. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); } - AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingsForProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); - - JToken propertyValueToEncrypt = EncryptionProcessor.BaseSerializer.FromStream(valueStream); - (EncryptionProcessor.TypeMarker typeMarker, byte[] serializedData) = EncryptionProcessor.Serialize(propertyValueToEncrypt); - - byte[] cipherText = aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt(serializedData); - - if (cipherText == null) - { - throw new InvalidOperationException($"{nameof(AddParameterAsync)} returned null cipherText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); - } - - byte[] cipherTextWithTypeMarker = new byte[cipherText.Length + 1]; - cipherTextWithTypeMarker[0] = (byte)typeMarker; - Buffer.BlockCopy(cipherText, 0, cipherTextWithTypeMarker, 1, cipherText.Length); - queryDefinitionwithEncryptedValues.WithParameter(name, cipherTextWithTypeMarker); + Stream valueStream = encryptionContainer.CosmosSerializer.ToStream(value); + Stream encryptedValueStream = await EncryptionProcessor.EncryptValueStreamAsync(settingsForProperty, valueStream, cancellationToken); + queryDefinitionwithEncryptedValues.WithParameterStream(name, encryptedValueStream); return queryDefinitionwithEncryptedValues; } From d55161e57fa7afb5388adf37922b5c2f3f09bbf8 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 30 Apr 2021 14:04:56 +0530 Subject: [PATCH 21/27] Update EncryptionContainer.cs --- .../src/EncryptionContainer.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index 62ab49fdb5..f8871fa719 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -526,8 +526,9 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( decryptedItems.Add(decryptedDocument.ToObject()); } - // we cannot rely currently on a specific exception, this is due to the fact that the run time issue can be variable, we can hit issue with either Json - // serialization say an item was not encrypted but the policy shows it as encrypted, or we could hit a MicrosoftDataEncryptionException from MDE lib etc. + // we cannot rely currently on a specific exception, this is due to the fact that the run time issue can be variable, + // we can hit issue with either Json serialization say an item was not encrypted but the policy shows it as encrypted, + // or we could hit a MicrosoftDataEncryptionException from MDE lib etc. catch (Exception) { // most likely the encryption policy has changed. @@ -679,7 +680,7 @@ public override Task PatchItemStreamAsync( { throw new NotImplementedException(); } - + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( string processorName, ChangeFeedHandler onChangesDelegate) @@ -723,7 +724,7 @@ public override Task> ReadManyItemsAsync( { throw new NotImplementedException(); } - + public async Task GetOrUpdateEncryptionSettingsFromCacheAsync( CancellationToken cancellationToken, EncryptionSettings obsoleteEncryptionSettings = null) From a0e303720037fd2e39098a59499558e2ea04832a Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 30 Apr 2021 14:57:29 +0530 Subject: [PATCH 22/27] Fixes as per review comments. --- .../src/EncryptionContainer.cs | 36 +++++----- .../src/EncryptionProcessor.cs | 68 +++++++++---------- .../src/EncryptionTransactionalBatch.cs | 5 +- .../src/QueryDefinitionExtensions.cs | 3 +- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index f8871fa719..f9f32ba7b9 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -763,7 +763,7 @@ private async Task CreateItemHelperAsync( ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken, - bool shouldRetry = true) + bool isRetry = true) { EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) @@ -784,7 +784,7 @@ private async Task CreateItemHelperAsync( ItemRequestOptions clonedRequestOptions = requestOptions; // only clone it on the first try. - if (shouldRetry) + if (isRetry) { clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); } @@ -801,11 +801,13 @@ private async Task CreateItemHelperAsync( // The idea is to have the container Rid cached and sent out as part of RequestOptions with Container Rid set in "x-ms-cosmos-intended-collection-rid" header. // So when the container being referenced here gets recreated we would end up with a stale encryption settings and container Rid and this would result in BadRequest( and a substatus 1024). // This would allow us to refresh the encryption settings and Container Rid, on the premise that the container recreated could possibly be configured with a new encryption policy. - if (shouldRetry && + if (isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { - // Even though the streamPayload position is expected to be 0, resetting it 0 to be on a safer side. + // Even though the streamPayload position is expected to be 0, + // because for MemoryStream's we just use the underlying buffer to send over the wire rather than using the Stream APIs + // resetting it 0 to be on a safer side. streamPayload.Position = 0; // Now the streamPayload itself is not disposed off(and hence safe to use it in the below call) since the stream that is passed to CreateItemStreamAsync is a MemoryStream and not the original Stream @@ -824,7 +826,7 @@ private async Task CreateItemHelperAsync( clonedRequestOptions, diagnosticsContext, cancellationToken, - shouldRetry: false); + isRetry: false); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -842,7 +844,7 @@ private async Task ReadItemHelperAsync( ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken, - bool shouldRetry = true) + bool isRetry = true) { EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) @@ -857,7 +859,7 @@ private async Task ReadItemHelperAsync( ItemRequestOptions clonedRequestOptions = requestOptions; // only clone it on the first try. - if (shouldRetry) + if (isRetry) { clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); } @@ -870,7 +872,7 @@ private async Task ReadItemHelperAsync( clonedRequestOptions, cancellationToken); - if (shouldRetry && + if (isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { @@ -885,7 +887,7 @@ await this.GetOrUpdateEncryptionSettingsFromCacheAsync( clonedRequestOptions, diagnosticsContext, cancellationToken, - shouldRetry: false); + isRetry: false); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -904,7 +906,7 @@ private async Task ReplaceItemHelperAsync( ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken, - bool shouldRetry = true) + bool isRetry = true) { if (partitionKey == null) { @@ -931,7 +933,7 @@ private async Task ReplaceItemHelperAsync( ItemRequestOptions clonedRequestOptions = requestOptions; // only clone it on the first try. - if (shouldRetry) + if (isRetry) { clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); } @@ -945,7 +947,7 @@ private async Task ReplaceItemHelperAsync( clonedRequestOptions, cancellationToken); - if (shouldRetry && + if (isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { @@ -963,7 +965,7 @@ private async Task ReplaceItemHelperAsync( clonedRequestOptions, diagnosticsContext, cancellationToken, - shouldRetry: false); + isRetry: false); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -981,7 +983,7 @@ private async Task UpsertItemHelperAsync( ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken, - bool shouldRetry = true) + bool isRetry = true) { if (partitionKey == null) { @@ -1007,7 +1009,7 @@ private async Task UpsertItemHelperAsync( ItemRequestOptions clonedRequestOptions = requestOptions; // only clone it on the first try. - if (shouldRetry) + if (isRetry) { clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); } @@ -1020,7 +1022,7 @@ private async Task UpsertItemHelperAsync( clonedRequestOptions, cancellationToken); - if (shouldRetry && + if (isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { @@ -1037,7 +1039,7 @@ private async Task UpsertItemHelperAsync( clonedRequestOptions, diagnosticsContext, cancellationToken, - shouldRetry: false); + isRetry: false); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs index f732ba7e2c..9383fbcad3 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs @@ -128,6 +128,40 @@ await DecryptObjectAsync( return document; } + internal static async Task EncryptValueStreamAsync( + Stream valueStream, + EncryptionSettingForProperty settingsForProperty, + CancellationToken cancellationToken) + { + if (valueStream == null) + { + throw new ArgumentNullException(nameof(valueStream)); + } + + if (settingsForProperty == null) + { + throw new ArgumentNullException(nameof(settingsForProperty)); + } + + AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingsForProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); + + JToken propertyValueToEncrypt = EncryptionProcessor.BaseSerializer.FromStream(valueStream); + (EncryptionProcessor.TypeMarker typeMarker, byte[] serializedData) = EncryptionProcessor.Serialize(propertyValueToEncrypt); + + byte[] cipherText = aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt(serializedData); + + if (cipherText == null) + { + throw new InvalidOperationException($"{nameof(EncryptValueStreamAsync)} returned null cipherText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); + } + + byte[] cipherTextWithTypeMarker = new byte[cipherText.Length + 1]; + cipherTextWithTypeMarker[0] = (byte)typeMarker; + Buffer.BlockCopy(cipherText, 0, cipherTextWithTypeMarker, 1, cipherText.Length); + + return EncryptionProcessor.BaseSerializer.ToStream(cipherTextWithTypeMarker); + } + private static (TypeMarker, byte[]) Serialize(JToken propertyValue) { SqlSerializerFactory sqlSerializerFactory = new SqlSerializerFactory(); @@ -164,40 +198,6 @@ private static JToken DeserializeAndAddProperty( }; } - internal static async Task EncryptValueStreamAsync( - EncryptionSettingForProperty settingsForProperty, - Stream valueStream, - CancellationToken cancellationToken) - { - if (valueStream == null) - { - throw new ArgumentNullException(nameof(valueStream)); - } - - if (settingsForProperty == null) - { - throw new ArgumentNullException(nameof(settingsForProperty)); - } - - AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = await settingsForProperty.BuildEncryptionAlgorithmForSettingAsync(cancellationToken: cancellationToken); - - JToken propertyValueToEncrypt = EncryptionProcessor.BaseSerializer.FromStream(valueStream); - (EncryptionProcessor.TypeMarker typeMarker, byte[] serializedData) = EncryptionProcessor.Serialize(propertyValueToEncrypt); - - byte[] cipherText = aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt(serializedData); - - if (cipherText == null) - { - throw new InvalidOperationException($"{nameof(EncryptValueStreamAsync)} returned null cipherText from {nameof(aeadAes256CbcHmac256EncryptionAlgorithm.Encrypt)}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); - } - - byte[] cipherTextWithTypeMarker = new byte[cipherText.Length + 1]; - cipherTextWithTypeMarker[0] = (byte)typeMarker; - Buffer.BlockCopy(cipherText, 0, cipherTextWithTypeMarker, 1, cipherText.Length); - - return EncryptionProcessor.BaseSerializer.ToStream(cipherTextWithTypeMarker); - } - private static void EncryptProperty( JObject itemJObj, JToken propertyValue, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index d5298b3135..fc4505b6c6 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -220,9 +220,8 @@ public override async Task ExecuteAsync( foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) { - // FIXME this should return BadRequest and not (-1), requires a backend fix. - if (transactionalBatchOperationResult.StatusCode == (HttpStatusCode)(-1) - && string.Equals(response.Headers.Get(EncryptionContainer.SubStatusHeader), EncryptionContainer.IncorrectContainerRidSubStatus)) + // FIXME this should check for BadRequest StatusCode too, requires a service fix to return 400 instead of -1 which is currently returned. + if (string.Equals(response.Headers.Get(EncryptionContainer.SubStatusHeader), EncryptionContainer.IncorrectContainerRidSubStatus)) { await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs index 9b8edc4fb8..e88dc74ea4 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs @@ -9,7 +9,6 @@ namespace Microsoft.Azure.Cosmos.Encryption using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Encryption.Cryptography; - using Newtonsoft.Json.Linq; /// /// This class provides extension methods for . @@ -93,7 +92,7 @@ public static async Task AddParameterAsync( } Stream valueStream = encryptionContainer.CosmosSerializer.ToStream(value); - Stream encryptedValueStream = await EncryptionProcessor.EncryptValueStreamAsync(settingsForProperty, valueStream, cancellationToken); + Stream encryptedValueStream = await EncryptionProcessor.EncryptValueStreamAsync(valueStream, settingsForProperty, cancellationToken); queryDefinitionwithEncryptedValues.WithParameterStream(name, encryptedValueStream); return queryDefinitionwithEncryptedValues; From 01ea0bed64c74c785176b768dc1025becca3534c Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 30 Apr 2021 14:58:45 +0530 Subject: [PATCH 23/27] Update EncryptionContainer.cs --- Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index f9f32ba7b9..ca1ea8d002 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -806,7 +806,7 @@ private async Task CreateItemHelperAsync( string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) { // Even though the streamPayload position is expected to be 0, - // because for MemoryStream's we just use the underlying buffer to send over the wire rather than using the Stream APIs + // because for MemoryStream we just use the underlying buffer to send over the wire rather than using the Stream APIs // resetting it 0 to be on a safer side. streamPayload.Position = 0; From 32261f75ed2e697fc1925c4e220b8f0513376252 Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Fri, 30 Apr 2021 16:21:06 +0530 Subject: [PATCH 24/27] Fixes as per review comments. --- Microsoft.Azure.Cosmos.Encryption/src/Constants.cs | 3 ++- .../src/EncryptionContainer.cs | 12 ++++-------- .../src/EncryptionFeedIterator.cs | 4 ++-- .../src/EncryptionTransactionalBatch.cs | 4 ++-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Constants.cs b/Microsoft.Azure.Cosmos.Encryption/src/Constants.cs index ebecfac521..2ce5886279 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Constants.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/Constants.cs @@ -6,7 +6,8 @@ namespace Microsoft.Azure.Cosmos.Encryption { internal static class Constants { - public const int CachedEncryptionSettingsDefaultTTLInMinutes = 60; public const string DocumentsResourcePropertyName = "Documents"; + public const string SubStatusHeader = "x-ms-substatus"; + public const string IncorrectContainerRidSubStatus = "1024"; } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index ca1ea8d002..d37a3df6a9 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -16,10 +16,6 @@ namespace Microsoft.Azure.Cosmos.Encryption internal sealed class EncryptionContainer : Container { - public const string SubStatusHeader = "x-ms-substatus"; - - public const string IncorrectContainerRidSubStatus = "1024"; - private readonly Container container; public CosmosSerializer CosmosSerializer { get; } @@ -803,7 +799,7 @@ private async Task CreateItemHelperAsync( // This would allow us to refresh the encryption settings and Container Rid, on the premise that the container recreated could possibly be configured with a new encryption policy. if (isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && - string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) + string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { // Even though the streamPayload position is expected to be 0, // because for MemoryStream we just use the underlying buffer to send over the wire rather than using the Stream APIs @@ -874,7 +870,7 @@ private async Task ReadItemHelperAsync( if (isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && - string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) + string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { // get the latest encryption settings. await this.GetOrUpdateEncryptionSettingsFromCacheAsync( @@ -949,7 +945,7 @@ private async Task ReplaceItemHelperAsync( if (isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && - string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) + string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { streamPayload.Position = 0; streamPayload = await this.DecryptStreamPayloadAndUpdateEncryptionSettingsAsync( @@ -1024,7 +1020,7 @@ private async Task UpsertItemHelperAsync( if (isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && - string.Equals(responseMessage.Headers.Get(SubStatusHeader), IncorrectContainerRidSubStatus)) + string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { streamPayload.Position = 0; streamPayload = await this.DecryptStreamPayloadAndUpdateEncryptionSettingsAsync( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs index f6ece3b667..092f81c236 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs @@ -42,7 +42,7 @@ public override async Task ReadNextAsync(CancellationToken canc // check for Bad Request and Wrong RID intended and update the cached RID and Client Encryption Policy. if (responseMessage.StatusCode == HttpStatusCode.BadRequest - && string.Equals(responseMessage.Headers.Get(EncryptionContainer.SubStatusHeader), EncryptionContainer.IncorrectContainerRidSubStatus)) + && string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, @@ -51,7 +51,7 @@ await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + responseMessage.ErrorMessage, responseMessage.StatusCode, - int.Parse(EncryptionContainer.IncorrectContainerRidSubStatus), + int.Parse(Constants.IncorrectContainerRidSubStatus), responseMessage.Headers.ActivityId, responseMessage.Headers.RequestCharge); } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index fc4505b6c6..ca83bb8937 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -221,7 +221,7 @@ public override async Task ExecuteAsync( foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) { // FIXME this should check for BadRequest StatusCode too, requires a service fix to return 400 instead of -1 which is currently returned. - if (string.Equals(response.Headers.Get(EncryptionContainer.SubStatusHeader), EncryptionContainer.IncorrectContainerRidSubStatus)) + if (string.Equals(response.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( cancellationToken: cancellationToken, @@ -230,7 +230,7 @@ await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, response.StatusCode, - int.Parse(EncryptionContainer.IncorrectContainerRidSubStatus), + int.Parse(Constants.IncorrectContainerRidSubStatus), response.Headers.ActivityId, response.Headers.RequestCharge); } From dcb30f5736720a9a220f6485176d43716df6de9f Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Sat, 1 May 2021 11:02:55 +0530 Subject: [PATCH 25/27] Fixes as per review comments. --- .../src/EncryptionContainer.cs | 32 +++++++++---------- .../src/EncryptionCosmosClient.cs | 7 +++- .../src/EncryptionSettingForProperty.cs | 4 +-- .../src/EncryptionSettings.cs | 5 +++ .../src/EncryptionTransactionalBatch.cs | 27 +++++++--------- .../Microsoft.Azure.Cosmos.Encryption.csproj | 1 - 6 files changed, 41 insertions(+), 35 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index d37a3df6a9..ee32a8acc5 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -759,7 +759,7 @@ private async Task CreateItemHelperAsync( ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken, - bool isRetry = true) + bool isRetry = false) { EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) @@ -780,7 +780,7 @@ private async Task CreateItemHelperAsync( ItemRequestOptions clonedRequestOptions = requestOptions; // only clone it on the first try. - if (isRetry) + if (!isRetry) { clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); } @@ -797,7 +797,7 @@ private async Task CreateItemHelperAsync( // The idea is to have the container Rid cached and sent out as part of RequestOptions with Container Rid set in "x-ms-cosmos-intended-collection-rid" header. // So when the container being referenced here gets recreated we would end up with a stale encryption settings and container Rid and this would result in BadRequest( and a substatus 1024). // This would allow us to refresh the encryption settings and Container Rid, on the premise that the container recreated could possibly be configured with a new encryption policy. - if (isRetry && + if (!isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { @@ -822,7 +822,7 @@ private async Task CreateItemHelperAsync( clonedRequestOptions, diagnosticsContext, cancellationToken, - isRetry: false); + isRetry: true); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -840,7 +840,7 @@ private async Task ReadItemHelperAsync( ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken, - bool isRetry = true) + bool isRetry = false) { EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) @@ -855,7 +855,7 @@ private async Task ReadItemHelperAsync( ItemRequestOptions clonedRequestOptions = requestOptions; // only clone it on the first try. - if (isRetry) + if (!isRetry) { clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); } @@ -868,7 +868,7 @@ private async Task ReadItemHelperAsync( clonedRequestOptions, cancellationToken); - if (isRetry && + if (!isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { @@ -883,7 +883,7 @@ await this.GetOrUpdateEncryptionSettingsFromCacheAsync( clonedRequestOptions, diagnosticsContext, cancellationToken, - isRetry: false); + isRetry: true); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -902,7 +902,7 @@ private async Task ReplaceItemHelperAsync( ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken, - bool isRetry = true) + bool isRetry = false) { if (partitionKey == null) { @@ -929,7 +929,7 @@ private async Task ReplaceItemHelperAsync( ItemRequestOptions clonedRequestOptions = requestOptions; // only clone it on the first try. - if (isRetry) + if (!isRetry) { clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); } @@ -943,7 +943,7 @@ private async Task ReplaceItemHelperAsync( clonedRequestOptions, cancellationToken); - if (isRetry && + if (!isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { @@ -961,7 +961,7 @@ private async Task ReplaceItemHelperAsync( clonedRequestOptions, diagnosticsContext, cancellationToken, - isRetry: false); + isRetry: true); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( @@ -979,7 +979,7 @@ private async Task UpsertItemHelperAsync( ItemRequestOptions requestOptions, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken, - bool isRetry = true) + bool isRetry = false) { if (partitionKey == null) { @@ -1005,7 +1005,7 @@ private async Task UpsertItemHelperAsync( ItemRequestOptions clonedRequestOptions = requestOptions; // only clone it on the first try. - if (isRetry) + if (!isRetry) { clonedRequestOptions = GetClonedItemRequestOptions(requestOptions); } @@ -1018,7 +1018,7 @@ private async Task UpsertItemHelperAsync( clonedRequestOptions, cancellationToken); - if (isRetry && + if (!isRetry && responseMessage.StatusCode == HttpStatusCode.BadRequest && string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { @@ -1035,7 +1035,7 @@ private async Task UpsertItemHelperAsync( clonedRequestOptions, diagnosticsContext, cancellationToken, - isRetry: false); + isRetry: true); } responseMessage.Content = await EncryptionProcessor.DecryptAsync( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs index 08efaf2877..509574aae2 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs @@ -187,6 +187,11 @@ public async Task GetClientEncryptionKeyPropertie throw new ArgumentNullException(nameof(databaseRid)); } + if (string.IsNullOrEmpty(clientEncryptionKeyId)) + { + throw new ArgumentNullException(nameof(clientEncryptionKeyId)); + } + // Client Encryption key Id is unique within a Database. string cacheKey = databaseRid + "|" + clientEncryptionKeyId; @@ -214,7 +219,7 @@ private async Task FetchClientEncryptionKeyProper { if (ex.StatusCode == HttpStatusCode.NotFound) { - throw new InvalidOperationException($"Encryption Based Container without Data Encryption Keys. Please make sure you have created the Client Encryption Keys:{ex.Message}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); + throw new InvalidOperationException($"Encryption Based Container without Client Encryption Keys. Please make sure you have created the Client Encryption Keys:{ex.Message}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); } else { diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs index 5c67b77de3..cabccc4c56 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettingForProperty.cs @@ -27,10 +27,10 @@ public EncryptionSettingForProperty( EncryptionContainer encryptionContainer, string databaseRid) { - this.ClientEncryptionKeyId = clientEncryptionKeyId ?? throw new ArgumentNullException(nameof(clientEncryptionKeyId)); + this.ClientEncryptionKeyId = string.IsNullOrEmpty(clientEncryptionKeyId) ? throw new ArgumentNullException(nameof(clientEncryptionKeyId)) : clientEncryptionKeyId; this.EncryptionType = encryptionType; this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer)); - this.databaseRid = databaseRid ?? throw new ArgumentNullException(nameof(databaseRid)); + this.databaseRid = string.IsNullOrEmpty(databaseRid) ? throw new ArgumentNullException(nameof(databaseRid)) : databaseRid; } public async Task BuildEncryptionAlgorithmForSettingAsync(CancellationToken cancellationToken) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index 3157d25a87..94db9b45a4 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -7,7 +7,9 @@ namespace Microsoft.Azure.Cosmos.Encryption using System; using System.Collections.Concurrent; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; + using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Encryption.Cryptography; @@ -80,6 +82,9 @@ private async Task InitializeEncryptionSettingsAsync(Cancell ContainerResponse containerResponse = await this.encryptionContainer.ReadContainerAsync(); + Debug.Assert(containerResponse.StatusCode == HttpStatusCode.OK, "ReadContainerAsync request has failed as part of InitializeEncryptionSettingsAsync operation. "); + Debug.Assert(containerResponse.Resource != null, "Null resource received in ContainerResponse as part of InitializeEncryptionSettingsAsync operation. "); + // set the Database Rid. this.databaseRidValue = containerResponse.Resource.SelfLink.Split('/').ElementAt(1); diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index ca83bb8937..4bb04d97a5 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -218,22 +218,19 @@ public override async Task ExecuteAsync( response = await this.transactionalBatch.ExecuteAsync(clonedRequestOptions, cancellationToken); } - foreach (TransactionalBatchOperationResult transactionalBatchOperationResult in response) + // FIXME this should check for BadRequest StatusCode too, requires a service fix to return 400 instead of -1 which is currently returned. + if (string.Equals(response.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { - // FIXME this should check for BadRequest StatusCode too, requires a service fix to return 400 instead of -1 which is currently returned. - if (string.Equals(response.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) - { - await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); - - throw new CosmosException( - "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, - response.StatusCode, - int.Parse(Constants.IncorrectContainerRidSubStatus), - response.Headers.ActivityId, - response.Headers.RequestCharge); - } + await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( + cancellationToken: cancellationToken, + obsoleteEncryptionSettings: encryptionSettings); + + throw new CosmosException( + "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, + HttpStatusCode.BadRequest, + int.Parse(Constants.IncorrectContainerRidSubStatus), + response.Headers.ActivityId, + response.Headers.RequestCharge); } return await this.DecryptTransactionalBatchResponseAsync( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj b/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj index 28012c7207..5cd428b637 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj +++ b/Microsoft.Azure.Cosmos.Encryption/src/Microsoft.Azure.Cosmos.Encryption.csproj @@ -35,7 +35,6 @@ - From 55c7e7adac7b7eca6f701eac64fb625d8f9c597c Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Thu, 6 May 2021 13:20:21 +0530 Subject: [PATCH 26/27] Changes as per review comments. --- .../src/EncryptionContainer.cs | 32 ++--- .../src/EncryptionContainerExtensions.cs | 2 +- .../src/EncryptionFeedIterator.cs | 6 +- .../src/EncryptionSettings.cs | 115 +++++++++--------- .../src/EncryptionTransactionalBatch.cs | 12 +- .../src/QueryDefinitionExtensions.cs | 2 +- 6 files changed, 82 insertions(+), 87 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index ee32a8acc5..35a132cfbb 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -18,6 +18,8 @@ internal sealed class EncryptionContainer : Container { private readonly Container container; + private readonly AsyncCache encryptionSettingsByContainerName; + public CosmosSerializer CosmosSerializer { get; } public CosmosResponseFactory ResponseFactory { get; } @@ -48,8 +50,6 @@ public EncryptionContainer( public override Database Database => this.container.Database; - private readonly AsyncCache encryptionSettingsByContainerName; - public override async Task> CreateItemAsync( T item, PartitionKey? partitionKey = null, @@ -510,7 +510,7 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( foreach (JObject document in documents) { - EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); try { JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( @@ -529,8 +529,8 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( { // most likely the encryption policy has changed. encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); + obsoleteEncryptionSettings: encryptionSettings, + cancellationToken: cancellationToken); JObject decryptedDocument = await EncryptionProcessor.DecryptAsync( document, @@ -722,13 +722,13 @@ public override Task> ReadManyItemsAsync( } public async Task GetOrUpdateEncryptionSettingsFromCacheAsync( - CancellationToken cancellationToken, - EncryptionSettings obsoleteEncryptionSettings = null) + EncryptionSettings obsoleteEncryptionSettings, + CancellationToken cancellationToken) { return await this.encryptionSettingsByContainerName.GetAsync( this.Id, obsoleteValue: obsoleteEncryptionSettings, - singleValueInitFunc: async () => await EncryptionSettings.CreateAsync(this), + singleValueInitFunc: () => EncryptionSettings.CreateAsync(this, cancellationToken), cancellationToken: cancellationToken); } @@ -761,7 +761,7 @@ private async Task CreateItemHelperAsync( CancellationToken cancellationToken, bool isRetry = false) { - EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.container.CreateItemStreamAsync( @@ -842,7 +842,7 @@ private async Task ReadItemHelperAsync( CancellationToken cancellationToken, bool isRetry = false) { - EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.container.ReadItemStreamAsync( @@ -874,8 +874,8 @@ private async Task ReadItemHelperAsync( { // get the latest encryption settings. await this.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); + obsoleteEncryptionSettings: encryptionSettings, + cancellationToken: cancellationToken); return await this.ReadItemHelperAsync( id, @@ -909,7 +909,7 @@ private async Task ReplaceItemHelperAsync( throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); } - EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.container.ReplaceItemStreamAsync( @@ -986,7 +986,7 @@ private async Task UpsertItemHelperAsync( throw new NotSupportedException($"{nameof(partitionKey)} cannot be null for operations using {nameof(EncryptionContainer)}."); } - EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.container.UpsertItemStreamAsync( @@ -1072,8 +1072,8 @@ private async Task DecryptStreamPayloadAndUpdateEncryptionSettingsAsync( // get the latest encryption settings. await this.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); + obsoleteEncryptionSettings: encryptionSettings, + cancellationToken: cancellationToken); return streamPayload; } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs index 232c9f148f..b20758d1bc 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainerExtensions.cs @@ -45,7 +45,7 @@ public static async Task InitializeEncryptionAsync( throw new ArgumentOutOfRangeException($"{nameof(InitializeEncryptionAsync)} requires the use of an encryption - enabled client. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); } - await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: cancellationToken); + await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); return container; } diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs index 092f81c236..b09102d94c 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionFeedIterator.cs @@ -35,7 +35,7 @@ public override async Task ReadNextAsync(CancellationToken canc CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(options: null); using (diagnosticsContext.CreateScope("FeedIterator.ReadNext")) { - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); encryptionSettings.SetRequestHeaders(this.requestOptions); ResponseMessage responseMessage = await this.feedIterator.ReadNextAsync(cancellationToken); @@ -45,8 +45,8 @@ public override async Task ReadNextAsync(CancellationToken canc && string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); + obsoleteEncryptionSettings: encryptionSettings, + cancellationToken: cancellationToken); throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + responseMessage.ErrorMessage, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index 94db9b45a4..a2c1a0f5d7 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -6,7 +6,7 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; using System.Collections.Concurrent; - using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Net; @@ -23,21 +23,13 @@ internal sealed class EncryptionSettings private readonly ConcurrentDictionary encryptionSettingsDictByPropertyName = new ConcurrentDictionary(); - private readonly EncryptionContainer encryptionContainer; + public string ContainerRidValue { get; } - private ClientEncryptionPolicy clientEncryptionPolicy; + public ReadOnlyCollection PropertiesToEncrypt { get; private set; } - private string databaseRidValue; - - public string ContainerRidValue { get; private set; } - - public ICollection PropertiesToEncrypt => this.encryptionSettingsDictByPropertyName.Keys; - - public static Task CreateAsync(EncryptionContainer encryptionContainer) + public static Task CreateAsync(EncryptionContainer encryptionContainer, CancellationToken cancellationToken) { - EncryptionSettings encryptionSettings = new EncryptionSettings(encryptionContainer); - - return encryptionSettings.InitializeEncryptionSettingsAsync(); + return InitializeEncryptionSettingsAsync(encryptionContainer, cancellationToken); } public EncryptionSettingForProperty GetEncryptionSettingForProperty(string propertyName) @@ -56,77 +48,80 @@ public void SetRequestHeaders(RequestOptions requestOptions) }; } - private EncryptionSettings(EncryptionContainer encryptionContainer) + private EncryptionSettings(string containerRidValue) { - this.encryptionContainer = encryptionContainer; + this.ContainerRidValue = containerRidValue; } - private EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPath clientEncryptionIncludedPath) + private static EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPath clientEncryptionIncludedPath) { - switch (clientEncryptionIncludedPath.EncryptionType) + return clientEncryptionIncludedPath.EncryptionType switch { - case CosmosEncryptionType.Deterministic: - return EncryptionType.Deterministic; - case CosmosEncryptionType.Randomized: - return EncryptionType.Randomized; - case CosmosEncryptionType.Plaintext: - return EncryptionType.Plaintext; - default: - throw new ArgumentException($"Invalid encryption type {clientEncryptionIncludedPath.EncryptionType}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "); - } + CosmosEncryptionType.Deterministic => EncryptionType.Deterministic, + CosmosEncryptionType.Randomized => EncryptionType.Randomized, + CosmosEncryptionType.Plaintext => EncryptionType.Plaintext, + _ => throw new ArgumentException($"Invalid encryption type {clientEncryptionIncludedPath.EncryptionType}. Please refer to https://aka.ms/CosmosClientEncryption for more details. "), + }; } - private async Task InitializeEncryptionSettingsAsync(CancellationToken cancellationToken = default) + private static async Task InitializeEncryptionSettingsAsync(EncryptionContainer encryptionContainer, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - ContainerResponse containerResponse = await this.encryptionContainer.ReadContainerAsync(); + ContainerResponse containerResponse = await encryptionContainer.ReadContainerAsync(); Debug.Assert(containerResponse.StatusCode == HttpStatusCode.OK, "ReadContainerAsync request has failed as part of InitializeEncryptionSettingsAsync operation. "); Debug.Assert(containerResponse.Resource != null, "Null resource received in ContainerResponse as part of InitializeEncryptionSettingsAsync operation. "); // set the Database Rid. - this.databaseRidValue = containerResponse.Resource.SelfLink.Split('/').ElementAt(1); + string databaseRidValue = containerResponse.Resource.SelfLink.Split('/').ElementAt(1); // set the Container Rid. - this.ContainerRidValue = containerResponse.Resource.SelfLink.Split('/').ElementAt(3); + string containerRidValue = containerResponse.Resource.SelfLink.Split('/').ElementAt(3); // set the ClientEncryptionPolicy for the Settings. - this.clientEncryptionPolicy = containerResponse.Resource.ClientEncryptionPolicy; - if (this.clientEncryptionPolicy == null) - { - return this; - } + ClientEncryptionPolicy clientEncryptionPolicy = containerResponse.Resource.ClientEncryptionPolicy; - // for each of the unique keys in the policy Add it in /Update the cache. - foreach (string clientEncryptionKeyId in this.clientEncryptionPolicy.IncludedPaths.Select(x => x.ClientEncryptionKeyId).Distinct()) - { - await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( - clientEncryptionKeyId: clientEncryptionKeyId, - encryptionContainer: this.encryptionContainer, - databaseRid: this.databaseRidValue, - cancellationToken: cancellationToken); - } + EncryptionSettings encryptionSettings = new EncryptionSettings(containerRidValue); - // update the property level setting. - foreach (ClientEncryptionIncludedPath propertyToEncrypt in this.clientEncryptionPolicy.IncludedPaths) + if (clientEncryptionPolicy != null) { - EncryptionType encryptionType = this.GetEncryptionTypeForProperty(propertyToEncrypt); - - EncryptionSettingForProperty encryptionSettingsForProperty = new EncryptionSettingForProperty( - propertyToEncrypt.ClientEncryptionKeyId, - encryptionType, - this.encryptionContainer, - this.databaseRidValue); - - string propertyName = propertyToEncrypt.Path.Substring(1); - - this.SetEncryptionSettingForProperty( - propertyName, - encryptionSettingsForProperty); + // for each of the unique keys in the policy Add it in /Update the cache. + foreach (string clientEncryptionKeyId in clientEncryptionPolicy.IncludedPaths.Select(x => x.ClientEncryptionKeyId).Distinct()) + { + await encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync( + clientEncryptionKeyId: clientEncryptionKeyId, + encryptionContainer: encryptionContainer, + databaseRid: databaseRidValue, + cancellationToken: cancellationToken); + } + + // update the property level setting. + foreach (ClientEncryptionIncludedPath propertyToEncrypt in clientEncryptionPolicy.IncludedPaths) + { + EncryptionType encryptionType = GetEncryptionTypeForProperty(propertyToEncrypt); + + EncryptionSettingForProperty encryptionSettingsForProperty = new EncryptionSettingForProperty( + propertyToEncrypt.ClientEncryptionKeyId, + encryptionType, + encryptionContainer, + databaseRidValue); + + string propertyName = propertyToEncrypt.Path.Substring(1); + + encryptionSettings.SetEncryptionSettingForProperty( + propertyName, + encryptionSettingsForProperty); + } } - return this; + encryptionSettings.SetPropertiesToEncrypt(); + return encryptionSettings; + } + + private void SetPropertiesToEncrypt() + { + this.PropertiesToEncrypt = (ReadOnlyCollection)this.encryptionSettingsDictByPropertyName.Keys; } private void SetEncryptionSettingForProperty( diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs index 4bb04d97a5..f8186fa5c2 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionTransactionalBatch.cs @@ -47,7 +47,7 @@ public override TransactionalBatch CreateItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); @@ -114,7 +114,7 @@ public override TransactionalBatch ReplaceItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); @@ -157,7 +157,7 @@ public override TransactionalBatch UpsertItemStream( CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions); using (diagnosticsContext.CreateScope("EncryptItemStream")) { - EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default) + EncryptionSettings encryptionSettings = this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: default) .ConfigureAwait(false) .GetAwaiter() .GetResult(); @@ -197,7 +197,7 @@ public override async Task ExecuteAsync( { TransactionalBatchResponse response = null; - EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken: default); + EncryptionSettings encryptionSettings = await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); if (!encryptionSettings.PropertiesToEncrypt.Any()) { return await this.transactionalBatch.ExecuteAsync(requestOptions, cancellationToken); @@ -222,8 +222,8 @@ public override async Task ExecuteAsync( if (string.Equals(response.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus)) { await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync( - cancellationToken: cancellationToken, - obsoleteEncryptionSettings: encryptionSettings); + obsoleteEncryptionSettings: encryptionSettings, + cancellationToken: cancellationToken); throw new CosmosException( "Operation has failed due to a possible mismatch in Client Encryption Policy configured on the container. Please refer to https://aka.ms/CosmosClientEncryption for more details. " + response.ErrorMessage, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs index e88dc74ea4..48a2588df7 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/QueryDefinitionExtensions.cs @@ -76,7 +76,7 @@ public static async Task AddParameterAsync( EncryptionContainer encryptionContainer = (EncryptionContainer)encryptionQueryDefinition.Container; // get the path's encryption setting. - EncryptionSettings encryptionSettings = await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(cancellationToken); + EncryptionSettings encryptionSettings = await encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(obsoleteEncryptionSettings: null, cancellationToken: cancellationToken); EncryptionSettingForProperty settingsForProperty = encryptionSettings.GetEncryptionSettingForProperty(path.Substring(1)); if (settingsForProperty == null) From 2fcb5e74f666109271d0592052e86386afc6eb5d Mon Sep 17 00:00:00 2001 From: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com> Date: Thu, 6 May 2021 18:24:39 +0530 Subject: [PATCH 27/27] Fixes as per review comments. --- .../src/EncryptionSettings.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs index a2c1a0f5d7..778a10a539 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs @@ -5,8 +5,7 @@ namespace Microsoft.Azure.Cosmos.Encryption { using System; - using System.Collections.Concurrent; - using System.Collections.ObjectModel; + using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; @@ -21,11 +20,11 @@ internal sealed class EncryptionSettings private const string IsClientEncryptedHeader = "x-ms-cosmos-is-client-encrypted"; - private readonly ConcurrentDictionary encryptionSettingsDictByPropertyName = new ConcurrentDictionary(); + private readonly Dictionary encryptionSettingsDictByPropertyName; public string ContainerRidValue { get; } - public ReadOnlyCollection PropertiesToEncrypt { get; private set; } + public IEnumerable PropertiesToEncrypt { get; } public static Task CreateAsync(EncryptionContainer encryptionContainer, CancellationToken cancellationToken) { @@ -51,6 +50,8 @@ public void SetRequestHeaders(RequestOptions requestOptions) private EncryptionSettings(string containerRidValue) { this.ContainerRidValue = containerRidValue; + this.encryptionSettingsDictByPropertyName = new Dictionary(); + this.PropertiesToEncrypt = this.encryptionSettingsDictByPropertyName.Keys; } private static EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPath clientEncryptionIncludedPath) @@ -115,15 +116,9 @@ await encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertie } } - encryptionSettings.SetPropertiesToEncrypt(); return encryptionSettings; } - private void SetPropertiesToEncrypt() - { - this.PropertiesToEncrypt = (ReadOnlyCollection)this.encryptionSettingsDictByPropertyName.Keys; - } - private void SetEncryptionSettingForProperty( string propertyName, EncryptionSettingForProperty encryptionSettingsForProperty)