Explorar o código

perf: improve ManagedAuthenticatedEncryptor Decrypt() and Encrypt() flow (#59424)

Korolev Dmitry hai 11 meses
pai
achega
f83c97fa0b

+ 1 - 0
eng/Dependencies.props

@@ -82,6 +82,7 @@ and are generated based on the last package release.
     <LatestPackageReference Include="System.DirectoryServices.Protocols" />
     <LatestPackageReference Include="System.IdentityModel.Tokens.Jwt" />
     <LatestPackageReference Include="System.IO.Pipelines" />
+    <LatestPackageReference Include="System.Memory" />
     <LatestPackageReference Include="System.Net.Http" />
     <LatestPackageReference Include="System.Net.Http.Json" />
     <LatestPackageReference Include="System.Net.Sockets" />

+ 1 - 0
eng/Versions.props

@@ -218,6 +218,7 @@
     <SystemCommandlineExperimentalVersion>0.3.0-alpha.19317.1</SystemCommandlineExperimentalVersion>
     <SystemComponentModelVersion>4.3.0</SystemComponentModelVersion>
     <SystemNetHttpVersion>4.3.4</SystemNetHttpVersion>
+    <SystemMemoryVersion>4.6.0</SystemMemoryVersion>
     <SystemNetSocketsVersion>4.3.0</SystemNetSocketsVersion>
     <SystemSecurityCryptographyX509CertificatesVersion>4.3.2</SystemSecurityCryptographyX509CertificatesVersion>
     <SystemRuntimeInteropServicesRuntimeInformationVersion>4.3.0</SystemRuntimeInteropServicesRuntimeInformationVersion>

+ 6 - 12
src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs

@@ -92,24 +92,18 @@ internal static unsafe class CryptoUtil
     }
 
     [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<byte> bufA, ReadOnlySpan<byte> 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

+ 4 - 0
src/DataProtection/Cryptography.Internal/src/Microsoft.AspNetCore.Cryptography.Internal.csproj

@@ -19,4 +19,8 @@
     <InternalsVisibleTo Include="Microsoft.AspNetCore.DataProtection.Extensions.Tests" />
     <InternalsVisibleTo Include="Microsoft.AspNetCore.DataProtection.Tests" />
   </ItemGroup>
+
+  <ItemGroup Condition="'$(TargetFramework)' != '$(DefaultNetCoreTargetFramework)'">
+    <Reference Include="System.Memory" />
+  </ItemGroup>
 </Project>

+ 3 - 2
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 unsafe class CryptoUtilTests
         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 unsafe class CryptoUtilTests
         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]

+ 8 - 10
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<byte[], HashAlgorithm> _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512
-
     private readonly byte[] _contextHeader;
 
     private readonly Secret _keyDerivationKey;
@@ -112,13 +110,13 @@ internal sealed unsafe class AesGcmAuthenticatedEncryptor : IOptimizedAuthentica
                 try
                 {
                     _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
-                    ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
+                    ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
                         kdk: decryptedKdk,
                         label: additionalAuthenticatedData,
                         contextHeader: _contextHeader,
-                        context: keyModifier,
-                        prfFactory: _kdkPrfFactory,
-                        output: new ArraySegment<byte>(derivedKey));
+                        contextData: keyModifier,
+                        operationSubkey: derivedKey,
+                        validationSubkey: Span<byte>.Empty /* filling in derivedKey only */ );
 
                     // Perform the decryption operation
                     var nonce = new Span<byte>(ciphertext.Array, nonceOffset, NONCE_SIZE_IN_BYTES);
@@ -185,13 +183,13 @@ internal sealed unsafe class AesGcmAuthenticatedEncryptor : IOptimizedAuthentica
                 try
                 {
                     _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
-                    ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
+                    ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
                         kdk: decryptedKdk,
                         label: additionalAuthenticatedData,
                         contextHeader: _contextHeader,
-                        context: keyModifier,
-                        prfFactory: _kdkPrfFactory,
-                        output: new ArraySegment<byte>(derivedKey));
+                        contextData: keyModifier,
+                        operationSubkey: derivedKey,
+                        validationSubkey: Span<byte>.Empty /* filling in derivedKey only */ );
 
                     // do gcm
                     var nonce = new Span<byte>(retVal, nonceOffset, NONCE_SIZE_IN_BYTES);

+ 6 - 0
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<byte> target);
+#endif
 }

+ 260 - 93
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<byte[], HashAlgorithm> _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 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
         ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
             kdk: EMPTY_ARRAY,
             label: EMPTY_ARRAY_SEGMENT,
