Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ClientEncryption: Adds the use of Azure.Core interfaces in public surface #3050

Merged
merged 9 commits into from
Mar 3, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Microsoft.Azure.Cosmos.Encryption
/// <summary>
/// Represents the encryption algorithms supported for data encryption.
/// </summary>
public static class DataEncryptionKeyAlgorithm
public static class EncryptionAlgorithm
{
/// <summary>
/// Represents the authenticated encryption algorithm with associated data as described in
Expand Down
37 changes: 34 additions & 3 deletions Microsoft.Azure.Cosmos.Encryption/src/EncryptionCosmosClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Microsoft.Azure.Cosmos.Encryption
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using global::Azure.Core.Cryptography;
using Microsoft.Data.Encryption.Cryptography;

/// <summary>
/// CosmosClient with Encryption support.
Expand All @@ -20,14 +22,43 @@ internal sealed class EncryptionCosmosClient : CosmosClient

private readonly AsyncCache<string, ClientEncryptionKeyProperties> clientEncryptionKeyPropertiesCacheByKeyId;

public EncryptionCosmosClient(CosmosClient cosmosClient, EncryptionKeyWrapProvider encryptionKeyWrapProvider)
public EncryptionCosmosClient(
CosmosClient cosmosClient,
IKeyEncryptionKeyResolver keyEncryptionKeyResolver,
string keyEncryptionKeyResolverName,
abhijitpai marked this conversation as resolved.
Show resolved Hide resolved
TimeSpan? keyCacheTimeToLive)
{
this.cosmosClient = cosmosClient ?? throw new ArgumentNullException(nameof(cosmosClient));
this.EncryptionKeyWrapProvider = encryptionKeyWrapProvider ?? throw new ArgumentNullException(nameof(encryptionKeyWrapProvider));
this.KeyEncryptionKeyResolver = keyEncryptionKeyResolver ?? throw new ArgumentNullException(nameof(keyEncryptionKeyResolver));
this.KeyEncryptionKeyResolverName = keyEncryptionKeyResolverName ?? throw new ArgumentNullException(nameof(keyEncryptionKeyResolverName));
this.clientEncryptionKeyPropertiesCacheByKeyId = new AsyncCache<string, ClientEncryptionKeyProperties>();
this.EncryptionKeyStoreProviderImpl = new EncryptionKeyStoreProviderImpl(keyEncryptionKeyResolver, keyEncryptionKeyResolverName);

keyCacheTimeToLive ??= TimeSpan.FromHours(1);

if (EncryptionCosmosClient.EncryptionKeyCacheSemaphore.Wait(-1))
{
try
{
// We pick the minimum between the existing and passed in value given this is a static cache.
// This also means that the maximum cache duration is the originally initialized value for ProtectedDataEncryptionKey.TimeToLive which is 2 hours.
if (keyCacheTimeToLive < ProtectedDataEncryptionKey.TimeToLive)
{
ProtectedDataEncryptionKey.TimeToLive = keyCacheTimeToLive.Value;
}
}
finally
{
EncryptionCosmosClient.EncryptionKeyCacheSemaphore.Release(1);
}
}
}

public EncryptionKeyWrapProvider EncryptionKeyWrapProvider { get; }
public EncryptionKeyStoreProviderImpl EncryptionKeyStoreProviderImpl { get; }

public IKeyEncryptionKeyResolver KeyEncryptionKeyResolver { get; }
abhijitpai marked this conversation as resolved.
Show resolved Hide resolved

public string KeyEncryptionKeyResolverName { get; }

public override CosmosClientOptions ClientOptions => this.cosmosClient.ClientOptions;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Microsoft.Azure.Cosmos.Encryption
{
using System;
using global::Azure.Core.Cryptography;
using Microsoft.Data.Encryption.Cryptography;

/// <summary>
Expand All @@ -16,23 +17,32 @@ public static class EncryptionCosmosClientExtensions
/// Get Cosmos Client with Encryption support for performing operations using client-side encryption.
/// </summary>
/// <param name="cosmosClient">Regular Cosmos Client.</param>
/// <param name="encryptionKeyWrapProvider">EncryptionKeyWrapProvider, provider that allows interaction with the master keys.</param>
/// <param name="keyEncryptionKeyResolver">IKeyEncryptionKeyResolver that allows interaction with the key encryption keys.</param>
/// <param name="keyEncryptionKeyResolverId">Identifier of the resolver, eg. KeyEncryptionKeyResolverId.AzureKeyVault. </param>
/// <param name="keyCacheTimeToLive">Time for which raw keys are cached in-memory. Defaults to 1 hour.</param>
/// <returns> CosmosClient to perform operations supporting client-side encryption / decryption.</returns>
public static CosmosClient WithEncryption(
this CosmosClient cosmosClient,
EncryptionKeyWrapProvider encryptionKeyWrapProvider)
IKeyEncryptionKeyResolver keyEncryptionKeyResolver,
string keyEncryptionKeyResolverId,
TimeSpan? keyCacheTimeToLive = null)
{
if (encryptionKeyWrapProvider == null)
if (keyEncryptionKeyResolver == null)
{
throw new ArgumentNullException(nameof(encryptionKeyWrapProvider));
throw new ArgumentNullException(nameof(keyEncryptionKeyResolver));
}

if (keyEncryptionKeyResolverId == null)
{
throw new ArgumentNullException(nameof(keyEncryptionKeyResolverId));
}

if (cosmosClient == null)
{
throw new ArgumentNullException(nameof(cosmosClient));
}

return new EncryptionCosmosClient(cosmosClient, encryptionKeyWrapProvider);
return new EncryptionCosmosClient(cosmosClient, keyEncryptionKeyResolver, keyEncryptionKeyResolverId, keyCacheTimeToLive);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static async Task<ClientEncryptionKeyResponse> CreateClientEncryptionKeyA
throw new ArgumentNullException(nameof(clientEncryptionKeyId));
}

if (!string.Equals(dataEncryptionKeyAlgorithm, DataEncryptionKeyAlgorithm.AeadAes256CbcHmacSha256))
if (!string.Equals(dataEncryptionKeyAlgorithm, EncryptionAlgorithm.AeadAes256CbcHmacSha256))
{
throw new ArgumentException($"Invalid Encryption Algorithm '{dataEncryptionKeyAlgorithm}' passed. Please refer to https://aka.ms/CosmosClientEncryption for more details. ");
}
Expand All @@ -63,17 +63,15 @@ public static async Task<ClientEncryptionKeyResponse> CreateClientEncryptionKeyA
? encryptionDatabase.EncryptionCosmosClient
: throw new ArgumentException("Creating a ClientEncryptionKey resource requires the use of an encryption - enabled client. Please refer to https://aka.ms/CosmosClientEncryption for more details. ");

EncryptionKeyWrapProvider encryptionKeyWrapProvider = encryptionCosmosClient.EncryptionKeyWrapProvider;

if (!string.Equals(encryptionKeyWrapMetadata.Type, encryptionKeyWrapProvider.ProviderName))
if (!string.Equals(encryptionKeyWrapMetadata.Type, encryptionCosmosClient.KeyEncryptionKeyResolverName))
{
throw new ArgumentException("The EncryptionKeyWrapMetadata Type value does not match with the ProviderName of EncryptionKeyWrapProvider configured on the Client. Please refer to https://aka.ms/CosmosClientEncryption for more details. ");
}

KeyEncryptionKey keyEncryptionKey = KeyEncryptionKey.GetOrCreate(
encryptionKeyWrapMetadata.Name,
encryptionKeyWrapMetadata.Value,
encryptionKeyWrapProvider.EncryptionKeyStoreProviderImpl);
encryptionCosmosClient.EncryptionKeyStoreProviderImpl);

