Skip to content

Commit

Permalink
Fixed encryption key resolver (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
PascalSenn authored Oct 21, 2022
1 parent 7a0c0b9 commit 6154a64
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Confix.CryptoProviders;

public interface IKeyEncryptionKeyProvider
public interface IEncryptionKeyProvider
{
ValueTask<byte[]> GetKeyAsync(string topic, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Confix.CryptoProviders;

public interface IDataEncryptionKeyRepository
{
Task<DataEncryptionKey> GetSecretByTopicAsync(
Task<DataEncryptionKey?> GetSecretByTopicAsync(
string topic,
CancellationToken cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@ public sealed class AzureKeyVaultOptions
public string Url { get; set; } = default!;

public string Algorithm { get; set; } = "RSA-OAEP-256";

public string TenantId { get; set; } = default!;

public string ClientId { get; set; } = default!;

public string ClientSecret { get; set; } = default!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static ICryptoProviderDescriptor UseAzureKeyVaultKeyEncryptionKeys(
.BindConfiguration(pathToConfig);

services.Services.AddSingleton<IKeyEncryptionKeyCache, KeyEncryptionKeyCache>();
services.Services.AddSingleton<IKeyEncryptionKeyProvider, KeyEncryptionKeyProvider>();
services.Services.AddSingleton<IEncryptionKeyProvider, EncryptionKeyProvider>();
services.Services.AddSingleton<ICryptographyClientFactory, CryptographyClientFactory>();
services.Services.AddSingleton<KeyVaultCryptoProvider>();
services.Services
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ public CryptographyClientFactory(IOptionsMonitor<AzureKeyVaultOptions> options)
_options = options;
}

private DefaultAzureCredential Credentials => new();
private ClientSecretCredential Credentials => new(
_options.CurrentValue.TenantId,
_options.CurrentValue.ClientId,
_options.CurrentValue.ClientSecret);

public CryptographyClient CreateCryptoClient(string keyId)
{
Expand Down
125 changes: 125 additions & 0 deletions src/Backend/src/CryptoProviders.AzureKeyVault/EncryptionKeyProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Security.Cryptography;
using System.Transactions;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using Microsoft.Extensions.Options;

namespace Confix.CryptoProviders.AzureKeyVault;

internal sealed class EncryptionKeyProvider : IEncryptionKeyProvider
{
private readonly ICryptographyClientFactory _clientFactory;
private readonly IDataEncryptionKeyRepository _dataEncryptionKeys;
private readonly IOptionsMonitor<AzureKeyVaultOptions> _options;
private readonly IKeyEncryptionKeyCache _keyEncryptionKeyCache;

public EncryptionKeyProvider(
IKeyEncryptionKeyCache keyEncryptionKeyCache,
ICryptographyClientFactory clientFactory,
IDataEncryptionKeyRepository dataEncryptionKeys,
IOptionsMonitor<AzureKeyVaultOptions> options)
{
_keyEncryptionKeyCache = keyEncryptionKeyCache;
_dataEncryptionKeys = dataEncryptionKeys;
_options = options;
_clientFactory = clientFactory;
}

public ValueTask<byte[]> GetKeyAsync(string topic, CancellationToken cancellationToken)
{
return _keyEncryptionKeyCache.GetOrCreateAsync(topic, CreateKey);

async Task<byte[]> CreateKey()
{
var secret =
await _dataEncryptionKeys.GetSecretByTopicAsync(topic, cancellationToken);

if (secret is null)
{
secret = await GetOrCreateSecretAsync(topic, cancellationToken);
}

return await DecryptSecretAsync(secret, cancellationToken);
}
}

private async Task EnsureKeyAsync(
string topic,
CancellationToken cancellationToken)
{
var keyClient = _clientFactory.CreateKeyClient(topic);

try
{
await keyClient.GetKeyAsync(topic, cancellationToken: cancellationToken);
}
catch (Azure.RequestFailedException ex)
{
if (ex.Message
.Contains($"A key with (name/id) {topic} was not found in this key vault."))
{
await keyClient.CreateKeyAsync(
topic,
KeyType.Rsa,
new() { KeyOperations = { KeyOperation.Encrypt, KeyOperation.Decrypt } },
cancellationToken);
}
else
{
throw;
}
}
}

private async Task<byte[]> DecryptSecretAsync(
DataEncryptionKey secret,
CancellationToken cancellationToken)
{
EncryptionAlgorithm algorithm = new(secret.EncryptionAlgorithm);
var encryptionClient = _clientFactory.CreateCryptoClient(secret.Topic);

var decodedKey = Convert.FromBase64String(secret.Key);

var decryptedKey =
await encryptionClient.DecryptAsync(algorithm, decodedKey, cancellationToken);

return decryptedKey.Plaintext;
}

private async Task<DataEncryptionKey> GetOrCreateSecretAsync(
string topic,
CancellationToken cancellationToken)
{
using (var scope = new TransactionScope(
TransactionScopeOption.RequiresNew,
TransactionScopeAsyncFlowOption.Enabled))
{
await EnsureKeyAsync(topic, cancellationToken);

using var aes = Aes.Create();
aes.GenerateKey();

EncryptionAlgorithm algorithm = new(_options.CurrentValue.Algorithm);
var encryptionClient = _clientFactory.CreateCryptoClient(topic);

var encryptedKey =
await encryptionClient.EncryptAsync(algorithm, aes.Key, cancellationToken);

var encodedKey = Convert.ToBase64String(encryptedKey.Ciphertext);

var newSecret = new DataEncryptionKey(
Guid.NewGuid(),
DateTime.UtcNow,
topic,
encodedKey,
_options.CurrentValue.Algorithm);

// It could be that another service has already created a secret. This way
// we ensure that we would receive this secret and throw away our secret
var secret =
await _dataEncryptionKeys.GetOrCreateByTopicAsync(newSecret, cancellationToken);
scope.Complete();
return secret;
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ namespace Confix.CryptoProviders.AzureKeyVault;

internal sealed class KeyVaultCryptoProvider : IEncryptor, IDecryptor
{
private readonly IKeyEncryptionKeyProvider _keyEncryptionKeyProvider;
private readonly IEncryptionKeyProvider _encryptionKeyProvider;

public KeyVaultCryptoProvider(IKeyEncryptionKeyProvider keyEncryptionKeyProvider)
public KeyVaultCryptoProvider(IEncryptionKeyProvider encryptionKeyProvider)
{
_keyEncryptionKeyProvider = keyEncryptionKeyProvider;
_encryptionKeyProvider = encryptionKeyProvider;
}

public async Task<string> DecryptAsync(
EncryptedValue encryptedValue,
CancellationToken cancellationToken)
{
var key = await _keyEncryptionKeyProvider.GetKeyAsync(encryptedValue.Topic,
var key = await _encryptionKeyProvider.GetKeyAsync(encryptedValue.Topic,
cancellationToken);
using var aes = Aes.Create();
aes.Key = key;
Expand All @@ -32,7 +32,7 @@ public async Task<EncryptedValue> EncryptAsync(
string value,
CancellationToken cancellationToken)
{
var key = await _keyEncryptionKeyProvider.GetKeyAsync(topic, cancellationToken);
var key = await _encryptionKeyProvider.GetKeyAsync(topic, cancellationToken);
using var aes = Aes.Create();
aes.GenerateIV();
aes.Key = key;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public async Task<DataEncryptionKey> GetOrCreateByTopicAsync(

var options = new FindOneAndUpdateOptions<DataEncryptionKey>
{
IsUpsert = true, ReturnDocument = ReturnDocument.After
IsUpsert = true,
ReturnDocument = ReturnDocument.After
};

return await _dbContext.Secrets.FindOneAndUpdateAsync(
Expand All @@ -36,7 +37,7 @@ public async Task<DataEncryptionKey> GetOrCreateByTopicAsync(
cancellationToken);
}

public async Task<DataEncryptionKey> GetSecretByTopicAsync(
public async Task<DataEncryptionKey?> GetSecretByTopicAsync(
string topic,
CancellationToken cancellationToken)
{
Expand Down

0 comments on commit 6154a64

Please sign in to comment.