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++; + } + } }