diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index 01108384735d..a9dae44b5f1e 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -82,6 +82,7 @@ and are generated based on the last package release.
+
diff --git a/eng/Versions.props b/eng/Versions.props
index 5658e7f588ef..16f6ad422017 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -218,6 +218,7 @@
0.3.0-alpha.19317.1
4.3.0
4.3.4
+ 4.6.0
4.3.0
4.3.2
4.3.0
diff --git a/src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs b/src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs
index 0c4526a6419b..dbabd1b9359e 100644
--- a/src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs
+++ b/src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs
@@ -92,24 +92,18 @@ public static bool TimeConstantBuffersAreEqual(byte* bufA, byte* bufB, uint coun
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
- public static bool TimeConstantBuffersAreEqual(byte[] bufA, int offsetA, int countA, byte[] bufB, int offsetB, int countB)
+ public static bool TimeConstantBuffersAreEqual(ReadOnlySpan bufA, ReadOnlySpan bufB)
{
- // Technically this is an early exit scenario, but it means that the caller did something bizarre.
- // An error at the call site isn't usable for timing attacks.
- Assert(countA == countB, "countA == countB");
+ // This early exit handles unexpected input without introducing timing vulnerabilities.
+ Assert(bufA.Length == bufB.Length, "countA == countB");
#if NETCOREAPP
- unsafe
- {
- return CryptographicOperations.FixedTimeEquals(
- bufA.AsSpan(start: offsetA, length: countA),
- bufB.AsSpan(start: offsetB, length: countB));
- }
+ return CryptographicOperations.FixedTimeEquals(bufA, bufB);
#else
bool areEqual = true;
- for (int i = 0; i < countA; i++)
+ for (int i = 0; i < bufA.Length; i++)
{
- areEqual &= (bufA[offsetA + i] == bufB[offsetB + i]);
+ areEqual &= (bufA[i] == bufB[i]);
}
return areEqual;
#endif
diff --git a/src/DataProtection/Cryptography.Internal/src/Microsoft.AspNetCore.Cryptography.Internal.csproj b/src/DataProtection/Cryptography.Internal/src/Microsoft.AspNetCore.Cryptography.Internal.csproj
index e90b466580d2..aa5d5730a02e 100644
--- a/src/DataProtection/Cryptography.Internal/src/Microsoft.AspNetCore.Cryptography.Internal.csproj
+++ b/src/DataProtection/Cryptography.Internal/src/Microsoft.AspNetCore.Cryptography.Internal.csproj
@@ -19,4 +19,8 @@
+
+
+
+
diff --git a/src/DataProtection/Cryptography.Internal/test/CryptoUtilTests.cs b/src/DataProtection/Cryptography.Internal/test/CryptoUtilTests.cs
index 612a2300b1ed..6e57b9051652 100644
--- a/src/DataProtection/Cryptography.Internal/test/CryptoUtilTests.cs
+++ b/src/DataProtection/Cryptography.Internal/test/CryptoUtilTests.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
using Xunit;
namespace Microsoft.AspNetCore.Cryptography;
@@ -15,7 +16,7 @@ public void TimeConstantBuffersAreEqual_Array_Equal()
byte[] b = new byte[] { 0xAB, 0xCD, 0x23, 0x45, 0x67, 0xEF };
// Act & assert
- Assert.True(CryptoUtil.TimeConstantBuffersAreEqual(a, 1, 3, b, 2, 3));
+ Assert.True(CryptoUtil.TimeConstantBuffersAreEqual(a.AsSpan(1, 3), b.AsSpan(2, 3)));
}
[Fact]
@@ -25,7 +26,7 @@ public void TimeConstantBuffersAreEqual_Array_Unequal()
byte[] b = new byte[] { 0xAB, 0xCD, 0x23, 0xFF, 0x67, 0xEF };
// Act & assert
- Assert.False(CryptoUtil.TimeConstantBuffersAreEqual(a, 1, 3, b, 2, 3));
+ Assert.False(CryptoUtil.TimeConstantBuffersAreEqual(a.AsSpan(1, 3), b.AsSpan(2, 3)));
}
[Fact]
diff --git a/src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs b/src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs
index 7fbc7ce37e1b..413adfb4825f 100644
--- a/src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs
+++ b/src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs
@@ -35,8 +35,6 @@ internal sealed unsafe class AesGcmAuthenticatedEncryptor : IOptimizedAuthentica
// 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
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 };
- private static readonly Func _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512
-
private readonly byte[] _contextHeader;
private readonly Secret _keyDerivationKey;
@@ -112,13 +110,13 @@ public byte[] Decrypt(ArraySegment ciphertext, ArraySegment addition
try
{
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment(decryptedKdk));
- ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
- context: keyModifier,
- prfFactory: _kdkPrfFactory,
- output: new ArraySegment(derivedKey));
+ contextData: keyModifier,
+ operationSubkey: derivedKey,
+ validationSubkey: Span.Empty /* filling in derivedKey only */ );
// Perform the decryption operation
var nonce = new Span(ciphertext.Array, nonceOffset, NONCE_SIZE_IN_BYTES);
@@ -185,13 +183,13 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona
try
{
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment(decryptedKdk));
- ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
- context: keyModifier,
- prfFactory: _kdkPrfFactory,
- output: new ArraySegment(derivedKey));
+ contextData: keyModifier,
+ operationSubkey: derivedKey,
+ validationSubkey: Span.Empty /* filling in derivedKey only */ );
// do gcm
var nonce = new Span(retVal, nonceOffset, NONCE_SIZE_IN_BYTES);
diff --git a/src/DataProtection/DataProtection/src/Managed/IManagedGenRandom.cs b/src/DataProtection/DataProtection/src/Managed/IManagedGenRandom.cs
index 2bb04c686fda..c865ecb2edf7 100644
--- a/src/DataProtection/DataProtection/src/Managed/IManagedGenRandom.cs
+++ b/src/DataProtection/DataProtection/src/Managed/IManagedGenRandom.cs
@@ -1,9 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
+
namespace Microsoft.AspNetCore.DataProtection.Managed;
internal interface IManagedGenRandom
{
byte[] GenRandom(int numBytes);
+
+#if NET10_0_OR_GREATER
+ void GenRandom(Span target);
+#endif
}
diff --git a/src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs b/src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
index 775e0070d6c7..e11e3862b53e 100644
--- a/src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
+++ b/src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
@@ -2,10 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Buffers;
+using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.Internal;
using Microsoft.AspNetCore.DataProtection.SP800_108;
namespace Microsoft.AspNetCore.DataProtection.Managed;
@@ -26,8 +30,6 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
// probability of collision, and this is acceptable for the expected KDK lifetime.
private const int KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
- private static readonly Func _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512
-
private readonly byte[] _contextHeader;
private readonly IManagedGenRandom _genRandom;
private readonly Secret _keyDerivationKey;
@@ -101,9 +103,10 @@ private byte[] CreateContextHeader()
ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
kdk: EMPTY_ARRAY,
label: EMPTY_ARRAY_SEGMENT,
- context: EMPTY_ARRAY_SEGMENT,
- prfFactory: _kdkPrfFactory,
- output: new ArraySegment(tempKeys));
+ contextHeader: EMPTY_ARRAY_SEGMENT,
+ contextData: EMPTY_ARRAY_SEGMENT,
+ operationSubkey: tempKeys.AsSpan(0, _symmetricAlgorithmSubkeyLengthInBytes),
+ validationSubkey: tempKeys.AsSpan(_symmetricAlgorithmSubkeyLengthInBytes, _validationAlgorithmSubkeyLengthInBytes));
// At this point, tempKeys := { K_E || K_H }.
@@ -144,20 +147,25 @@ private SymmetricAlgorithm CreateSymmetricAlgorithm()
retVal.Mode = CipherMode.CBC;
retVal.Padding = PaddingMode.PKCS7;
+
return retVal;
}
- private KeyedHashAlgorithm CreateValidationAlgorithm(byte[] key)
+ private KeyedHashAlgorithm CreateValidationAlgorithm(byte[]? key = null)
{
var retVal = _validationAlgorithmFactory();
CryptoUtil.Assert(retVal != null, "retVal != null");
- retVal.Key = key;
+ if (key is not null)
+ {
+ retVal.Key = key;
+ }
return retVal;
}
public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment additionalAuthenticatedData)
{
+ // Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
protectedPayload.Validate();
additionalAuthenticatedData.Validate();
@@ -167,12 +175,9 @@ public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment ad
throw Error.CryptCommon_PayloadInvalid();
}
- // Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
-
try
{
// Step 1: Extract the key modifier and IV from the payload.
-
int keyModifierOffset; // position in protectedPayload.Array where key modifier begins
int ivOffset; // position in protectedPayload.Array where key modifier ends / IV begins
int ciphertextOffset; // position in protectedPayload.Array where IV ends / ciphertext begins
@@ -186,60 +191,72 @@ public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment ad
ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes;
}
- ArraySegment keyModifier = new ArraySegment(protectedPayload.Array!, keyModifierOffset, ivOffset - keyModifierOffset);
- var iv = new byte[_symmetricAlgorithmBlockSizeInBytes];
- Buffer.BlockCopy(protectedPayload.Array!, ivOffset, iv, 0, iv.Length);
+ ReadOnlySpan keyModifier = protectedPayload.Array!.AsSpan(keyModifierOffset, ivOffset - keyModifierOffset);
// Step 2: Decrypt the KDK and use it to restore the original encryption and MAC keys.
- // We pin all unencrypted keys to limit their exposure via GC relocation.
-
+#if NET10_0_OR_GREATER
+ Span decryptedKdk = _keyDerivationKey.Length <= 256
+ ? stackalloc byte[256].Slice(0, _keyDerivationKey.Length)
+ : new byte[_keyDerivationKey.Length];
+#else
var decryptedKdk = new byte[_keyDerivationKey.Length];
- var decryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
- var validationSubkey = new byte[_validationAlgorithmSubkeyLengthInBytes];
- var derivedKeysBuffer = new byte[checked(decryptionSubkey.Length + validationSubkey.Length)];
-
- fixed (byte* __unused__1 = decryptedKdk)
+#endif
+
+ byte[]? validationSubkeyArray = null;
+ var validationSubkey = _validationAlgorithmSubkeyLengthInBytes <= 128
+ ? stackalloc byte[128].Slice(0, _validationAlgorithmSubkeyLengthInBytes)
+ : (validationSubkeyArray = new byte[_validationAlgorithmSubkeyLengthInBytes]);
+
+#if NET10_0_OR_GREATER
+ Span decryptionSubkey =
+ _symmetricAlgorithmSubkeyLengthInBytes <= 128
+ ? stackalloc byte[128].Slice(0, _symmetricAlgorithmSubkeyLengthInBytes)
+ : new byte[_symmetricAlgorithmBlockSizeInBytes];
+#else
+ byte[] decryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
+#endif
+
+ // calling "fixed" is basically pinning the array, meaning the GC won't move it around. (Also for safety concerns)
+ // note: it is safe to call `fixed` on null - it is just a no-op
+ fixed (byte* decryptedKdkUnsafe = decryptedKdk)
fixed (byte* __unused__2 = decryptionSubkey)
- fixed (byte* __unused__3 = validationSubkey)
- fixed (byte* __unused__4 = derivedKeysBuffer)
+ fixed (byte* __unused__3 = validationSubkeyArray)
{
try
{
- _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment(decryptedKdk));
- ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
+ _keyDerivationKey.WriteSecretIntoBuffer(decryptedKdkUnsafe, decryptedKdk.Length);
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
- context: keyModifier,
- prfFactory: _kdkPrfFactory,
- output: new ArraySegment(derivedKeysBuffer));
-
- Buffer.BlockCopy(derivedKeysBuffer, 0, decryptionSubkey, 0, decryptionSubkey.Length);
- Buffer.BlockCopy(derivedKeysBuffer, decryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length);
+ contextData: keyModifier,
+ operationSubkey: decryptionSubkey,
+ validationSubkey: validationSubkey);
// Step 3: Calculate the correct MAC for this payload.
// correctHash := MAC(IV || ciphertext)
- byte[] correctHash;
-
- using (var hashAlgorithm = CreateValidationAlgorithm(validationSubkey))
+ checked
{
- checked
- {
- eofOffset = protectedPayload.Offset + protectedPayload.Count;
- macOffset = eofOffset - _validationAlgorithmDigestLengthInBytes;
- }
-
- correctHash = hashAlgorithm.ComputeHash(protectedPayload.Array!, ivOffset, macOffset - ivOffset);
+ eofOffset = protectedPayload.Offset + protectedPayload.Count;
+ macOffset = eofOffset - _validationAlgorithmDigestLengthInBytes;
}
// Step 4: Validate the MAC provided as part of the payload.
-
- if (!CryptoUtil.TimeConstantBuffersAreEqual(correctHash, 0, correctHash.Length, protectedPayload.Array!, macOffset, eofOffset - macOffset))
- {
- throw Error.CryptCommon_PayloadInvalid(); // integrity check failure
- }
+ CalculateAndValidateMac(protectedPayload.Array!, ivOffset, macOffset, eofOffset, validationSubkey, validationSubkeyArray);
// Step 5: Decipher the ciphertext and return it to the caller.
+#if NET10_0_OR_GREATER
+ using var symmetricAlgorithm = CreateSymmetricAlgorithm();
+ symmetricAlgorithm.SetKey(decryptionSubkey); // setKey is a single-shot usage of symmetricAlgorithm. Not allocatey
+
+ // note: here protectedPayload.Array is taken without an offset (can't use AsSpan() on ArraySegment)
+ var ciphertext = protectedPayload.Array.AsSpan(ciphertextOffset, macOffset - ciphertextOffset);
+ var iv = protectedPayload.Array.AsSpan(ivOffset, _symmetricAlgorithmBlockSizeInBytes);
+
+ // symmetricAlgorithm is created with CBC mode (see CreateSymmetricAlgorithm())
+ return symmetricAlgorithm.DecryptCbc(ciphertext, iv);
+#else
+ var iv = protectedPayload.Array!.AsSpan(ivOffset, _symmetricAlgorithmBlockSizeInBytes).ToArray();
using (var symmetricAlgorithm = CreateSymmetricAlgorithm())
using (var cryptoTransform = symmetricAlgorithm.CreateDecryptor(decryptionSubkey, iv))
@@ -254,14 +271,19 @@ public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment ad
return outputStream.ToArray();
}
}
+#endif
}
finally
{
// delete since these contain secret material
+ validationSubkey.Clear();
+
+#if NET10_0_OR_GREATER
+ decryptedKdk.Clear();
+ decryptionSubkey.Clear();
+#else
Array.Clear(decryptedKdk, 0, decryptedKdk.Length);
- Array.Clear(decryptionSubkey, 0, decryptionSubkey.Length);
- Array.Clear(validationSubkey, 0, validationSubkey.Length);
- Array.Clear(derivedKeysBuffer, 0, derivedKeysBuffer.Length);
+#endif
}
}
}
@@ -272,62 +294,144 @@ public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment ad
}
}
- public void Dispose()
- {
- _keyDerivationKey.Dispose();
- }
-
public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData)
{
plaintext.Validate();
additionalAuthenticatedData.Validate();
+ var plainTextSpan = plaintext.AsSpan();
try
{
- var outputStream = new MemoryStream();
-
- // Step 1: Generate a random key modifier and IV for this operation.
- // Both will be equal to the block size of the block cipher algorithm.
-
- var keyModifier = _genRandom.GenRandom(KEY_MODIFIER_SIZE_IN_BYTES);
- var iv = _genRandom.GenRandom(_symmetricAlgorithmBlockSizeInBytes);
-
- // Step 2: Copy the key modifier and the IV to the output stream since they'll act as a header.
-
- outputStream.Write(keyModifier, 0, keyModifier.Length);
- outputStream.Write(iv, 0, iv.Length);
-
- // At this point, outputStream := { keyModifier || IV }.
-
- // Step 3: Decrypt the KDK, and use it to generate new encryption and HMAC keys.
- // We pin all unencrypted keys to limit their exposure via GC relocation.
-
+ var keyModifierLength = KEY_MODIFIER_SIZE_IN_BYTES;
+ var ivLength = _symmetricAlgorithmBlockSizeInBytes;
+
+#if NET10_0_OR_GREATER
+ Span decryptedKdk = _keyDerivationKey.Length <= 256
+ ? stackalloc byte[256].Slice(0, _keyDerivationKey.Length)
+ : new byte[_keyDerivationKey.Length];
+#else
var decryptedKdk = new byte[_keyDerivationKey.Length];
- var encryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
- var validationSubkey = new byte[_validationAlgorithmSubkeyLengthInBytes];
- var derivedKeysBuffer = new byte[checked(encryptionSubkey.Length + validationSubkey.Length)];
-
- fixed (byte* __unused__1 = decryptedKdk)
- fixed (byte* __unused__2 = encryptionSubkey)
- fixed (byte* __unused__3 = validationSubkey)
- fixed (byte* __unused__4 = derivedKeysBuffer)
+#endif
+
+#if NET10_0_OR_GREATER
+ byte[]? validationSubkeyArray = null;
+ Span validationSubkey = _validationAlgorithmSubkeyLengthInBytes <= 128
+ ? stackalloc byte[128].Slice(0, _validationAlgorithmSubkeyLengthInBytes)
+ : (validationSubkeyArray = new byte[_validationAlgorithmSubkeyLengthInBytes]);
+#else
+ var validationSubkeyArray = new byte[_validationAlgorithmSubkeyLengthInBytes];
+ var validationSubkey = validationSubkeyArray.AsSpan();
+#endif
+
+#if NET10_0_OR_GREATER
+ Span encryptionSubkey = _symmetricAlgorithmSubkeyLengthInBytes <= 128
+ ? stackalloc byte[128].Slice(0, _symmetricAlgorithmSubkeyLengthInBytes)
+ : new byte[_symmetricAlgorithmSubkeyLengthInBytes];
+#else
+ byte[] encryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
+#endif
+
+ fixed (byte* decryptedKdkUnsafe = decryptedKdk)
+ fixed (byte* __unused__1 = encryptionSubkey)
+ fixed (byte* __unused__2 = validationSubkeyArray)
{
+ // Step 1: Generate a random key modifier and IV for this operation.
+ // Both will be equal to the block size of the block cipher algorithm.
+#if NET10_0_OR_GREATER
+ Span keyModifier = keyModifierLength <= 128
+ ? stackalloc byte[128].Slice(0, keyModifierLength)
+ : new byte[keyModifierLength];
+
+ _genRandom.GenRandom(keyModifier);
+#else
+ var keyModifier = _genRandom.GenRandom(keyModifierLength);
+#endif
+
try
{
- _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment(decryptedKdk));
- ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
+ // Step 2: Decrypt the KDK, and use it to generate new encryption and HMAC keys.
+ _keyDerivationKey.WriteSecretIntoBuffer(decryptedKdkUnsafe, decryptedKdk.Length);
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
- context: new ArraySegment(keyModifier),
- prfFactory: _kdkPrfFactory,
- output: new ArraySegment(derivedKeysBuffer));
+ contextData: keyModifier,
+ operationSubkey: encryptionSubkey,
+ validationSubkey: validationSubkey);
+
+#if NET10_0_OR_GREATER
+ // idea of optimization here is firstly get all the types preset
+ // for calculating length of the output array and allocating it.
+ // then we are filling it with the data directly, without any additional copying
+
+ using var symmetricAlgorithm = CreateSymmetricAlgorithm();
+ symmetricAlgorithm.SetKey(encryptionSubkey); // setKey is a single-shot usage of symmetricAlgorithm. Not allocatey
+
+ using var validationAlgorithm = CreateValidationAlgorithm();
+
+ // Later framework has an API to pre-calculate optimal length of the ciphertext.
+ // That means we can avoid allocating more data than we need.
+
+ var cipherTextLength = symmetricAlgorithm.GetCiphertextLengthCbc(plainTextSpan.Length); // CBC because symmetricAlgorithm is created with CBC mode
+ var macLength = _validationAlgorithmDigestLengthInBytes;
+
+ // allocating an array of a specific required length
+ var outputArray = new byte[keyModifierLength + ivLength + cipherTextLength + macLength];
+ var outputSpan = outputArray.AsSpan();
+#else
+ var outputStream = new MemoryStream();
+#endif
+
+#if NET10_0_OR_GREATER
+ // Step 2: Copy the key modifier to the output stream (part of a header)
+ keyModifier.CopyTo(outputSpan.Slice(start: 0, length: keyModifierLength));
+
+ // Step 3: Generate IV for this operation right into the output stream (no allocation)
+ // key modifier and IV together act as a header.
+ var iv = outputSpan.Slice(start: keyModifierLength, length: ivLength);
+ _genRandom.GenRandom(iv);
+#else
+ // Step 2: Copy the key modifier and the IV to the output stream since they'll act as a header.
+ outputStream.Write(keyModifier, 0, keyModifier.Length);
+
+ // Step 3: Generate IV for this operation right into the result array
+ var iv = _genRandom.GenRandom(_symmetricAlgorithmBlockSizeInBytes);
+ outputStream.Write(iv, 0, iv.Length);
+#endif
+
+#if NET10_0_OR_GREATER
+ // Step 4: Perform the encryption operation.
+ // encrypting plaintext into the target array directly
+ symmetricAlgorithm.EncryptCbc(plainTextSpan, iv, outputSpan.Slice(start: keyModifierLength + ivLength, length: cipherTextLength));
- Buffer.BlockCopy(derivedKeysBuffer, 0, encryptionSubkey, 0, encryptionSubkey.Length);
- Buffer.BlockCopy(derivedKeysBuffer, encryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length);
+ // At this point, outputStream := { keyModifier || IV || ciphertext }
- // Step 4: Perform the encryption operation.
+ // Step 5: Calculate the digest over the IV and ciphertext.
+ // We don't need to calculate the digest over the key modifier since that
+ // value has already been mixed into the KDF used to generate the MAC key.
+
+ var ivAndCipherTextSpan = outputSpan.Slice(start: keyModifierLength, length: ivLength + cipherTextLength);
+ var macDestinationSpan = outputSpan.Slice(keyModifierLength + ivLength + cipherTextLength, macLength);
+ // if we can use an optimized method for specific algorithm - we use it (no extra alloc for subKey)
+ if (validationAlgorithm is HMACSHA256)
+ {
+ HMACSHA256.HashData(key: validationSubkey, source: ivAndCipherTextSpan, destination: macDestinationSpan);
+ }
+ else if (validationAlgorithm is HMACSHA512)
+ {
+ HMACSHA512.HashData(key: validationSubkey, source: ivAndCipherTextSpan, destination: macDestinationSpan);
+ }
+ else
+ {
+ validationAlgorithm.Key = validationSubkeyArray ?? validationSubkey.ToArray();
+ validationAlgorithm.TryComputeHash(source: ivAndCipherTextSpan, destination: macDestinationSpan, bytesWritten: out _);
+ }
+
+ // At this point, outputArray := { keyModifier || IV || ciphertext || MAC(IV || ciphertext) }
+ return outputArray;
+#else
+ // Step 4: Perform the encryption operation.
using (var symmetricAlgorithm = CreateSymmetricAlgorithm())
using (var cryptoTransform = symmetricAlgorithm.CreateEncryptor(encryptionSubkey, iv))
using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write))
@@ -340,8 +444,7 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona
// Step 5: Calculate the digest over the IV and ciphertext.
// We don't need to calculate the digest over the key modifier since that
// value has already been mixed into the KDF used to generate the MAC key.
-
- using (var validationAlgorithm = CreateValidationAlgorithm(validationSubkey))
+ using (var validationAlgorithm = CreateValidationAlgorithm(validationSubkeyArray))
{
// As an optimization, avoid duplicating the underlying buffer
var underlyingBuffer = outputStream.GetBuffer();
@@ -354,14 +457,17 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona
return outputStream.ToArray();
}
}
+#endif
}
finally
{
- // delete since these contain secret material
+#if NET10_0_OR_GREATER
+ keyModifier.Clear();
+ decryptedKdk.Clear();
+#else
+ Array.Clear(keyModifier, 0, keyModifierLength);
Array.Clear(decryptedKdk, 0, decryptedKdk.Length);
- Array.Clear(encryptionSubkey, 0, encryptionSubkey.Length);
- Array.Clear(validationSubkey, 0, validationSubkey.Length);
- Array.Clear(derivedKeysBuffer, 0, derivedKeysBuffer.Length);
+#endif
}
}
}
@@ -371,4 +477,65 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona
throw Error.CryptCommon_GenericError(ex);
}
}
+
+ private void CalculateAndValidateMac(
+ byte[] payloadArray,
+ int ivOffset, int macOffset, int eofOffset, // offsets to slice the payload array
+ ReadOnlySpan validationSubkey,
+ byte[]? validationSubkeyArray)
+ {
+ using var validationAlgorithm = CreateValidationAlgorithm();
+ var hashSize = validationAlgorithm.GetDigestSizeInBytes();
+
+ byte[]? correctHashArray = null;
+ Span correctHash = hashSize <= 128
+ ? stackalloc byte[128].Slice(0, hashSize)
+ : (correctHashArray = new byte[hashSize]);
+
+ try
+ {
+#if NET10_0_OR_GREATER
+ var hashSource = payloadArray!.AsSpan(ivOffset, macOffset - ivOffset);
+
+ int bytesWritten;
+ if (validationAlgorithm is HMACSHA256)
+ {
+ bytesWritten = HMACSHA256.HashData(key: validationSubkey, source: hashSource, destination: correctHash);
+ }
+ else if (validationAlgorithm is HMACSHA512)
+ {
+ bytesWritten = HMACSHA512.HashData(key: validationSubkey, source: hashSource, destination: correctHash);
+ }
+ else
+ {
+ // if validationSubkey is stackalloc'ed, there is no way we avoid an alloc here
+ validationAlgorithm.Key = validationSubkeyArray ?? validationSubkey.ToArray();
+ var success = validationAlgorithm.TryComputeHash(hashSource, correctHash, out bytesWritten);
+ Debug.Assert(success);
+ }
+
+ Debug.Assert(bytesWritten == hashSize);
+#else
+ // if validationSubkey is stackalloc'ed, there is no way we avoid an alloc here
+ validationAlgorithm.Key = validationSubkeyArray ?? validationSubkey.ToArray();
+ correctHashArray = validationAlgorithm.ComputeHash(payloadArray, macOffset, eofOffset - macOffset);
+#endif
+
+ // Step 4: Validate the MAC provided as part of the payload.
+ var payloadMacSpan = payloadArray!.AsSpan(macOffset, eofOffset - macOffset);
+ if (!CryptoUtil.TimeConstantBuffersAreEqual(correctHash, payloadMacSpan))
+ {
+ throw Error.CryptCommon_PayloadInvalid(); // integrity check failure
+ }
+ }
+ finally
+ {
+ correctHash.Clear();
+ }
+ }
+
+ public void Dispose()
+ {
+ _keyDerivationKey.Dispose();
+ }
}
diff --git a/src/DataProtection/DataProtection/src/Managed/ManagedGenRandomImpl.cs b/src/DataProtection/DataProtection/src/Managed/ManagedGenRandomImpl.cs
index 07dd20c4a9ff..04136930074b 100644
--- a/src/DataProtection/DataProtection/src/Managed/ManagedGenRandomImpl.cs
+++ b/src/DataProtection/DataProtection/src/Managed/ManagedGenRandomImpl.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
using System.Security.Cryptography;
namespace Microsoft.AspNetCore.DataProtection.Managed;
@@ -16,6 +17,10 @@ private ManagedGenRandomImpl()
{
}
+#if NET10_0_OR_GREATER
+ public void GenRandom(Span target) => RandomNumberGenerator.Fill(target);
+#endif
+
public byte[] GenRandom(int numBytes)
{
var bytes = new byte[numBytes];
diff --git a/src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs b/src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
index 3b343684786e..eccb36dddf11 100644
--- a/src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
+++ b/src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
@@ -2,25 +2,84 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Buffers;
+using System.Diagnostics;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.Internal;
using Microsoft.AspNetCore.DataProtection.Managed;
namespace Microsoft.AspNetCore.DataProtection.SP800_108;
internal static class ManagedSP800_108_CTR_HMACSHA512
{
- public static void DeriveKeys(byte[] kdk, ArraySegment label, ArraySegment context, Func prfFactory, ArraySegment output)
+#if !NET10_0_OR_GREATER
+ public static void DeriveKeys(
+ byte[] kdk,
+ ReadOnlySpan label,
+ ReadOnlySpan contextHeader,
+ ReadOnlySpan contextData,
+ Span operationSubkey,
+ Span validationSubkey)
{
- // make copies so we can mutate these local vars
- var outputOffset = output.Offset;
- var outputCount = output.Count;
+ // netFX and netStandard dont have API to NOT use HashAlgorithm
+ using HashAlgorithm prf = new HMACSHA512(kdk);
- using (var prf = prfFactory(kdk))
- {
- // See SP800-108, Sec. 5.1 for the format of the input to the PRF routine.
- var prfInput = new byte[checked(sizeof(uint) /* [i]_2 */ + label.Count + 1 /* 0x00 */ + context.Count + sizeof(uint) /* [K]_2 */)];
+ // kdk is passed just to have a shared implementation for different framework versions
+ DeriveKeys(kdk, label, contextHeader, contextData, operationSubkey, validationSubkey, prf);
+ }
+#endif
+
+#if NET10_0_OR_GREATER
+ public static void DeriveKeys(ReadOnlySpan kdk, ReadOnlySpan label, ReadOnlySpan contextHeader, ReadOnlySpan contextData, Span operationSubkey, Span validationSubkey)
+ => DeriveKeys(kdk, label, contextHeader, contextData, operationSubkey, validationSubkey, prf: null);
+#endif
+
+ ///
+ /// note: kdk will be used only if prf is null and only in later framework versions (10+)
+ /// where static method on `HMACSHA512` exists which avoids allocations
+ ///
+ private static void DeriveKeys(
+ ReadOnlySpan kdk,
+ ReadOnlySpan label,
+ ReadOnlySpan contextHeader,
+ ReadOnlySpan contextData,
+ Span operationSubkey,
+ Span validationSubkey,
+ HashAlgorithm? prf = null)
+ {
+ var operationSubKeyIndex = 0;
+ var validationSubKeyIndex = 0;
+ var outputCount = operationSubkey.Length + validationSubkey.Length;
+
+ int prfOutputSizeInBytes =
+#if NET10_0_OR_GREATER
+ HMACSHA512.HashSizeInBytes;
+#else
+ prf.GetDigestSizeInBytes();
+#endif
+
+#if NET10_0_OR_GREATER
+ Span prfOutput = prfOutputSizeInBytes <= 128
+ ? stackalloc byte[128].Slice(0, prfOutputSizeInBytes)
+ : new byte[prfOutputSizeInBytes];
+#endif
+
+ // See SP800-108, Sec. 5.1 for the format of the input to the PRF routine.
+ var prfInputLength = checked(sizeof(uint) /* [i]_2 */ + label.Length + 1 /* 0x00 */ + (contextHeader.Length + contextData.Length) + sizeof(uint) /* [K]_2 */);
+
+#if NET10_0_OR_GREATER
+ byte[]? prfInputArray = null;
+ Span prfInput = prfInputLength <= 128
+ ? stackalloc byte[128].Slice(0, prfInputLength)
+ : (prfInputArray = new byte[prfInputLength]);
+#else
+ var prfInputArray = new byte[prfInputLength];
+ var prfInput = prfInputArray.AsSpan();
+#endif
+ try
+ {
// Copy [L]_2 to prfInput since it's stable over all iterations
uint outputSizeInBits = (uint)checked((int)outputCount * 8);
prfInput[prfInput.Length - 4] = (byte)(outputSizeInBits >> 24);
@@ -29,10 +88,10 @@ public static void DeriveKeys(byte[] kdk, ArraySegment label, ArraySegment
prfInput[prfInput.Length - 1] = (byte)(outputSizeInBits);
// Copy label and context to prfInput since they're stable over all iterations
- Buffer.BlockCopy(label.Array!, label.Offset, prfInput, sizeof(uint), label.Count);
- Buffer.BlockCopy(context.Array!, context.Offset, prfInput, sizeof(int) + label.Count + 1, context.Count);
+ label.CopyTo(prfInput.Slice(sizeof(uint)));
+ contextHeader.CopyTo(prfInput.Slice(sizeof(uint) + label.Length + 1));
+ contextData.CopyTo(prfInput.Slice(sizeof(uint) + label.Length + 1 + contextHeader.Length));
- var prfOutputSizeInBytes = prf.GetDigestSizeInBytes();
for (uint i = 1; outputCount > 0; i++)
{
// Copy [i]_2 to prfInput since it mutates with each iteration
@@ -41,25 +100,43 @@ public static void DeriveKeys(byte[] kdk, ArraySegment label, ArraySegment
prfInput[2] = (byte)(i >> 8);
prfInput[3] = (byte)(i);
- // Run the PRF and copy the results to the output buffer
- var prfOutput = prf.ComputeHash(prfInput);
+#if NET10_0_OR_GREATER
+ var success = HMACSHA512.TryHashData(kdk, prfInput, prfOutput, out var bytesWritten);
+ Debug.Assert(success);
+ Debug.Assert(bytesWritten == prfOutputSizeInBytes);
+#else
+ var prfOutputArray = prf.ComputeHash(prfInputArray);
+ var prfOutput = prfOutputArray.AsSpan();
+#endif
CryptoUtil.Assert(prfOutputSizeInBytes == prfOutput.Length, "prfOutputSizeInBytes == prfOutput.Length");
var numBytesToCopyThisIteration = Math.Min(prfOutputSizeInBytes, outputCount);
- Buffer.BlockCopy(prfOutput, 0, output.Array!, outputOffset, numBytesToCopyThisIteration);
- Array.Clear(prfOutput, 0, prfOutput.Length); // contains key material, so delete it
- // adjust offsets
- outputOffset += numBytesToCopyThisIteration;
+ // we need to write into the operationSubkey
+ // but it may be the case that we need to split the output
+ // so lets count how many bytes we can write into the operationSubKey
+ var bytesToWrite = Math.Min(numBytesToCopyThisIteration, operationSubkey.Length - operationSubKeyIndex);
+ var leftOverBytes = numBytesToCopyThisIteration - bytesToWrite;
+ if (operationSubKeyIndex < operationSubkey.Length) // meaning we need to write to operationSubKey
+ {
+ var destination = operationSubkey.Slice(operationSubKeyIndex, bytesToWrite);
+ prfOutput.Slice(0, bytesToWrite).CopyTo(destination);
+ operationSubKeyIndex += bytesToWrite;
+ }
+
+ if (operationSubKeyIndex == operationSubkey.Length && leftOverBytes != 0) // we have filled the operationSubKey. It's time for the validationSubKey
+ {
+ var destination = validationSubkey.Slice(validationSubKeyIndex, leftOverBytes);
+ prfOutput.Slice(bytesToWrite, leftOverBytes).CopyTo(destination);
+ validationSubKeyIndex += leftOverBytes;
+ }
+
outputCount -= numBytesToCopyThisIteration;
+ prfOutput.Clear(); // contains key material, so delete it
}
}
- }
-
- public static void DeriveKeysWithContextHeader(byte[] kdk, ArraySegment label, byte[] contextHeader, ArraySegment context, Func prfFactory, ArraySegment output)
- {
- var combinedContext = new byte[checked(contextHeader.Length + context.Count)];
- Buffer.BlockCopy(contextHeader, 0, combinedContext, 0, contextHeader.Length);
- Buffer.BlockCopy(context.Array!, context.Offset, combinedContext, contextHeader.Length, context.Count);
- DeriveKeys(kdk, label, new ArraySegment(combinedContext), prfFactory, output);
+ finally
+ {
+ prfInput.Clear();
+ }
}
}
diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/KeyRingBasedDataProtectorTests.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/KeyRingBasedDataProtectorTests.cs
index d63a5cdd3d12..1a17c3b44215 100644
--- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/KeyRingBasedDataProtectorTests.cs
+++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/KeyRingBasedDataProtectorTests.cs
@@ -8,6 +8,7 @@
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
using Microsoft.AspNetCore.InternalTesting;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SP800_108/SP800_108Tests.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SP800_108/SP800_108Tests.cs
index 697330e853c3..3791d254adfb 100644
--- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SP800_108/SP800_108Tests.cs
+++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SP800_108/SP800_108Tests.cs
@@ -163,8 +163,7 @@ private static void TestManagedKeyDerivation(byte[] kdk, byte[] label, byte[] co
Buffer.BlockCopy(context, 0, contextSegment.Array, contextSegment.Offset, contextSegment.Count);
var derivedSubkeySegment = new ArraySegment(new byte[numDerivedBytes + 10], 4, numDerivedBytes);
- ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(kdk, labelSegment, contextHeader, contextSegment,
- bytes => new HMACSHA512(bytes), derivedSubkeySegment);
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(kdk, labelSegment, contextHeader, contextSegment, derivedSubkeySegment, validationSubkey: Span.Empty /* filling in derivedSubkeySegment only */);
Assert.Equal(expectedDerivedSubkeyAsBase64, Convert.ToBase64String(derivedSubkeySegment.AsStandaloneArray()));
}
}
diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SequentialGenRandom.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SequentialGenRandom.cs
index 02f34da40414..6eba86edb7e6 100644
--- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SequentialGenRandom.cs
+++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SequentialGenRandom.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
using Microsoft.AspNetCore.DataProtection.Cng;
using Microsoft.AspNetCore.DataProtection.Managed;
@@ -27,4 +28,12 @@ public void GenRandom(byte* pbBuffer, uint cbBuffer)
pbBuffer[i] = _value++;
}
}
+
+ public void GenRandom(Span target)
+ {
+ for (var i = 0; i < target.Length; i++)
+ {
+ target[i] = _value++;
+ }
+ }
}