Skip to content

Commit 5be1082

Browse files
authored
DataProtection use built in AesGcm where available (#29814)
1 parent e51b58e commit 5be1082

File tree

8 files changed

+289
-13
lines changed

8 files changed

+289
-13
lines changed

src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,19 @@ public static T Fail<T>(string message) where T : class
7676
#endif
7777
public static bool TimeConstantBuffersAreEqual(byte* bufA, byte* bufB, uint count)
7878
{
79+
#if NETCOREAPP
80+
var byteCount = Convert.ToInt32(count);
81+
var bytesA = new ReadOnlySpan<byte>(bufA, byteCount);
82+
var bytesB = new ReadOnlySpan<byte>(bufB, byteCount);
83+
return CryptographicOperations.FixedTimeEquals(bytesA, bytesB);
84+
#else
7985
bool areEqual = true;
8086
for (uint i = 0; i < count; i++)
8187
{
8288
areEqual &= (bufA[i] == bufB[i]);
8389
}
8490
return areEqual;
91+
#endif
8592
}
8693

8794
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
@@ -91,12 +98,21 @@ public static bool TimeConstantBuffersAreEqual(byte[] bufA, int offsetA, int cou
9198
// An error at the call site isn't usable for timing attacks.
9299
Assert(countA == countB, "countA == countB");
93100

101+
#if NETCOREAPP
102+
unsafe
103+
{
104+
return CryptographicOperations.FixedTimeEquals(
105+
bufA.AsSpan(start: offsetA, length: countA),
106+
bufB.AsSpan(start: offsetB, length: countB));
107+
}
108+
#else
94109
bool areEqual = true;
95110
for (int i = 0; i < countA; i++)
96111
{
97112
areEqual &= (bufA[offsetA + i] == bufB[offsetB + i]);
98113
}
99114
return areEqual;
115+
#endif
100116
}
101117
}
102118
}

src/DataProtection/DataProtection/src/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Cryptography.Cng;
1111
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
1212
using Microsoft.AspNetCore.DataProtection.KeyManagement;
13+
using Microsoft.AspNetCore.DataProtection.Managed;
1314
using Microsoft.Extensions.Logging;
1415

1516
namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
@@ -54,6 +55,9 @@ public AuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
5455

