Skip to content

Commit

Permalink
ClientEncryption: Adds the use of Azure.Core interfaces in public sur…
Browse files Browse the repository at this point in the history
…face (#3050)

Use interfaces from Azure.Core.Cryptography for customers to provide the implementation used to wrap/unwrap the data encryption key instead of the existing custom wrapper interfaces created over Microsoft.Data.Encryption.Cryptography. This allows using the Azure.Security.KeyVault.Keys implementation for CMKs in Azure Key Vault and we no longer need the MDE specific Microsoft.Data.Encryption.AzureKeyVaultProvider.
  • Loading branch information
abhijitpai committed Mar 3, 2022
1 parent f9baebf commit 6156c4c
Show file tree
Hide file tree
Showing 17 changed files with 243 additions and 1,098 deletions.
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,
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; }

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

0 comments on commit 6156c4c

Please sign in to comment.