ProtectedDataEncryptionKey protectedDataEncryptionKey = new ProtectedDataEncryptionKey(
clientEncryptionKeyId,
Expand Down Expand Up @@ -143,9 +141,7 @@ public static async Task<ClientEncryptionKeyResponse> RewrapClientEncryptionKeyA
? encryptionDatabase.EncryptionCosmosClient
: throw new ArgumentException("Rewraping a ClientEncryptionKey requires the use of an encryption - enabled client. Please refer to https://aka.ms/CosmosClientEncryption for more details. ");

EncryptionKeyWrapProvider encryptionKeyWrapProvider = encryptionCosmosClient.EncryptionKeyWrapProvider;

if (!string.Equals(newEncryptionKeyWrapMetadata.Type, encryptionKeyWrapProvider.ProviderName))
if (!string.Equals(newEncryptionKeyWrapMetadata.Type, encryptionCosmosClient.KeyEncryptionKeyResolverName))
{
throw new ArgumentException("The EncryptionKeyWrapMetadata Type value does not match with the ProviderName of EncryptionKeyWrapProvider configured on the Client. Please refer to https://aka.ms/CosmosClientEncryption for more details. ");
}
Expand All @@ -160,14 +156,14 @@ public static async Task<ClientEncryptionKeyResponse> RewrapClientEncryptionKeyA
KeyEncryptionKey keyEncryptionKey = KeyEncryptionKey.GetOrCreate(
clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Name,
clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Value,
encryptionKeyWrapProvider.EncryptionKeyStoreProviderImpl);
encryptionCosmosClient.EncryptionKeyStoreProviderImpl);

byte[] unwrappedKey = keyEncryptionKey.DecryptEncryptionKey(clientEncryptionKeyProperties.WrappedDataEncryptionKey);