-            context: EMPTY_ARRAY_SEGMENT,
-            prfFactory: _kdkPrfFactory,
-            output: new ArraySegment<byte>(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 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
 
         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<byte> protectedPayload, ArraySegment<byte> additionalAuthenticatedData)
     {
+        // Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
         protectedPayload.Validate();
         additionalAuthenticatedData.Validate();
 
@@ -167,12 +175,9 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
             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 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
                 ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes;
             }
 
-            ArraySegment<byte> keyModifier = new ArraySegment<byte>(protectedPayload.Array!, keyModifierOffset, ivOffset - keyModifierOffset);
-            var iv = new byte[_symmetricAlgorithmBlockSizeInBytes];
-            Buffer.BlockCopy(protectedPayload.Array!, ivOffset, iv, 0, iv.Length);
+            ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte>(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<byte>(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 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
                             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 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
         }
     }
 
-    public void Dispose()
-    {
-        _keyDerivationKey.Dispose();
-    }
-
     public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte>(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<byte>(keyModifier),
-                        prfFactory: _kdkPrfFactory,
-                        output: new ArraySegment<byte>(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 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
                         // 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 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
                             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 @@ internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncry
             throw Error.CryptCommon_GenericError(ex);
         }
     }
+
+    private void CalculateAndValidateMac(
+        byte[] payloadArray,
+        int ivOffset, int macOffset, int eofOffset, // offsets to slice the payload array
+        ReadOnlySpan<byte> validationSubkey,
+        byte[]? validationSubkeyArray)
+    {
+        using var validationAlgorithm = CreateValidationAlgorithm();
+        var hashSize = validationAlgorithm.GetDigestSizeInBytes();
+
+        byte[]? correctHashArray = null;
+        Span<byte> 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();
+    }
 }

+ 5 - 0
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 @@ internal sealed unsafe class ManagedGenRandomImpl : IManagedGenRandom
     {
     }
 
+#if NET10_0_OR_GREATER
+    public void GenRandom(Span<byte> target) => RandomNumberGenerator.Fill(target);
+#endif
+
     public byte[] GenRandom(int numBytes)
     {
         var bytes = new byte[numBytes];

+ 102 - 25
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<byte> label, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
+#if !NET10_0_OR_GREATER
+    public static void DeriveKeys(
+        byte[] kdk,
+        ReadOnlySpan<byte> label,
+        ReadOnlySpan<byte> contextHeader,
+        ReadOnlySpan<byte> contextData,
+        Span<byte> operationSubkey,
+        Span<byte> 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<byte> kdk, ReadOnlySpan<byte> label, ReadOnlySpan<byte> contextHeader, ReadOnlySpan<byte> contextData, Span<byte> operationSubkey, Span<byte> validationSubkey)
+        => DeriveKeys(kdk, label, contextHeader, contextData, operationSubkey, validationSubkey, prf: null);
+#endif
+
+    /// <remarks>
+    /// 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
+    /// </remarks>
+    private static void DeriveKeys(
+        ReadOnlySpan<byte> kdk,
+        ReadOnlySpan<byte> label,
+        ReadOnlySpan<byte> contextHeader,
+        ReadOnlySpan<byte> contextData,
+        Span<byte> operationSubkey,
+        Span<byte> 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<byte> 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<byte> 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 @@ internal static class ManagedSP800_108_CTR_HMACSHA512
             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 @@ internal static class ManagedSP800_108_CTR_HMACSHA512
                 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<byte> label, byte[] contextHeader, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> 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<byte>(combinedContext), prfFactory, output);
+        finally
+        {
+            prfInput.Clear();
+        }
     }
 }

+ 1 - 0
src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/KeyRingBasedDataProtectorTests.cs

@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
 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;

+ 1 - 2
src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SP800_108/SP800_108Tests.cs

@@ -163,8 +163,7 @@ public unsafe class SP800_108Tests
         Buffer.BlockCopy(context, 0, contextSegment.Array, contextSegment.Offset, contextSegment.Count);
         var derivedSubkeySegment = new ArraySegment<byte>(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<byte>.Empty /* filling in derivedSubkeySegment only */);
         Assert.Equal(expectedDerivedSubkeyAsBase64, Convert.ToBase64String(derivedSubkeySegment.AsStandaloneArray()));
     }
 }

+ 9 - 0
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 @@ internal unsafe class SequentialGenRandom : IBCryptGenRandom, IManagedGenRandom
             pbBuffer[i] = _value++;
         }
     }
+
+    public void GenRandom(Span<byte> target)
+    {
+        for (var i = 0; i < target.Length; i++)
+        {
+            target[i] = _value++;
+        }
+    }
 }