5556
if (IsGcmAlgorithm(authenticatedConfiguration.EncryptionAlgorithm))
5657
{
58+
#if NETCOREAPP
59+
return new AesGcmAuthenticatedEncryptor(secret, GetAlgorithmKeySizeInBits(authenticatedConfiguration.EncryptionAlgorithm) / 8);
60+
#else
5761
// GCM requires CNG, and CNG is only supported on Windows.
5862
if (!OSVersionUtil.IsWindows())
5963
{
@@ -69,6 +73,7 @@ public AuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
6973
};
7074

7175
return new CngGcmAuthenticatedEncryptorFactory(_loggerFactory).CreateAuthenticatedEncryptorInstance(secret, configuration);
76+
#endif
7277
}
7378
else
7479
{

src/DataProtection/DataProtection/src/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactory.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
1818
{
1919
/// <summary>
20-
/// An <see cref="IAuthenticatedEncryptorFactory"/> for <see cref="GcmAuthenticatedEncryptor"/>.
20+
/// An <see cref="IAuthenticatedEncryptorFactory"/> for <see cref="CngGcmAuthenticatedEncryptor"/>.
2121
/// </summary>
2222
public sealed class CngGcmAuthenticatedEncryptorFactory : IAuthenticatedEncryptorFactory
2323
{
@@ -48,7 +48,7 @@ public CngGcmAuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
4848

4949
[SupportedOSPlatform("windows")]
5050
[return: NotNullIfNotNull("configuration")]
51-
internal GcmAuthenticatedEncryptor? CreateAuthenticatedEncryptorInstance(
51+
internal CngGcmAuthenticatedEncryptor? CreateAuthenticatedEncryptorInstance(
5252
ISecret secret,
5353
CngGcmAuthenticatedEncryptorConfiguration configuration)
5454
{
@@ -57,7 +57,7 @@ public CngGcmAuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
5757
return null;
5858
}
5959

60-
return new GcmAuthenticatedEncryptor(
60+
return new CngGcmAuthenticatedEncryptor(
6161
keyDerivationKey: new Secret(secret),
6262
symmetricAlgorithmHandle: GetSymmetricBlockCipherAlgorithmHandle(configuration),
6363
symmetricAlgorithmKeySizeInBytes: (uint)(configuration.EncryptionAlgorithmKeySize / 8));

src/DataProtection/DataProtection/src/Cng/GcmAuthenticatedEncryptor.cs renamed to src/DataProtection/DataProtection/src/Cng/CngGcmAuthenticatedEncryptor.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
54
using Microsoft.AspNetCore.Cryptography;
65
using Microsoft.AspNetCore.Cryptography.Cng;
76
using Microsoft.AspNetCore.Cryptography.SafeHandles;
@@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.DataProtection.Cng
2120
// going to the IV. This means that we'll only hit the 2^-32 probability limit after 2^96 encryption
2221
// operations, which will realistically never happen. (At the absurd rate of one encryption operation
2322
// per nanosecond, it would still take 180 times the age of the universe to hit 2^96 operations.)
24-
internal unsafe sealed class GcmAuthenticatedEncryptor : CngAuthenticatedEncryptorBase
23+
internal unsafe sealed class CngGcmAuthenticatedEncryptor : CngAuthenticatedEncryptorBase
2524
{
2625
// Having a key modifier ensures with overwhelming probability that no two encryption operations
2726
// will ever derive the same (encryption subkey, MAC subkey) pair. This limits an attacker's
@@ -38,12 +37,12 @@ internal unsafe sealed class GcmAuthenticatedEncryptor : CngAuthenticatedEncrypt
3837
private readonly BCryptAlgorithmHandle _symmetricAlgorithmHandle;
3938
private readonly uint _symmetricAlgorithmSubkeyLengthInBytes;
4039

41-
public GcmAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, IBCryptGenRandom? genRandom = null)
40+
public CngGcmAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, IBCryptGenRandom? genRandom = null)
4241
{
4342
// Is the key size appropriate?
4443
AlgorithmAssert.IsAllowableSymmetricAlgorithmKeySize(checked(symmetricAlgorithmKeySizeInBytes * 8));
4544
CryptoUtil.Assert(symmetricAlgorithmHandle.GetCipherBlockLength() == 128 / 8, "GCM requires a block cipher algorithm with a 128-bit block size.");
46-
45+
4746
_genRandom = genRandom ?? BCryptGenRandomImpl.Instance;
4847
_sp800_108_ctr_hmac_provider = SP800_108_CTR_HMACSHA512Util.CreateProvider(keyDerivationKey);
4948
_symmetricAlgorithmHandle = symmetricAlgorithmHandle;
@@ -151,7 +150,6 @@ protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byt
151150
cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes);
152151

153152
// Perform the decryption operation
154-
155153
using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
156154
{
157155
byte dummy;
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#if NETCOREAPP
5+
using System;
6+
using System.IO;
7+
using System.Security.Cryptography;
8+
using Microsoft.AspNetCore.Cryptography;
9+
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
10+
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
11+
using Microsoft.AspNetCore.DataProtection.SP800_108;
12+
13+
namespace Microsoft.AspNetCore.DataProtection.Managed
14+
{
15+
// An encryptor that uses AesGcm to do encryption
16+
internal unsafe sealed class AesGcmAuthenticatedEncryptor : IOptimizedAuthenticatedEncryptor, IDisposable
17+
{
18+
// Having a key modifier ensures with overwhelming probability that no two encryption operations
19+
// will ever derive the same (encryption subkey, MAC subkey) pair. This limits an attacker's
20+
// ability to mount a key-dependent chosen ciphertext attack. See also the class-level comment
21+
// on CngGcmAuthenticatedEncryptor for how this is used to overcome GCM's IV limitations.
22+
private const int KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
23+
24+
private const int NONCE_SIZE_IN_BYTES = 96 / 8; // GCM has a fixed 96-bit IV
25+
private const int TAG_SIZE_IN_BYTES = 128 / 8; // we're hardcoding a 128-bit authentication tag size
26+
27+
// See CngGcmAuthenticatedEncryptor.CreateContextHeader for how these were precomputed
28+
29+
// 128 "00-01-00-00-00-10-00-00-00-0C-00-00-00-10-00-00-00-10-95-7C-50-FF-69-2E-38-8B-9A-D5-C7-68-9E-4B-9E-2B"
30+
private static readonly byte[] AES_128_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x95, 0x7C, 0x50, 0xFF, 0x69, 0x2E, 0x38, 0x8B, 0x9A, 0xD5, 0xC7, 0x68, 0x9E, 0x4B, 0x9E, 0x2B };
31+
32+
// 192 "00-01-00-00-00-18-00-00-00-0C-00-00-00-10-00-00-00-10-0D-AA-01-3A-95-0A-DA-2B-79-8F-5F-F2-72-FA-D3-63"
33+
private static readonly byte[] AES_192_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x0D, 0xAA, 0x01, 0x3A, 0x95, 0x0A, 0xDA, 0x2B, 0x79, 0x8F, 0x5F, 0xF2, 0x72, 0xFA, 0xD3, 0x63 };
34+
35+
// 256 00-01-00-00-00-20-00-00-00-0C-00-00-00-10-00-00-00-10-E7-DC-CE-66-DF-85-5A-32-3A-6B-B7-BD-7A-59-BE-45
36+
private static readonly byte[] AES_256_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0xE7, 0xDC, 0xCE, 0x66, 0xDF, 0x85, 0x5A, 0x32, 0x3A, 0x6B, 0xB7, 0xBD, 0x7A, 0x59, 0xBE, 0x45 };
37+
38+
private static readonly Func<byte[], HashAlgorithm> _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512
39+
40+
private readonly byte[] _contextHeader;
41+
42+
private readonly Secret _keyDerivationKey;
43+
private readonly int _derivedkeySizeInBytes;
44+
private readonly IManagedGenRandom _genRandom;
45+
46+
public AesGcmAuthenticatedEncryptor(ISecret keyDerivationKey, int derivedKeySizeInBytes, IManagedGenRandom? genRandom = null)
47+
{
48+
_keyDerivationKey = new Secret(keyDerivationKey);
49+
_derivedkeySizeInBytes = derivedKeySizeInBytes;
50+
51+
switch (_derivedkeySizeInBytes)
52+
{
53+
case 16:
54+
_contextHeader = AES_128_GCM_Header;
55+
break;
56+
case 24:
57+
_contextHeader = AES_192_GCM_Header;
58+
break;
59+
case 32:
60+
_contextHeader = AES_256_GCM_Header;
61+
break;
62+
default:
63+
throw CryptoUtil.Fail("Unexpected AES key size in bytes only support 16, 24, 32."); // should never happen
64+
}
65+
66+
_genRandom = genRandom ?? ManagedGenRandomImpl.Instance;
67+
}
68+
69+
public byte[] Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData)
70+
{
71+
ciphertext.Validate();
72+
additionalAuthenticatedData.Validate();
73+
74+
// Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag
75+
if (ciphertext.Count < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES)
76+
{
77+
throw Error.CryptCommon_PayloadInvalid();
78+
}
79+
80+
// Assumption: pbCipherText := { keyModifier || nonce || encryptedData || authenticationTag }
81+
var plaintextBytes = ciphertext.Count - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES);
82+
var plaintext = new byte[plaintextBytes];
83+
84+
try
85+
{
86+
// Step 1: Extract the key modifier from the payload.
87+
88+
int keyModifierOffset; // position in ciphertext.Array where key modifier begins
89+
int nonceOffset; // position in ciphertext.Array where key modifier ends / nonce begins
90+
int encryptedDataOffset; // position in ciphertext.Array where nonce ends / encryptedData begins
91+
int tagOffset; // position in ciphertext.Array where encrypted data ends
92+
93+
checked
94+
{
95+
keyModifierOffset = ciphertext.Offset;
96+
nonceOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES;
97+
encryptedDataOffset = nonceOffset + NONCE_SIZE_IN_BYTES;
98+
tagOffset = encryptedDataOffset + plaintextBytes;
99+
}
100+
101+
var keyModifier = new ArraySegment<byte>(ciphertext.Array!, keyModifierOffset, KEY_MODIFIER_SIZE_IN_BYTES);
102+
103+
// Step 2: Decrypt the KDK and use it to restore the original encryption and MAC keys.
104+
// We pin all unencrypted keys to limit their exposure via GC relocation.
105+
106+
var decryptedKdk = new byte[_keyDerivationKey.Length];
107+
var derivedKey = new byte[_derivedkeySizeInBytes];
108+
109+
fixed (byte* __unused__1 = decryptedKdk)
110+
fixed (byte* __unused__2 = derivedKey)
111+
{
112+
try
113+
{
114+
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
115+
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
116+
kdk: decryptedKdk,
117+
label: additionalAuthenticatedData,
118+
contextHeader: _contextHeader,
119+
context: keyModifier,
120+
prfFactory: _kdkPrfFactory,
121+
output: new ArraySegment<byte>(derivedKey));
122+
123+
// Perform the decryption operation
124+
var nonce = new Span<byte>(ciphertext.Array, nonceOffset, NONCE_SIZE_IN_BYTES);
125+
var tag = new Span<byte>(ciphertext.Array, tagOffset, TAG_SIZE_IN_BYTES);
126+
var encrypted = new Span<byte>(ciphertext.Array, encryptedDataOffset, plaintextBytes);
127+
using var aes = new AesGcm(derivedKey);
128+
aes.Decrypt(nonce, encrypted, tag, plaintext);
129+
return plaintext;
130+
}
131+
finally
132+
{
133+
// delete since these contain secret material
134+
Array.Clear(decryptedKdk, 0, decryptedKdk.Length);
135+
Array.Clear(derivedKey, 0, derivedKey.Length);
136+
}
137+
}
138+
}
139+
catch (Exception ex) when (ex.RequiresHomogenization())
140+
{
141+
// Homogenize all exceptions to CryptographicException.
142+
throw Error.CryptCommon_GenericError(ex);
143+
}
144+
}
145+
146+
public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData, uint preBufferSize, uint postBufferSize)
147+
{
148+
plaintext.Validate();
149+
additionalAuthenticatedData.Validate();
150+
151+
try
152+
{
153+
// Allocate a buffer to hold the key modifier, nonce, encrypted data, and tag.
154+
// In GCM, the encrypted output will be the same length as the plaintext input.
155+
var retVal = new byte[checked(preBufferSize + KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Count + TAG_SIZE_IN_BYTES + postBufferSize)];
156+
int keyModifierOffset; // position in ciphertext.Array where key modifier begins
157+
int nonceOffset; // position in ciphertext.Array where key modifier ends / nonce begins
158+
int encryptedDataOffset; // position in ciphertext.Array where nonce ends / encryptedData begins
159+
int tagOffset; // position in ciphertext.Array where encrypted data ends
160+
161+
checked
162+
{
163+
keyModifierOffset = plaintext.Offset + (int)preBufferSize;
164+
nonceOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES;
165+
encryptedDataOffset = nonceOffset + NONCE_SIZE_IN_BYTES;
166+
tagOffset = encryptedDataOffset + plaintext.Count;
167+
}
168+
169+
// Randomly generate the key modifier and nonce
170+
var keyModifier = _genRandom.GenRandom(KEY_MODIFIER_SIZE_IN_BYTES);
171+
var nonceBytes = _genRandom.GenRandom(NONCE_SIZE_IN_BYTES);
172+
173+
Buffer.BlockCopy(keyModifier, 0, retVal, (int)preBufferSize, keyModifier.Length);
174+
Buffer.BlockCopy(nonceBytes, 0, retVal, (int)preBufferSize + keyModifier.Length, nonceBytes.Length);
175+
176+
// At this point, retVal := { preBuffer | keyModifier | nonce | _____ | _____ | postBuffer }
177+
178+
// Use the KDF to generate a new symmetric block cipher key
179+
// We'll need a temporary buffer to hold the symmetric encryption subkey
180+
var decryptedKdk = new byte[_keyDerivationKey.Length];
181+
var derivedKey = new byte[_derivedkeySizeInBytes];
182+
fixed (byte* __unused__1 = decryptedKdk)
183+
fixed (byte* __unused__2 = derivedKey)
184+
{
185+
try
186+
{
187+
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
188+
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
189+
kdk: decryptedKdk,
190+
label: additionalAuthenticatedData,
191+
contextHeader: _contextHeader,
192+
context: keyModifier,
193+
prfFactory: _kdkPrfFactory,
194+
output: new ArraySegment<byte>(derivedKey));
195+
196+
// do gcm
197+
var nonce = new Span<byte>(retVal, nonceOffset, NONCE_SIZE_IN_BYTES);
198+
var tag = new Span<byte>(retVal, tagOffset, TAG_SIZE_IN_BYTES);
199+
var encrypted = new Span<byte>(retVal, encryptedDataOffset, plaintext.Count);
200+
using var aes = new AesGcm(derivedKey);
201+
aes.Encrypt(nonce, plaintext, encrypted, tag);
202+
203+
// At this point, retVal := { preBuffer | keyModifier | nonce | encryptedData | authenticationTag | postBuffer }
204+
// And we're done!
205+
return retVal;
206+
}
207+
finally
208+
{
209+
// delete since these contain secret material
210+
Array.Clear(decryptedKdk, 0, decryptedKdk.Length);
211+
Array.Clear(derivedKey, 0, derivedKey.Length);
212+
}
213+
}
214+
}
215+
catch (Exception ex) when (ex.RequiresHomogenization())
216+
{
217+
// Homogenize all exceptions to CryptographicException.
218+
throw Error.CryptCommon_GenericError(ex);
219+
}
220+
}
221+
222+
public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData)
223+
=> Encrypt(plaintext, additionalAuthenticatedData, 0, 0);
224+
225+
public void Dispose()
226+
{
227+
_keyDerivationKey.Dispose();
228+
}
229+
}
230+
}
231+
#endif

src/DataProtection/DataProtection/test/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactoryTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void CreateEncrptorInstance_ExpectedDescriptorType_ReturnsEncryptor()
4646

4747
// Assert
4848
Assert.NotNull(encryptor);
49-
Assert.IsType<GcmAuthenticatedEncryptor>(encryptor);
49+
Assert.IsType<CngGcmAuthenticatedEncryptor>(encryptor);
5050
}
5151
}
5252
}

0 commit comments

Comments
 (0)