keyEncryptionKey = KeyEncryptionKey.GetOrCreate(
newEncryptionKeyWrapMetadata.Name,
newEncryptionKeyWrapMetadata.Value,
encryptionKeyWrapProvider.EncryptionKeyStoreProviderImpl);
encryptionCosmosClient.EncryptionKeyStoreProviderImpl);

byte[] rewrappedKey = keyEncryptionKey.EncryptEncryptionKey(unwrappedKey);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,36 @@
namespace Microsoft.Azure.Cosmos.Encryption
{
using System;
using global::Azure.Core.Cryptography;
using Microsoft.Data.Encryption.Cryptography;

/// <summary>
/// The purpose/intention to introduce this class is to utilize the cache provide by the <see cref="EncryptionKeyStoreProvider"/> abstract class. This class basically
/// redirects all the corresponding calls to <see cref="EncryptionKeyWrapProvider"/> 's overridden methods and thus allowing us
/// redirects all the corresponding calls to <see cref="IKeyEncryptionKeyResolver"/> 's methods and thus allowing us
/// to utilize the virtual method <see cref="EncryptionKeyStoreProvider.GetOrCreateDataEncryptionKey"/> to access the cache.
///
/// Note: Since <see cref="EncryptionKeyStoreProvider.Sign"/> and <see cref="EncryptionKeyStoreProvider.Verify"/> methods are not exposed, <see cref="EncryptionKeyStoreProvider.GetOrCreateSignatureVerificationResult"/> is not supported either.
///
/// <remark>
/// The call hierarchy is as follows. Note, all core MDE API's used in internal cosmos encryption code are passed an EncryptionKeyStoreProviderImpl object.
/// ProtectedDataEncryptionKey -> KeyEncryptionKey(containing EncryptionKeyStoreProviderImpl object) -> EncryptionKeyStoreProviderImpl.WrapKey -> this.EncryptionKeyWrapProvider.WrapKeyAsync
/// ProtectedDataEncryptionKey -> KeyEncryptionKey(containing EncryptionKeyStoreProviderImpl object) -> EncryptionKeyStoreProviderImpl.UnWrapKey -> this.EncryptionKeyWrapProvider.UnwrapKeyAsync
/// ProtectedDataEncryptionKey -> KeyEncryptionKey(containing EncryptionKeyStoreProviderImpl object) -> EncryptionKeyStoreProviderImpl.WrapKey -> this.keyEncryptionKeyResolver.WrapKey
/// ProtectedDataEncryptionKey -> KeyEncryptionKey(containing EncryptionKeyStoreProviderImpl object) -> EncryptionKeyStoreProviderImpl.UnWrapKey -> this.keyEncryptionKeyResolver.UnwrapKey
/// </remark>
/// </summary>
internal class EncryptionKeyStoreProviderImpl : EncryptionKeyStoreProvider
{
private readonly EncryptionKeyWrapProvider encryptionKeyWrapProvider;
private readonly IKeyEncryptionKeyResolver keyEncryptionKeyResolver;

public EncryptionKeyStoreProviderImpl(EncryptionKeyWrapProvider encryptionKeyWrapProvider)
public EncryptionKeyStoreProviderImpl(IKeyEncryptionKeyResolver keyEncryptionKeyResolver, string providerName)
{
this.encryptionKeyWrapProvider = encryptionKeyWrapProvider;
this.keyEncryptionKeyResolver = keyEncryptionKeyResolver;
this.ProviderName = providerName;
this.DataEncryptionKeyCacheTimeToLive = TimeSpan.Zero;
}

public override string ProviderName => this.encryptionKeyWrapProvider.ProviderName;
public override string ProviderName { get; }

public override byte[] UnwrapKey(string encryptionKeyId, Data.Encryption.Cryptography.KeyEncryptionKeyAlgorithm algorithm, byte[] encryptedKey)
public override byte[] UnwrapKey(string encryptionKeyId, KeyEncryptionKeyAlgorithm algorithm, byte[] encryptedKey)
{
// since we do not expose GetOrCreateDataEncryptionKey we first look up the cache.
// Cache miss results in call to UnWrapCore which updates the cache after UnwrapKeyAsync is called.
Expand All @@ -40,35 +43,43 @@ public override byte[] UnwrapKey(string encryptionKeyId, Data.Encryption.Cryptog
// delegate that is called by GetOrCreateDataEncryptionKey, which unwraps the key and updates the cache in case of cache miss.
byte[] UnWrapKeyCore()
{
return this.encryptionKeyWrapProvider.UnwrapKeyAsync(encryptionKeyId, algorithm.ToString(), encryptedKey)
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
return this.keyEncryptionKeyResolver
.Resolve(encryptionKeyId)
.UnwrapKey(EncryptionKeyStoreProviderImpl.GetNameForKeyEncryptionKeyAlgorithm(algorithm), encryptedKey);
}
}

public override byte[] WrapKey(string encryptionKeyId, Data.Encryption.Cryptography.KeyEncryptionKeyAlgorithm algorithm, byte[] key)
public override byte[] WrapKey(string encryptionKeyId, KeyEncryptionKeyAlgorithm algorithm, byte[] key)
{
return this.encryptionKeyWrapProvider.WrapKeyAsync(encryptionKeyId, algorithm.ToString(), key)
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
return this.keyEncryptionKeyResolver
.Resolve(encryptionKeyId)
.WrapKey(EncryptionKeyStoreProviderImpl.GetNameForKeyEncryptionKeyAlgorithm(algorithm), key);
}

private static string GetNameForKeyEncryptionKeyAlgorithm(KeyEncryptionKeyAlgorithm algorithm)
{
if (algorithm == KeyEncryptionKeyAlgorithm.RSA_OAEP)
{
return "RSA-OAEP";
}

throw new InvalidOperationException(string.Format("Unexpected algorithm {0}", algorithm));
}

/// <Remark>
/// The public facing Cosmos Encryption library interface does not expose this method, hence not supported.
/// </Remark>
public override byte[] Sign(string encryptionKeyId, bool allowEnclaveComputations)
{
throw new NotSupportedException("The Sign operation is not supported. ");
throw new NotSupportedException("The Sign operation is not supported.");
}

/// <Remark>
/// The public facing Cosmos Encryption library interface does not expose this method, hence not supported.
/// </Remark>
public override bool Verify(string encryptionKeyId, bool allowEnclaveComputations, byte[] signature)
{
throw new NotSupportedException("The Verify operation is not supported. ");
throw new NotSupportedException("The Verify operation is not supported.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public async Task<AeadAes256CbcHmac256EncryptionAlgorithm> BuildEncryptionAlgori
// Here a request is sent out to unwrap using the Master Key configured via the Key Encryption Key.
protectedDataEncryptionKey = await this.BuildProtectedDataEncryptionKeyAsync(
clientEncryptionKeyProperties,
this.encryptionContainer.EncryptionCosmosClient.EncryptionKeyWrapProvider,
this.ClientEncryptionKeyId,
cancellationToken);
}
Expand All @@ -73,7 +72,6 @@ public async Task<AeadAes256CbcHmac256EncryptionAlgorithm> BuildEncryptionAlgori
// try to build the ProtectedDataEncryptionKey. If it fails, try to force refresh the gateway cache and get the latest client encryption key.
protectedDataEncryptionKey = await this.BuildProtectedDataEncryptionKeyAsync(
clientEncryptionKeyProperties,
this.encryptionContainer.EncryptionCosmosClient.EncryptionKeyWrapProvider,
this.ClientEncryptionKeyId,
cancellationToken);
}
Expand Down Expand Up @@ -141,7 +139,6 @@ private async Task<ProtectedDataEncryptionKey> ForceRefreshGatewayCacheAndBuildP

ProtectedDataEncryptionKey protectedDataEncryptionKey = await this.BuildProtectedDataEncryptionKeyAsync(
clientEncryptionKeyProperties,
this.encryptionContainer.EncryptionCosmosClient.EncryptionKeyWrapProvider,
this.ClientEncryptionKeyId,
cancellationToken);

Expand All @@ -150,7 +147,6 @@ private async Task<ProtectedDataEncryptionKey> ForceRefreshGatewayCacheAndBuildP

private async Task<ProtectedDataEncryptionKey> BuildProtectedDataEncryptionKeyAsync(
ClientEncryptionKeyProperties clientEncryptionKeyProperties,
EncryptionKeyWrapProvider encryptionKeyWrapProvider,
string keyId,
CancellationToken cancellationToken)
{
Expand All @@ -161,7 +157,7 @@ private async Task<ProtectedDataEncryptionKey> BuildProtectedDataEncryptionKeyAs
KeyEncryptionKey keyEncryptionKey = KeyEncryptionKey.GetOrCreate(
clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Name,
clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Value,
encryptionKeyWrapProvider.EncryptionKeyStoreProviderImpl);
this.encryptionContainer.EncryptionCosmosClient.EncryptionKeyStoreProviderImpl);

ProtectedDataEncryptionKey protectedDataEncryptionKey = ProtectedDataEncryptionKey.GetOrCreate(
keyId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Microsoft.Azure.Cosmos.Encryption
{
using global::Azure.Core.Cryptography;

/// <summary>
/// Has constants for names of well-known implementations of <see cref="IKeyEncryptionKeyResolver" />.
/// </summary>
public static class KeyEncryptionKeyResolverName
{
/// <summary>
/// IKeyEncryptionKeyResolver implementation for keys in Azure Key Vault.
/// </summary>
public const string AzureKeyVault = "AZURE_KEY_VAULT";
}
}
Loading