Skip to content

Commit

Permalink
Add support for AES encryption and key wrap algorithms (#16025)
Browse files Browse the repository at this point in the history
* Add oct-HSM key type

Resolves #14887

* Add additional encryption algorithms to Keys

Resolves #14888

* Add AES-CBC and AES-GCM implementations/proxies

* Add AES support to AesCryptographyProvider

* Use factory methods for encrypt/decrypt options

* Update public API
  • Loading branch information
heaths authored Oct 22, 2020
1 parent effdccd commit 5e79cd7
Show file tree
Hide file tree
Showing 39 changed files with 1,940 additions and 233 deletions.
4 changes: 4 additions & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 4.2.0-beta.3 (Unreleased)

### Added

- Added `KeyType.OctHsm` to support "oct-HSM" key operations.
- Added AES-GCM and AES-CBC support for encrypting and decrypting.

## 4.2.0-beta.2 (2020-10-06)

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Security.Cryptography;

namespace Azure.Security.KeyVault.Keys.Cryptography
{
/// <summary>
/// Copied from Microsoft.Azure.KeyVault.Cryptography for vanilla AESCBC as defined in https://tools.ietf.org/html/rfc3394
/// </summary>
internal class AesCbc
{
public const int BlockByteSize = 16;

public static readonly AesCbc Aes128Cbc = new AesCbc("A128CBC", 128, PaddingMode.Zeros);
public static readonly AesCbc Aes192Cbc = new AesCbc("A192CBC", 192, PaddingMode.Zeros);
public static readonly AesCbc Aes256Cbc = new AesCbc("A256CBC", 256, PaddingMode.Zeros);

public static readonly AesCbc Aes128CbcPad = new AesCbc("A128CBCPAD", 128, PaddingMode.PKCS7);
public static readonly AesCbc Aes192CbcPad = new AesCbc("A192CBCPAD", 192, PaddingMode.PKCS7);
public static readonly AesCbc Aes256CbcPad = new AesCbc("A256CBCPAD", 256, PaddingMode.PKCS7);

private AesCbc(string name, int keySize, PaddingMode padding)
{
Name = name;
KeySizeInBytes = keySize >> 3;
Padding = padding;
}

public string Name { get; }

public int KeySizeInBytes { get; }

public PaddingMode Padding { get; }

private static Aes Create( byte[] key, byte[] iv, PaddingMode padding )
{
var aes = Aes.Create();

aes.Mode = CipherMode.CBC;
aes.Padding = padding;
aes.KeySize = key.Length * 8;
aes.Key = key;
aes.IV = iv;

return aes;
}

public ICryptoTransform CreateDecryptor( byte[] key, byte[] iv )
{
if ( key == null )
throw new CryptographicException( "No key material" );

if (key.Length < KeySizeInBytes)
throw new CryptographicException("key", $"key must be at least {KeySizeInBytes << 3} bits");

if ( iv == null )
throw new CryptographicException( "No initialization vector" );

// Create the AES provider
using ( var aes = Create( key.Take(KeySizeInBytes), iv, Padding ) )
{
return aes.CreateDecryptor();
}
}

public ICryptoTransform CreateEncryptor( byte[] key, byte[] iv )
{
if ( key == null )
throw new CryptographicException( "No key material" );

if (key.Length < KeySizeInBytes)
throw new CryptographicException("key", $"key must be at least {KeySizeInBytes << 3} bits");

if ( iv == null )
throw new CryptographicException( "No initialization vector" );

// Create the AES provider
using ( var aes = Create( key.Take(KeySizeInBytes), iv, Padding ) )
{
return aes.CreateEncryptor();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public override bool SupportsOperation(KeyOperation operation)
{
if (KeyMaterial != null)
{
if (operation == KeyOperation.WrapKey || operation == KeyOperation.UnwrapKey)
if (operation == KeyOperation.Encrypt || operation == KeyOperation.Decrypt || operation == KeyOperation.WrapKey || operation == KeyOperation.UnwrapKey)
{
return KeyMaterial.SupportsOperation(operation);
}
Expand All @@ -27,26 +27,122 @@ public override bool SupportsOperation(KeyOperation operation)
return false;
}

public override DecryptResult Decrypt(DecryptOptions options, CancellationToken cancellationToken = default)
{
Argument.AssertNotNull(options, nameof(options));

ThrowIfTimeInvalid();

EncryptionAlgorithm algorithm = options.Algorithm;
if (algorithm.GetAesCbcEncryptionAlgorithm() is AesCbc aesCbc)
{
using ICryptoTransform decryptor = aesCbc.CreateDecryptor(KeyMaterial.K, options.Iv);

byte[] ciphertext = options.Ciphertext;
byte[] plaintext = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);

return new DecryptResult
{
Algorithm = algorithm,
KeyId = KeyMaterial.Id,
Plaintext = plaintext,
};
}
else if (algorithm.IsAesGcm() && AesGcmProxy.TryCreate(KeyMaterial.K, out AesGcmProxy aesGcm))
{
using (aesGcm)
{
byte[] ciphertext = options.Ciphertext;
byte[] plaintext = new byte[ciphertext.Length];

aesGcm.Decrypt(options.Iv, ciphertext, options.AuthenticationTag, plaintext, options.AdditionalAuthenticatedData);

return new DecryptResult
{
Algorithm = algorithm,
KeyId = KeyMaterial.Id,
Plaintext = plaintext,
};
}
}
else
{
KeysEventSource.Singleton.AlgorithmNotSupported(nameof(Decrypt), algorithm);
return null;
}
}

public override EncryptResult Encrypt(EncryptOptions options, CancellationToken cancellationToken = default)
{
Argument.AssertNotNull(options, nameof(options));

ThrowIfTimeInvalid();

EncryptionAlgorithm algorithm = options.Algorithm;
if (algorithm.GetAesCbcEncryptionAlgorithm() is AesCbc aesCbc)
{
using ICryptoTransform encryptor = aesCbc.CreateEncryptor(KeyMaterial.K, options.Iv);

byte[] plaintext = options.Plaintext;
byte[] ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);

return new EncryptResult
{
Algorithm = algorithm,
KeyId = KeyMaterial.Id,
Ciphertext = ciphertext,
Iv = options.Iv,
};
}
else if (algorithm.IsAesGcm() && AesGcmProxy.TryCreate(KeyMaterial.K, out AesGcmProxy aesGcm))
{
using (aesGcm)
{
byte[] plaintext = options.Plaintext;
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[AesGcmProxy.NonceByteSize];

// Generate an nonce only for local AES-GCM; Managed HSM will do it service-side and err if serialized.
byte[] iv = Crypto.GenerateIv(AesGcmProxy.NonceByteSize);

aesGcm.Encrypt(iv, plaintext, ciphertext, tag, options.AdditionalAuthenticatedData);

return new EncryptResult
{
Algorithm = algorithm,
KeyId = KeyMaterial.Id,
Ciphertext = ciphertext,
Iv = iv,
AuthenticationTag = tag,
AdditionalAuthenticatedData = options.AdditionalAuthenticatedData,
};
}
}
else
{
KeysEventSource.Singleton.AlgorithmNotSupported(nameof(Encrypt), algorithm);
return null;
}
}

public override UnwrapResult UnwrapKey(KeyWrapAlgorithm algorithm, byte[] encryptedKey, CancellationToken cancellationToken)
{
Argument.AssertNotNull(encryptedKey, nameof(encryptedKey));

int algorithmKeySizeBytes = algorithm.GetKeySizeInBytes();
if (algorithmKeySizeBytes == 0)
AesKw keyWrapAlgorithm = algorithm.GetAesKeyWrapAlgorithm();
if (keyWrapAlgorithm == null)
{
KeysEventSource.Singleton.AlgorithmNotSupported(nameof(UnwrapKey), algorithm);
return null;
}

int keySizeBytes = GetKeySizeInBytes();
if (keySizeBytes < algorithmKeySizeBytes)
if (keySizeBytes < keyWrapAlgorithm.KeySizeInBytes)
{
throw new ArgumentException($"Key wrap algorithm {algorithm} key size {algorithmKeySizeBytes} is greater than the underlying key size {keySizeBytes}");
throw new ArgumentException($"Key wrap algorithm {algorithm} key size {keyWrapAlgorithm.KeySizeInBytes} is greater than the underlying key size {keySizeBytes}");
}

byte[] sizedKey = (keySizeBytes == algorithmKeySizeBytes) ? KeyMaterial.K : KeyMaterial.K.Take(algorithmKeySizeBytes);

using ICryptoTransform decryptor = AesKw.CreateDecryptor(sizedKey);
using ICryptoTransform decryptor = keyWrapAlgorithm.CreateDecryptor(KeyMaterial.K);

byte[] key = decryptor.TransformFinalBlock(encryptedKey, 0, encryptedKey.Length);
return new UnwrapResult
Expand All @@ -63,22 +159,20 @@ public override WrapResult WrapKey(KeyWrapAlgorithm algorithm, byte[] key, Cance

ThrowIfTimeInvalid();

int algorithmKeySizeBytes = algorithm.GetKeySizeInBytes();
if (algorithmKeySizeBytes == 0)
AesKw keyWrapAlgorithm = algorithm.GetAesKeyWrapAlgorithm();
if (keyWrapAlgorithm == null)
{
KeysEventSource.Singleton.AlgorithmNotSupported(nameof(WrapKey), algorithm);
return null;
}

int keySizeBytes = GetKeySizeInBytes();
if (keySizeBytes < algorithmKeySizeBytes)
if (keySizeBytes < keyWrapAlgorithm.KeySizeInBytes)
{
throw new ArgumentException($"Key wrap algorithm {algorithm} key size {algorithmKeySizeBytes} is greater than the underlying key size {keySizeBytes}");
throw new ArgumentException($"Key wrap algorithm {algorithm} key size {keyWrapAlgorithm.KeySizeInBytes} is greater than the underlying key size {keySizeBytes}");
}

byte[] sizedKey = (keySizeBytes == algorithmKeySizeBytes) ? KeyMaterial.K : KeyMaterial.K.Take(algorithmKeySizeBytes);

using ICryptoTransform encryptor = AesKw.CreateEncryptor(sizedKey);
using ICryptoTransform encryptor = keyWrapAlgorithm.CreateEncryptor(KeyMaterial.K);

byte[] encryptedKey = encryptor.TransformFinalBlock(key, 0, key.Length);
return new WrapResult
Expand All @@ -89,11 +183,6 @@ public override WrapResult WrapKey(KeyWrapAlgorithm algorithm, byte[] key, Cance
};
}

private int GetKeySizeInBits()
{
return GetKeySizeInBytes() << 3;
}

private int GetKeySizeInBytes()
{
if (KeyMaterial.K != null)
Expand All @@ -102,7 +191,6 @@ private int GetKeySizeInBytes()
}

return 0;

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Reflection;
using System.Security.Cryptography;

namespace Azure.Security.KeyVault.Keys.Cryptography
{
/// <summary>
/// Since System.Security.Cryptography.AesGcm requires targeting netstandard2.1,
/// for needing this instance only we will proxy calls via reflection instead of multi-targeting.
/// This feature will light up on netcoreapp3.0 or newer.
/// </summary>
internal class AesGcmProxy : IDisposable
{
public const int NonceByteSize = 12;

private static MethodInfo s_decryptMethod;
private static MethodInfo s_encryptMethod;

private readonly object _aes;

private AesGcmProxy(object aes)
{
_aes = aes ?? throw new ArgumentNullException(nameof(aes));
}

public static bool TryCreate(byte[] key, out AesGcmProxy proxy)
{
Type t = typeof(Aes).Assembly.GetType("System.Security.Cryptography.AesGcm", false);
if (t != null)
{
try
{
object aes = Activator.CreateInstance(t, key);

proxy = new AesGcmProxy(aes);
return true;
}
catch
{
}
}

proxy = null;
return false;
}

public void Decrypt(byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext, byte[] associatedData = default)
{
if (s_decryptMethod is null)
{
s_decryptMethod = _aes.GetType().GetMethod(nameof(Decrypt), new Type[] { typeof(byte[]), typeof(byte[]), typeof(byte[]), typeof(byte[]), typeof(byte[]) }) ??
throw new InvalidOperationException($"{nameof(Decrypt)} method not found");
}

s_decryptMethod.Invoke(_aes, new object[] { nonce, ciphertext, tag, plaintext, associatedData });
}

public void Encrypt(byte[] nonce, byte[] plaintext, byte[] ciphertext, byte[] tag, byte[] associatedData = default)
{
if (s_encryptMethod is null)
{
s_encryptMethod = _aes.GetType().GetMethod(nameof(Encrypt), new Type[] { typeof(byte[]), typeof(byte[]), typeof(byte[]), typeof(byte[]), typeof(byte[]) }) ??
throw new InvalidOperationException($"{nameof(Encrypt)} method not found");
}

s_encryptMethod.Invoke(_aes, new object[] { nonce, plaintext, ciphertext, tag, associatedData });
}

public void Dispose() => ((IDisposable)_aes)?.Dispose();
}
}
Loading

0 comments on commit 5e79cd7

Please sign in to comment.