Skip to content

Commit 5e79cd7

Browse files
authored
Add support for AES encryption and key wrap algorithms (#16025)
* 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
1 parent effdccd commit 5e79cd7

39 files changed

+1940
-233
lines changed

sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 4.2.0-beta.3 (Unreleased)
44

5+
### Added
6+
7+
- Added `KeyType.OctHsm` to support "oct-HSM" key operations.
8+
- Added AES-GCM and AES-CBC support for encrypting and decrypting.
59

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

sdk/keyvault/Azure.Security.KeyVault.Keys/api/Azure.Security.KeyVault.Keys.netstandard2.0.cs

Lines changed: 65 additions & 1 deletion
Large diffs are not rendered by default.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Security.Cryptography;
5+
6+
namespace Azure.Security.KeyVault.Keys.Cryptography
7+
{
8+
/// <summary>
9+
/// Copied from Microsoft.Azure.KeyVault.Cryptography for vanilla AESCBC as defined in https://tools.ietf.org/html/rfc3394
10+
/// </summary>
11+
internal class AesCbc
12+
{
13+
public const int BlockByteSize = 16;
14+
15+
public static readonly AesCbc Aes128Cbc = new AesCbc("A128CBC", 128, PaddingMode.Zeros);
16+
public static readonly AesCbc Aes192Cbc = new AesCbc("A192CBC", 192, PaddingMode.Zeros);
17+
public static readonly AesCbc Aes256Cbc = new AesCbc("A256CBC", 256, PaddingMode.Zeros);
18+
19+
public static readonly AesCbc Aes128CbcPad = new AesCbc("A128CBCPAD", 128, PaddingMode.PKCS7);
20+
public static readonly AesCbc Aes192CbcPad = new AesCbc("A192CBCPAD", 192, PaddingMode.PKCS7);
21+
public static readonly AesCbc Aes256CbcPad = new AesCbc("A256CBCPAD", 256, PaddingMode.PKCS7);
22+
23+
private AesCbc(string name, int keySize, PaddingMode padding)
24+
{
25+
Name = name;
26+
KeySizeInBytes = keySize >> 3;
27+
Padding = padding;
28+
}
29+
30+
public string Name { get; }
31+
32+
public int KeySizeInBytes { get; }
33+
34+
public PaddingMode Padding { get; }
35+
36+
private static Aes Create( byte[] key, byte[] iv, PaddingMode padding )
37+
{
38+
var aes = Aes.Create();
39+
40+
aes.Mode = CipherMode.CBC;
41+
aes.Padding = padding;
42+
aes.KeySize = key.Length * 8;
43+
aes.Key = key;
44+
aes.IV = iv;
45+
46+
return aes;
47+
}
48+
49+
public ICryptoTransform CreateDecryptor( byte[] key, byte[] iv )
50+
{
51+
if ( key == null )
52+
throw new CryptographicException( "No key material" );
53+
54+
if (key.Length < KeySizeInBytes)
55+
throw new CryptographicException("key", $"key must be at least {KeySizeInBytes << 3} bits");
56+
57+
if ( iv == null )
58+
throw new CryptographicException( "No initialization vector" );
59+
60+
// Create the AES provider
61+
using ( var aes = Create( key.Take(KeySizeInBytes), iv, Padding ) )
62+
{
63+
return aes.CreateDecryptor();
64+
}
65+
}
66+
67+
public ICryptoTransform CreateEncryptor( byte[] key, byte[] iv )
68+
{
69+
if ( key == null )
70+
throw new CryptographicException( "No key material" );
71+
72+
if (key.Length < KeySizeInBytes)
73+
throw new CryptographicException("key", $"key must be at least {KeySizeInBytes << 3} bits");
74+
75+
if ( iv == null )
76+
throw new CryptographicException( "No initialization vector" );
77+
78+
// Create the AES provider
79+
using ( var aes = Create( key.Take(KeySizeInBytes), iv, Padding ) )
80+
{
81+
return aes.CreateEncryptor();
82+
}
83+
}
84+
}
85+
}

sdk/keyvault/Azure.Security.KeyVault.Keys/src/Cryptography/AesCryptographyProvider.cs

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public override bool SupportsOperation(KeyOperation operation)
1818
{
1919
if (KeyMaterial != null)
2020
{
21-
if (operation == KeyOperation.WrapKey || operation == KeyOperation.UnwrapKey)
21+
if (operation == KeyOperation.Encrypt || operation == KeyOperation.Decrypt || operation == KeyOperation.WrapKey || operation == KeyOperation.UnwrapKey)
2222
{
2323
return KeyMaterial.SupportsOperation(operation);
2424
}
@@ -27,26 +27,122 @@ public override bool SupportsOperation(KeyOperation operation)
2727
return false;
2828
}
2929

30+
public override DecryptResult Decrypt(DecryptOptions options, CancellationToken cancellationToken = default)
31+
{
32+
Argument.AssertNotNull(options, nameof(options));
33+
34+
ThrowIfTimeInvalid();
35+
36+
EncryptionAlgorithm algorithm = options.Algorithm;
37+
if (algorithm.GetAesCbcEncryptionAlgorithm() is AesCbc aesCbc)
38+
{
39+
using ICryptoTransform decryptor = aesCbc.CreateDecryptor(KeyMaterial.K, options.Iv);
40+
41+
byte[] ciphertext = options.Ciphertext;
42+
byte[] plaintext = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
43+
44+
return new DecryptResult
45+
{
46+
Algorithm = algorithm,
47+
KeyId = KeyMaterial.Id,
48+
Plaintext = plaintext,
49+
};
50+
}
51+
else if (algorithm.IsAesGcm() && AesGcmProxy.TryCreate(KeyMaterial.K, out AesGcmProxy aesGcm))
52+
{
53+
using (aesGcm)
54+
{
55+
byte[] ciphertext = options.Ciphertext;
56+
byte[] plaintext = new byte[ciphertext.Length];
57+
58+
aesGcm.Decrypt(options.Iv, ciphertext, options.AuthenticationTag, plaintext, options.AdditionalAuthenticatedData);
59+
60+
return new DecryptResult
61+
{
62+
Algorithm = algorithm,
63+
KeyId = KeyMaterial.Id,
64+
Plaintext = plaintext,
65+
};
66+
}
67+
}
68+
else
69+
{
70+
KeysEventSource.Singleton.AlgorithmNotSupported(nameof(Decrypt), algorithm);
71+
return null;
72+
}
73+
}
74+
75+
public override EncryptResult Encrypt(EncryptOptions options, CancellationToken cancellationToken = default)
76+
{
77+
Argument.AssertNotNull(options, nameof(options));
78+
79+
ThrowIfTimeInvalid();
80+
81+
EncryptionAlgorithm algorithm = options.Algorithm;
82+
if (algorithm.GetAesCbcEncryptionAlgorithm() is AesCbc aesCbc)
83+
{
84+
using ICryptoTransform encryptor = aesCbc.CreateEncryptor(KeyMaterial.K, options.Iv);
85+
86+
byte[] plaintext = options.Plaintext;
87+
byte[] ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
88+
89+
return new EncryptResult
90+
{
91+
Algorithm = algorithm,
92+
KeyId = KeyMaterial.Id,
93+
Ciphertext = ciphertext,
94+
Iv = options.Iv,
95+
};
96+
}
97+
else if (algorithm.IsAesGcm() && AesGcmProxy.TryCreate(KeyMaterial.K, out AesGcmProxy aesGcm))
98+
{
99+
using (aesGcm)
100+
{
101+
byte[] plaintext = options.Plaintext;
102+
byte[] ciphertext = new byte[plaintext.Length];
103+
byte[] tag = new byte[AesGcmProxy.NonceByteSize];
104+
105+
// Generate an nonce only for local AES-GCM; Managed HSM will do it service-side and err if serialized.
106+
byte[] iv = Crypto.GenerateIv(AesGcmProxy.NonceByteSize);
107+
108+
aesGcm.Encrypt(iv, plaintext, ciphertext, tag, options.AdditionalAuthenticatedData);
109+
110+
return new EncryptResult
111+
{
112+
Algorithm = algorithm,
113+
KeyId = KeyMaterial.Id,
114+
Ciphertext = ciphertext,
115+
Iv = iv,
116+
AuthenticationTag = tag,
117+
AdditionalAuthenticatedData = options.AdditionalAuthenticatedData,
118+
};
119+
}
120+
}
121+
else
122+
{
123+
KeysEventSource.Singleton.AlgorithmNotSupported(nameof(Encrypt), algorithm);
124+
return null;
125+
}
126+
}
127+
30128
public override UnwrapResult UnwrapKey(KeyWrapAlgorithm algorithm, byte[] encryptedKey, CancellationToken cancellationToken)
31129
{
32130
Argument.AssertNotNull(encryptedKey, nameof(encryptedKey));
33131

34-
int algorithmKeySizeBytes = algorithm.GetKeySizeInBytes();
35-
if (algorithmKeySizeBytes == 0)
132+
AesKw keyWrapAlgorithm = algorithm.GetAesKeyWrapAlgorithm();
133+
if (keyWrapAlgorithm == null)
36134
{
37135
KeysEventSource.Singleton.AlgorithmNotSupported(nameof(UnwrapKey), algorithm);
38136
return null;
39137
}
40138

41139
int keySizeBytes = GetKeySizeInBytes();
42-
if (keySizeBytes < algorithmKeySizeBytes)
140+
if (keySizeBytes < keyWrapAlgorithm.KeySizeInBytes)
43141
{
44-
throw new ArgumentException($"Key wrap algorithm {algorithm} key size {algorithmKeySizeBytes} is greater than the underlying key size {keySizeBytes}");
142+
throw new ArgumentException($"Key wrap algorithm {algorithm} key size {keyWrapAlgorithm.KeySizeInBytes} is greater than the underlying key size {keySizeBytes}");
45143
}
46144

47-
byte[] sizedKey = (keySizeBytes == algorithmKeySizeBytes) ? KeyMaterial.K : KeyMaterial.K.Take(algorithmKeySizeBytes);
48-
49-
using ICryptoTransform decryptor = AesKw.CreateDecryptor(sizedKey);
145+
using ICryptoTransform decryptor = keyWrapAlgorithm.CreateDecryptor(KeyMaterial.K);
50146

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

64160
ThrowIfTimeInvalid();
65161

66-
int algorithmKeySizeBytes = algorithm.GetKeySizeInBytes();
67-
if (algorithmKeySizeBytes == 0)
162+
AesKw keyWrapAlgorithm = algorithm.GetAesKeyWrapAlgorithm();
163+
if (keyWrapAlgorithm == null)
68164
{
69165
KeysEventSource.Singleton.AlgorithmNotSupported(nameof(WrapKey), algorithm);
70166
return null;
71167
}
72168

73169
int keySizeBytes = GetKeySizeInBytes();
74-
if (keySizeBytes < algorithmKeySizeBytes)
170+
if (keySizeBytes < keyWrapAlgorithm.KeySizeInBytes)
75171
{
76-
throw new ArgumentException($"Key wrap algorithm {algorithm} key size {algorithmKeySizeBytes} is greater than the underlying key size {keySizeBytes}");
172+
throw new ArgumentException($"Key wrap algorithm {algorithm} key size {keyWrapAlgorithm.KeySizeInBytes} is greater than the underlying key size {keySizeBytes}");
77173
}
78174

79-
byte[] sizedKey = (keySizeBytes == algorithmKeySizeBytes) ? KeyMaterial.K : KeyMaterial.K.Take(algorithmKeySizeBytes);
80-
81-
using ICryptoTransform encryptor = AesKw.CreateEncryptor(sizedKey);
175+
using ICryptoTransform encryptor = keyWrapAlgorithm.CreateEncryptor(KeyMaterial.K);
82176

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

92-
private int GetKeySizeInBits()
93-
{
94-
return GetKeySizeInBytes() << 3;
95-
}
96-
97186
private int GetKeySizeInBytes()
98187
{
99188
if (KeyMaterial.K != null)
@@ -102,7 +191,6 @@ private int GetKeySizeInBytes()
102191
}
103192

104193
return 0;
105-
106194
}
107195
}
108196
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Reflection;
6+
using System.Security.Cryptography;
7+
8+
namespace Azure.Security.KeyVault.Keys.Cryptography
9+
{
10+
/// <summary>
11+
/// Since System.Security.Cryptography.AesGcm requires targeting netstandard2.1,
12+
/// for needing this instance only we will proxy calls via reflection instead of multi-targeting.
13+
/// This feature will light up on netcoreapp3.0 or newer.
14+
/// </summary>
15+
internal class AesGcmProxy : IDisposable
16+
{
17+
public const int NonceByteSize = 12;
18+
19+
private static MethodInfo s_decryptMethod;
20+
private static MethodInfo s_encryptMethod;
21+
22+
private readonly object _aes;
23+
24+
private AesGcmProxy(object aes)
25+
{
26+
_aes = aes ?? throw new ArgumentNullException(nameof(aes));
27+
}
28+
29+
public static bool TryCreate(byte[] key, out AesGcmProxy proxy)
30+
{
31+
Type t = typeof(Aes).Assembly.GetType("System.Security.Cryptography.AesGcm", false);
32+
if (t != null)
33+
{
34+
try
35+
{
36+
object aes = Activator.CreateInstance(t, key);
37+
38+
proxy = new AesGcmProxy(aes);
39+
return true;
40+
}
41+
catch
42+
{
43+
}
44+
}
45+
46+
proxy = null;
47+
return false;
48+
}
49+
50+
public void Decrypt(byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext, byte[] associatedData = default)
51+
{
52+
if (s_decryptMethod is null)
53+
{
54+
s_decryptMethod = _aes.GetType().GetMethod(nameof(Decrypt), new Type[] { typeof(byte[]), typeof(byte[]), typeof(byte[]), typeof(byte[]), typeof(byte[]) }) ??
55+
throw new InvalidOperationException($"{nameof(Decrypt)} method not found");
56+
}
57+
58+
s_decryptMethod.Invoke(_aes, new object[] { nonce, ciphertext, tag, plaintext, associatedData });
59+
}
60+
61+
public void Encrypt(byte[] nonce, byte[] plaintext, byte[] ciphertext, byte[] tag, byte[] associatedData = default)
62+
{
63+
if (s_encryptMethod is null)
64+
{
65+
s_encryptMethod = _aes.GetType().GetMethod(nameof(Encrypt), new Type[] { typeof(byte[]), typeof(byte[]), typeof(byte[]), typeof(byte[]), typeof(byte[]) }) ??
66+
throw new InvalidOperationException($"{nameof(Encrypt)} method not found");
67+
}
68+
69+
s_encryptMethod.Invoke(_aes, new object[] { nonce, plaintext, ciphertext, tag, associatedData });
70+
}
71+
72+
public void Dispose() => ((IDisposable)_aes)?.Dispose();
73+
}
74+
}

0 commit comments

Comments
 (0)