diff --git a/AspNetCore.slnx b/AspNetCore.slnx index 60f5ca0d74fc..a1029b412c5e 100644 --- a/AspNetCore.slnx +++ b/AspNetCore.slnx @@ -161,6 +161,9 @@ + + + @@ -1143,6 +1146,7 @@ + @@ -1190,8 +1194,8 @@ - + diff --git a/src/DataProtection/Abstractions/src/ISpanDataProtector.cs b/src/DataProtection/Abstractions/src/ISpanDataProtector.cs new file mode 100644 index 000000000000..641d36910207 --- /dev/null +++ b/src/DataProtection/Abstractions/src/ISpanDataProtector.cs @@ -0,0 +1,56 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.DataProtection; + +/// +/// An interface that can provide data protection services. +/// Is an optimized version of . +/// +public interface ISpanDataProtector : IDataProtector +{ + /// + /// Cryptographically protects a piece of plaintext data and writes the result to a buffer writer. + /// + /// The type of buffer writer to write the protected data to. + /// The plaintext data to protect. + /// The buffer writer to which the protected data will be written. + /// + /// This method provides an optimized, streaming alternative to . + /// Rather than allocating an intermediate buffer, the protected data is written directly to the provided + /// buffer writer, which can improve performance and reduce memory allocation pressure. + /// The buffer writer is advanced by the total number of bytes written to it. + /// + void Protect(ReadOnlySpan plaintext, ref TWriter destination) + where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif + ; + + /// + /// Cryptographically unprotects a piece of protected data and writes the result to a buffer writer. + /// + /// The type of buffer writer to write the unprotected data to. + /// The protected data to unprotect. + /// The buffer writer to which the unprotected plaintext will be written. + /// + /// This method provides an optimized, streaming alternative to . + /// Rather than allocating an intermediate buffer, the unprotected plaintext is written directly to the provided + /// buffer writer, which can improve performance and reduce memory allocation pressure. + /// The buffer writer is advanced by the total number of bytes written to it. + /// + void Unprotect(ReadOnlySpan protectedData, ref TWriter destination) + where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif + ; +} diff --git a/src/DataProtection/Abstractions/src/Microsoft.AspNetCore.DataProtection.Abstractions.csproj b/src/DataProtection/Abstractions/src/Microsoft.AspNetCore.DataProtection.Abstractions.csproj index 1fe6c9dd19ba..694e0a251ed0 100644 --- a/src/DataProtection/Abstractions/src/Microsoft.AspNetCore.DataProtection.Abstractions.csproj +++ b/src/DataProtection/Abstractions/src/Microsoft.AspNetCore.DataProtection.Abstractions.csproj @@ -22,6 +22,10 @@ Microsoft.AspNetCore.DataProtection.IDataProtector + + + + diff --git a/src/DataProtection/Abstractions/src/PublicAPI.Unshipped.txt b/src/DataProtection/Abstractions/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..59769c61fd0a 100644 --- a/src/DataProtection/Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/DataProtection/Abstractions/src/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.AspNetCore.DataProtection.ISpanDataProtector +Microsoft.AspNetCore.DataProtection.ISpanDataProtector.Protect(System.ReadOnlySpan plaintext, TWriter destination) -> void +Microsoft.AspNetCore.DataProtection.ISpanDataProtector.Unprotect(System.ReadOnlySpan protectedData, TWriter destination) -> void diff --git a/src/DataProtection/DataProtection.slnf b/src/DataProtection/DataProtection.slnf index dafc26b7f42a..cd4ecbfc4810 100644 --- a/src/DataProtection/DataProtection.slnf +++ b/src/DataProtection/DataProtection.slnf @@ -16,6 +16,7 @@ "src\\DataProtection\\Extensions\\test\\Microsoft.AspNetCore.DataProtection.Extensions.Tests.csproj", "src\\DataProtection\\StackExchangeRedis\\src\\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj", "src\\DataProtection\\StackExchangeRedis\\test\\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Tests.csproj", + "src\\DataProtection\\benchmarks\\Microsoft.AspNetCore.DataProtection.MicroBenchmarks\\Microsoft.AspNetCore.DataProtection.MicroBenchmarks.csproj", "src\\DataProtection\\samples\\CustomEncryptorSample\\CustomEncryptorSample.csproj", "src\\DataProtection\\samples\\EntityFrameworkCoreSample\\EntityFrameworkCoreSample.csproj", "src\\DataProtection\\samples\\KeyManagementSample\\KeyManagementSample.csproj", diff --git a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ISpanAuthenticatedEncryptor.cs b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ISpanAuthenticatedEncryptor.cs new file mode 100644 index 000000000000..cba950dae7d9 --- /dev/null +++ b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ISpanAuthenticatedEncryptor.cs @@ -0,0 +1,64 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; + +/// +/// Provides an authenticated encryption and decryption routine via a span-based API. +/// +public interface ISpanAuthenticatedEncryptor : IAuthenticatedEncryptor +{ + /// + /// Encrypts and authenticates a piece of plaintext data and writes the result to a buffer writer. + /// + /// The type of buffer writer to write the ciphertext to. + /// The plaintext to encrypt. This input may be zero bytes in length. + /// + /// A piece of data which will not be included in the returned ciphertext + /// but which will still be covered by the authentication tag. This input may be zero bytes in length. + /// The same AAD must be specified in the corresponding call to . + /// + /// The buffer writer to which the ciphertext (including authentication tag) will be written. + /// + /// This method provides an optimized, streaming alternative to . + /// Rather than allocating an intermediate buffer, the ciphertext is written directly to the provided buffer writer, + /// which can improve performance and reduce memory allocation pressure. + /// The buffer writer is advanced by the total number of bytes written to it. + /// + void Encrypt(ReadOnlySpan plaintext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) + where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif + ; + + /// + /// Validates the authentication tag of and decrypts a blob of encrypted data, writing the result to a buffer writer. + /// + /// The type of buffer writer to write the plaintext to. + /// The ciphertext (including authentication tag) to decrypt. + /// + /// Any ancillary data which was used during computation of the authentication tag. + /// The same AAD must have been specified in the corresponding call to . + /// + /// The buffer writer to which the decrypted plaintext will be written. + /// + /// This method provides an optimized, streaming alternative to . + /// Rather than allocating an intermediate buffer, the plaintext is written directly to the provided buffer writer, + /// which can improve performance and reduce memory allocation pressure. + /// The buffer writer is advanced by the total number of bytes written to it. + /// + void Decrypt(ReadOnlySpan ciphertext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) + where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif + ; +} diff --git a/src/DataProtection/DataProtection/src/Cng/CbcAuthenticatedEncryptor.cs b/src/DataProtection/DataProtection/src/Cng/CbcAuthenticatedEncryptor.cs index c77f84671f38..4e0cc67d1393 100644 --- a/src/DataProtection/DataProtection/src/Cng/CbcAuthenticatedEncryptor.cs +++ b/src/DataProtection/DataProtection/src/Cng/CbcAuthenticatedEncryptor.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; +using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Cryptography; using Microsoft.AspNetCore.Cryptography.Cng; using Microsoft.AspNetCore.Cryptography.SafeHandles; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; -using Microsoft.AspNetCore.DataProtection.Cng.Internal; using Microsoft.AspNetCore.DataProtection.SP800_108; namespace Microsoft.AspNetCore.DataProtection.Cng; @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.DataProtection.Cng; // An encryptor which does Encrypt(CBC) + HMAC using the Windows CNG (BCrypt*) APIs. // The payloads produced by this encryptor should be compatible with the payloads // produced by the managed Encrypt(CBC) + HMAC encryptor. -internal sealed unsafe class CbcAuthenticatedEncryptor : CngAuthenticatedEncryptorBase +internal sealed unsafe class CbcAuthenticatedEncryptor : IOptimizedAuthenticatedEncryptor, ISpanAuthenticatedEncryptor, IDisposable { // Even when IVs are chosen randomly, CBC is susceptible to IV collisions within a single // key. For a 64-bit block cipher (like 3DES), we'd expect a collision after 2^32 block @@ -56,222 +57,153 @@ public CbcAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle _contextHeader = CreateContextHeader(); } - private byte[] CreateContextHeader() + public void Decrypt(ReadOnlySpan ciphertext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif { - var retVal = new byte[checked( - 1 /* KDF alg */ - + 1 /* chaining mode */ - + sizeof(uint) /* sym alg key size */ - + sizeof(uint) /* sym alg block size */ - + sizeof(uint) /* hmac alg key size */ - + sizeof(uint) /* hmac alg digest size */ - + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */ - + _hmacAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)]; - - fixed (byte* pbRetVal = retVal) + // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC + if (ciphertext.Length < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes)) { - byte* ptr = pbRetVal; - - // First is the two-byte header - *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF - *(ptr++) = 0; // 0x00 = CBC encryption + HMAC authentication + throw Error.CryptCommon_PayloadInvalid(); + } - // Next is information about the symmetric algorithm (key size followed by block size) - BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmSubkeyLengthInBytes); - BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmBlockSizeInBytes); + var cbEncryptedDataLength = checked(ciphertext.Length - (int)(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes)); - // Next is information about the HMAC algorithm (key size followed by digest size) - BitHelpers.WriteTo(ref ptr, _hmacAlgorithmSubkeyLengthInBytes); - BitHelpers.WriteTo(ref ptr, _hmacAlgorithmDigestLengthInBytes); - - // See the design document for an explanation of the following code. - var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes]; - fixed (byte* pbTempKeys = tempKeys) + // Assumption: ciphertext := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) } + fixed (byte* pbCiphertext = ciphertext) + fixed (byte* pbAdditionalAuthenticatedData = additionalAuthenticatedData) + { + // Calculate offsets + byte* pbKeyModifier = pbCiphertext; + byte* pbIV = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; + byte* pbEncryptedData = &pbIV[_symmetricAlgorithmBlockSizeInBytes]; + byte* pbActualHmac = &pbEncryptedData[cbEncryptedDataLength]; + + // Use the KDF to recreate the symmetric encryption and HMAC subkeys + // We'll need a temporary buffer to hold them + var cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes); + byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)]; + try { - byte dummy; - - // Derive temporary keys for encryption + HMAC. - using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider()) - { - provider.DeriveKey( - pbLabel: &dummy, - cbLabel: 0, - pbContext: &dummy, - cbContext: 0, - pbDerivedKey: pbTempKeys, - cbDerivedKey: (uint)tempKeys.Length); - } - - // At this point, tempKeys := { K_E || K_H }. - byte* pbSymmetricEncryptionSubkey = pbTempKeys; - byte* pbHmacSubkey = &pbTempKeys[_symmetricAlgorithmSubkeyLengthInBytes]; - - // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer. - using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) + _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( + pbLabel: pbAdditionalAuthenticatedData, + cbLabel: (uint)additionalAuthenticatedData.Length, + contextHeader: _contextHeader, + pbContext: pbKeyModifier, + cbContext: KEY_MODIFIER_SIZE_IN_BYTES, + pbDerivedKey: pbTempSubkeys, + cbDerivedKey: cbTempSubkeys); + + // Calculate offsets + byte* pbSymmetricEncryptionSubkey = pbTempSubkeys; + byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes]; + + // First, perform an explicit integrity check over (iv | encryptedPayload) to ensure the + // data hasn't been tampered with. The integrity check is also implicitly performed over + // keyModifier since that value was provided to the KDF earlier. + using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) { - fixed (byte* pbIV = new byte[_symmetricAlgorithmBlockSizeInBytes] /* will be zero-initialized */) + if (!ValidateHash(hashHandle, pbIV, _symmetricAlgorithmBlockSizeInBytes + (uint)cbEncryptedDataLength, pbActualHmac)) { - DoCbcEncrypt( - symmetricKeyHandle: symmetricKeyHandle, - pbIV: pbIV, - pbInput: &dummy, - cbInput: 0, - pbOutput: ptr, - cbOutput: _symmetricAlgorithmBlockSizeInBytes); + throw Error.CryptCommon_PayloadInvalid(); } } - ptr += _symmetricAlgorithmBlockSizeInBytes; - // MAC a zero-length input string and copy the digest to the return buffer. - using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) + // If the integrity check succeeded, decrypt the payload. + using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) { - hashHandle.HashData( - pbInput: &dummy, - cbInput: 0, - pbHashDigest: ptr, - cbHashDigest: _hmacAlgorithmDigestLengthInBytes); + // BCryptDecrypt mutates the provided IV; we need to clone it to prevent mutation of the original value + byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)]; + UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes); + + // First, query the output size needed + var ntstatus = UnsafeNativeMethods.BCryptDecrypt( + hKey: decryptionSubkeyHandle, + pbInput: pbEncryptedData, + cbInput: (uint)cbEncryptedDataLength, + pPaddingInfo: null, + pbIV: pbClonedIV, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: null, // NULL output = size query only + cbOutput: 0, + pcbResult: out var dwRequiredSize, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + // Get buffer from writer with the required size + var buffer = destination.GetSpan(checked((int)dwRequiredSize)); + + // Clone IV again for the actual decryption call + byte* pbClonedIV2 = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)]; + UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV2, byteCount: _symmetricAlgorithmBlockSizeInBytes); + + // Perform the actual decryption + fixed (byte* pbBuffer = buffer) + { + byte dummy; + ntstatus = UnsafeNativeMethods.BCryptDecrypt( + hKey: decryptionSubkeyHandle, + pbInput: pbEncryptedData, + cbInput: (uint)cbEncryptedDataLength, + pPaddingInfo: null, + pbIV: pbClonedIV2, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: (buffer.Length > 0) ? pbBuffer : &dummy, + cbOutput: (uint)buffer.Length, + pcbResult: out var dwActualDecryptedByteCount, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + // Advance the writer by the number of bytes actually written + destination.Advance(checked((int)dwActualDecryptedByteCount)); + } } - - ptr += _hmacAlgorithmDigestLengthInBytes; - CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length"); + } + finally + { + // Buffer contains sensitive key material; delete. + UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys); } } - - // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || hmacAlgKeySize || hmacAlgDigestSize || E("") || MAC("") }. - return retVal; } - protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) + public byte[] Decrypt(ArraySegment ciphertext, ArraySegment additionalAuthenticatedData) { - // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC - if (cbCiphertext < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes)) + ciphertext.Validate(); + additionalAuthenticatedData.Validate(); + + var outputSize = ciphertext.Count - (int)(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes); + if (outputSize <= 0) { throw Error.CryptCommon_PayloadInvalid(); } - // Assumption: pbCipherText := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) } - - var cbEncryptedData = checked(cbCiphertext - (KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes)); - - // Calculate offsets - byte* pbKeyModifier = pbCiphertext; - byte* pbIV = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; - byte* pbEncryptedData = &pbIV[_symmetricAlgorithmBlockSizeInBytes]; - byte* pbActualHmac = &pbEncryptedData[cbEncryptedData]; - - // Use the KDF to recreate the symmetric encryption and HMAC subkeys - // We'll need a temporary buffer to hold them - var cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes); - byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)]; +#if NET + var refPooledBuffer = new RefPooledArrayBufferWriter(outputSize); try { - _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( - pbLabel: pbAdditionalAuthenticatedData, - cbLabel: cbAdditionalAuthenticatedData, - contextHeader: _contextHeader, - pbContext: pbKeyModifier, - cbContext: KEY_MODIFIER_SIZE_IN_BYTES, - pbDerivedKey: pbTempSubkeys, - cbDerivedKey: cbTempSubkeys); - - // Calculate offsets - byte* pbSymmetricEncryptionSubkey = pbTempSubkeys; - byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes]; - - // First, perform an explicit integrity check over (iv | encryptedPayload) to ensure the - // data hasn't been tampered with. The integrity check is also implicitly performed over - // keyModifier since that value was provided to the KDF earlier. - using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) - { - if (!ValidateHash(hashHandle, pbIV, _symmetricAlgorithmBlockSizeInBytes + cbEncryptedData, pbActualHmac)) - { - throw Error.CryptCommon_PayloadInvalid(); - } - } - - // If the integrity check succeeded, decrypt the payload. - using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) - { - return DoCbcDecrypt(decryptionSubkeyHandle, pbIV, pbEncryptedData, cbEncryptedData); - } + Decrypt(ciphertext, additionalAuthenticatedData, ref refPooledBuffer); + return refPooledBuffer.WrittenSpan.ToArray(); } finally { - // Buffer contains sensitive key material; delete. - UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys); + refPooledBuffer.Dispose(); } - } - public override void Dispose() - { - _sp800_108_ctr_hmac_provider.Dispose(); - - // We don't want to dispose of the underlying algorithm instances because they - // might be reused. - } - - // 'pbIV' must be a pointer to a buffer equal in length to the symmetric algorithm block size. - private byte[] DoCbcDecrypt(BCryptKeyHandle symmetricKeyHandle, byte* pbIV, byte* pbInput, uint cbInput) - { - // BCryptDecrypt mutates the provided IV; we need to clone it to prevent mutation of the original value - byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)]; - UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes); - - // First, figure out how large an output buffer we require. - // Ideally we'd be able to transform the last block ourselves and strip - // off the padding before creating the return value array, but we don't - // know the actual padding scheme being used under the covers (we can't - // assume PKCS#7). So unfortunately we're stuck with the temporary buffer. - // (Querying the output size won't mutate the IV.) - uint dwEstimatedDecryptedByteCount; - var ntstatus = UnsafeNativeMethods.BCryptDecrypt( - hKey: symmetricKeyHandle, - pbInput: pbInput, - cbInput: cbInput, - pPaddingInfo: null, - pbIV: pbClonedIV, - cbIV: _symmetricAlgorithmBlockSizeInBytes, - pbOutput: null, - cbOutput: 0, - pcbResult: out dwEstimatedDecryptedByteCount, - dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); - UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); - - var decryptedPayload = new byte[dwEstimatedDecryptedByteCount]; - uint dwActualDecryptedByteCount; - fixed (byte* pbDecryptedPayload = decryptedPayload) - { - byte dummy; - - // Perform the actual decryption. - ntstatus = UnsafeNativeMethods.BCryptDecrypt( - hKey: symmetricKeyHandle, - pbInput: pbInput, - cbInput: cbInput, - pPaddingInfo: null, - pbIV: pbClonedIV, - cbIV: _symmetricAlgorithmBlockSizeInBytes, - pbOutput: (pbDecryptedPayload != null) ? pbDecryptedPayload : &dummy, // CLR won't pin zero-length arrays - cbOutput: dwEstimatedDecryptedByteCount, - pcbResult: out dwActualDecryptedByteCount, - dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); - UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); - } - - // Decryption finished! - CryptoUtil.Assert(dwActualDecryptedByteCount <= dwEstimatedDecryptedByteCount, "dwActualDecryptedByteCount <= dwEstimatedDecryptedByteCount"); - if (dwActualDecryptedByteCount == dwEstimatedDecryptedByteCount) +#else + var pooledArrayBuffer = new PooledArrayBufferWriter(outputSize); + try { - // payload takes up the entire buffer - return decryptedPayload; + Decrypt(ciphertext, additionalAuthenticatedData, ref pooledArrayBuffer); + return pooledArrayBuffer.WrittenSpan.ToArray(); } - else + finally { - // payload takes up only a partial buffer - var resizedDecryptedPayload = new byte[dwActualDecryptedByteCount]; - Buffer.BlockCopy(decryptedPayload, 0, resizedDecryptedPayload, 0, resizedDecryptedPayload.Length); - return resizedDecryptedPayload; + pooledArrayBuffer.Dispose(); } +#endif } // 'pbIV' must be a pointer to a buffer equal in length to the symmetric algorithm block size. @@ -299,7 +231,16 @@ private void DoCbcEncrypt(BCryptKeyHandle symmetricKeyHandle, byte* pbIV, byte* CryptoUtil.Assert(dwEncryptedBytes == cbOutput, "dwEncryptedBytes == cbOutput"); } - protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer) + public int GetEncryptedSize(int plainTextLength) + { + uint paddedCiphertextLength = GetCbcEncryptedOutputSizeWithPadding((uint)plainTextLength); + return checked((int)(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + paddedCiphertextLength + _hmacAlgorithmDigestLengthInBytes)); + } + + public void Encrypt(ReadOnlySpan plaintext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif { // This buffer will be used to hold the symmetric encryption and HMAC subkeys // used in the generation of this payload. @@ -318,14 +259,17 @@ protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* byte* pbIV = &pbKeyModifierAndIV[KEY_MODIFIER_SIZE_IN_BYTES]; // Use the KDF to generate a new symmetric encryption and HMAC subkey - _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( - pbLabel: pbAdditionalAuthenticatedData, - cbLabel: cbAdditionalAuthenticatedData, - contextHeader: _contextHeader, - pbContext: pbKeyModifier, - cbContext: KEY_MODIFIER_SIZE_IN_BYTES, - pbDerivedKey: pbTempSubkeys, - cbDerivedKey: cbTempSubkeys); + fixed (byte* pbAdditionalAuthenticatedData = additionalAuthenticatedData) + { + _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( + pbLabel: pbAdditionalAuthenticatedData, + cbLabel: (uint)additionalAuthenticatedData.Length, + contextHeader: _contextHeader, + pbContext: pbKeyModifier, + cbContext: KEY_MODIFIER_SIZE_IN_BYTES, + pbDerivedKey: pbTempSubkeys, + cbDerivedKey: cbTempSubkeys); + } // Calculate offsets byte* pbSymmetricEncryptionSubkey = pbTempSubkeys; @@ -333,50 +277,76 @@ protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) { - // We can't assume PKCS#7 padding (maybe the underlying provider is really using CTS), - // so we need to query the padded output size before we can allocate the return value array. - var cbOutputCiphertext = GetCbcEncryptedOutputSizeWithPadding(symmetricKeyHandle, pbPlaintext, cbPlaintext); - - // Allocate return value array and start copying some data - var retVal = new byte[checked(cbPreBuffer + KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext + _hmacAlgorithmDigestLengthInBytes + cbPostBuffer)]; - fixed (byte* pbRetVal = retVal) + // Query the padded ciphertext output size + byte dummy; + fixed (byte* pbPlaintextArray = plaintext) { - // Calculate offsets - byte* pbOutputKeyModifier = &pbRetVal[cbPreBuffer]; - byte* pbOutputIV = &pbOutputKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; - byte* pbOutputCiphertext = &pbOutputIV[_symmetricAlgorithmBlockSizeInBytes]; - byte* pbOutputHmac = &pbOutputCiphertext[cbOutputCiphertext]; - - UnsafeBufferUtil.BlockCopy(from: pbKeyModifierAndIV, to: pbOutputKeyModifier, byteCount: cbKeyModifierAndIV); - - // retVal will eventually contain { preBuffer | keyModifier | iv | encryptedData | HMAC(iv | encryptedData) | postBuffer } - // At this point, retVal := { preBuffer | keyModifier | iv | _____ | _____ | postBuffer } + var pbPlaintext = (pbPlaintextArray != null) ? pbPlaintextArray : &dummy; - DoCbcEncrypt( - symmetricKeyHandle: symmetricKeyHandle, - pbIV: pbIV, + // First, query the size needed for ciphertext + var ntstatus = UnsafeNativeMethods.BCryptEncrypt( + hKey: symmetricKeyHandle, pbInput: pbPlaintext, - cbInput: cbPlaintext, - pbOutput: pbOutputCiphertext, - cbOutput: cbOutputCiphertext); + cbInput: (uint)plaintext.Length, + pPaddingInfo: null, + pbIV: pbIV, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: null, // NULL output = size query only + cbOutput: 0, + pcbResult: out var dwCiphertextSize, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); - // At this point, retVal := { preBuffer | keyModifier | iv | encryptedData | _____ | postBuffer } + // Calculate total required size + var totalRequiredSize = checked((int)(cbKeyModifierAndIV + dwCiphertextSize + _hmacAlgorithmDigestLengthInBytes)); - // Compute the HMAC over the IV and the ciphertext (prevents IV tampering). - // The HMAC is already implicitly computed over the key modifier since the key - // modifier is used as input to the KDF. - using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) + // Get buffer from writer with the required total size + var buffer = destination.GetSpan(totalRequiredSize); + + fixed (byte* pbBuffer = buffer) { - hashHandle.HashData( - pbInput: pbOutputIV, - cbInput: checked(_symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext), - pbHashDigest: pbOutputHmac, - cbHashDigest: _hmacAlgorithmDigestLengthInBytes); + // Calculate offsets in destination buffer + byte* pbOutputKeyModifier = pbBuffer; + byte* pbOutputIV = &pbOutputKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; + byte* pbOutputCiphertext = &pbOutputIV[_symmetricAlgorithmBlockSizeInBytes]; + byte* pbOutputHmac = &pbOutputCiphertext[dwCiphertextSize]; + + // Copy key modifier and IV to output + Unsafe.CopyBlock(pbOutputKeyModifier, pbKeyModifierAndIV, cbKeyModifierAndIV); + + // Clone IV for encryption (BCryptEncrypt mutates it) + byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)]; + UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes); + + // Perform encryption + ntstatus = UnsafeNativeMethods.BCryptEncrypt( + hKey: symmetricKeyHandle, + pbInput: pbPlaintext, + cbInput: (uint)plaintext.Length, + pPaddingInfo: null, + pbIV: pbClonedIV, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: pbOutputCiphertext, + cbOutput: dwCiphertextSize, + pcbResult: out var dwActualCiphertextSize, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + CryptoUtil.Assert(dwActualCiphertextSize == dwCiphertextSize, "dwActualCiphertextSize == dwCiphertextSize"); + + // Calculate HMAC over (IV | ciphertext) + using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) + { + hashHandle.HashData( + pbInput: pbOutputIV, + cbInput: checked(_symmetricAlgorithmBlockSizeInBytes + dwCiphertextSize), + pbHashDigest: pbOutputHmac, + cbHashDigest: _hmacAlgorithmDigestLengthInBytes); + } + + // Advance the writer by the total bytes written + destination.Advance(totalRequiredSize); } - - // At this point, retVal := { preBuffer | keyModifier | iv | encryptedData | HMAC(iv | encryptedData) | postBuffer } - // And we're done! - return retVal; } } } @@ -387,6 +357,90 @@ protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* } } + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) + => Encrypt(plaintext, additionalAuthenticatedData, 0, 0); + + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData, uint preBufferSize, uint postBufferSize) + { + plaintext.Validate(); + additionalAuthenticatedData.Validate(); + + var size = GetEncryptedSize(plaintext.Count); + var outputSize = (int)(preBufferSize + size + postBufferSize); + +#if NET + var refPooledBuffer = new RefPooledArrayBufferWriter(outputSize); + try + { + // arrays are pooled. and they MAY contain non-zeros in the pre-buffer and post-buffer regions. + // we could clean them up, but it's not strictly necessary - the important part is that output array + // has those pre/post buffer regions, which will be used by the caller. + refPooledBuffer.Advance(preBufferSize); + Encrypt(plaintext, additionalAuthenticatedData, ref refPooledBuffer); + refPooledBuffer.Advance(postBufferSize); + + var resultSpan = refPooledBuffer.WrittenSpan.ToArray(); + CryptoUtil.Assert(resultSpan.Length == outputSize, "writtenSpan length should equal calculated outputSize"); + return resultSpan; + } + finally + { + refPooledBuffer.Dispose(); + } +#else + var pooledArrayBuffer = new PooledArrayBufferWriter(outputSize); + try + { + pooledArrayBuffer.Advance(preBufferSize); + Encrypt(plaintext, additionalAuthenticatedData, ref pooledArrayBuffer); + pooledArrayBuffer.Advance(postBufferSize); + + var resultSpan = pooledArrayBuffer.WrittenSpan.ToArray(); + CryptoUtil.Assert(resultSpan.Length == outputSize, "writtenSpan length should equal calculated outputSize"); + + return resultSpan; + } + finally + { + pooledArrayBuffer.Dispose(); + } +#endif + } + + /// + /// Should be used only for expected encrypt/decrypt size calculation, + /// use the other overload + /// for the actual encryption algorithm + /// + private uint GetCbcEncryptedOutputSizeWithPadding(uint cbInput) + { + // Create a temporary key with dummy data for size calculation only + // The actual key material doesn't matter for size calculation + byte* pbDummyKey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)]; + // Leave pbDummyKey uninitialized (all zeros) - BCrypt doesn't care for size queries + + using var tempKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbDummyKey, _symmetricAlgorithmSubkeyLengthInBytes); + + // Use uninitialized IV and input data - only the lengths matter + byte* pbDummyIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)]; + byte* pbDummyInput = stackalloc byte[checked((int)cbInput)]; + + var ntstatus = UnsafeNativeMethods.BCryptEncrypt( + hKey: tempKeyHandle, + pbInput: pbDummyInput, + cbInput: cbInput, + pPaddingInfo: null, + pbIV: pbDummyIV, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: null, // NULL output = size query only + cbOutput: 0, + pcbResult: out var dwResult, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + return dwResult; + } + private uint GetCbcEncryptedOutputSizeWithPadding(BCryptKeyHandle symmetricKeyHandle, byte* pbInput, uint cbInput) { // ok for this memory to remain uninitialized since nobody depends on it @@ -394,7 +448,6 @@ private uint GetCbcEncryptedOutputSizeWithPadding(BCryptKeyHandle symmetricKeyHa // Calling BCryptEncrypt with a null output pointer will cause it to return the total number // of bytes required for the output buffer. - uint dwResult; var ntstatus = UnsafeNativeMethods.BCryptEncrypt( hKey: symmetricKeyHandle, pbInput: pbInput, @@ -404,7 +457,7 @@ private uint GetCbcEncryptedOutputSizeWithPadding(BCryptKeyHandle symmetricKeyHa cbIV: _symmetricAlgorithmBlockSizeInBytes, pbOutput: null, cbOutput: 0, - pcbResult: out dwResult, + pcbResult: out var dwResult, dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); @@ -418,4 +471,97 @@ private bool ValidateHash(BCryptHashHandle hashHandle, byte* pbInput, uint cbInp hashHandle.HashData(pbInput, cbInput, pbActualDigest, _hmacAlgorithmDigestLengthInBytes); return CryptoUtil.TimeConstantBuffersAreEqual(pbExpectedDigest, pbActualDigest, _hmacAlgorithmDigestLengthInBytes); } + + private byte[] CreateContextHeader() + { + var retVal = new byte[checked( + 1 /* KDF alg */ + + 1 /* chaining mode */ + + sizeof(uint) /* sym alg key size */ + + sizeof(uint) /* sym alg block size */ + + sizeof(uint) /* hmac alg key size */ + + sizeof(uint) /* hmac alg digest size */ + + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */ + + _hmacAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)]; + + fixed (byte* pbRetVal = retVal) + { + byte* ptr = pbRetVal; + + // First is the two-byte header + *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF + *(ptr++) = 0; // 0x00 = CBC encryption + HMAC authentication + + // Next is information about the symmetric algorithm (key size followed by block size) + BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmSubkeyLengthInBytes); + BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmBlockSizeInBytes); + + // Next is information about the HMAC algorithm (key size followed by digest size) + BitHelpers.WriteTo(ref ptr, _hmacAlgorithmSubkeyLengthInBytes); + BitHelpers.WriteTo(ref ptr, _hmacAlgorithmDigestLengthInBytes); + + // See the design document for an explanation of the following code. + var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes]; + fixed (byte* pbTempKeys = tempKeys) + { + byte dummy; + + // Derive temporary keys for encryption + HMAC. + using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider()) + { + provider.DeriveKey( + pbLabel: &dummy, + cbLabel: 0, + pbContext: &dummy, + cbContext: 0, + pbDerivedKey: pbTempKeys, + cbDerivedKey: (uint)tempKeys.Length); + } + + // At this point, tempKeys := { K_E || K_H }. + byte* pbSymmetricEncryptionSubkey = pbTempKeys; + byte* pbHmacSubkey = &pbTempKeys[_symmetricAlgorithmSubkeyLengthInBytes]; + + // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer. + using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) + { + fixed (byte* pbIV = new byte[_symmetricAlgorithmBlockSizeInBytes] /* will be zero-initialized */) + { + DoCbcEncrypt( + symmetricKeyHandle: symmetricKeyHandle, + pbIV: pbIV, + pbInput: &dummy, + cbInput: 0, + pbOutput: ptr, + cbOutput: _symmetricAlgorithmBlockSizeInBytes); + } + } + ptr += _symmetricAlgorithmBlockSizeInBytes; + + // MAC a zero-length input string and copy the digest to the return buffer. + using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) + { + hashHandle.HashData( + pbInput: &dummy, + cbInput: 0, + pbHashDigest: ptr, + cbHashDigest: _hmacAlgorithmDigestLengthInBytes); + } + + ptr += _hmacAlgorithmDigestLengthInBytes; + CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length"); + } + } + + // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || hmacAlgKeySize || hmacAlgDigestSize || E("") || MAC("") }. + return retVal; + } + + public void Dispose() + { + _sp800_108_ctr_hmac_provider.Dispose(); + + // We don't want to dispose of the underlying algorithm instances because they + // might be reused. + } } diff --git a/src/DataProtection/DataProtection/src/Cng/CngGcmAuthenticatedEncryptor.cs b/src/DataProtection/DataProtection/src/Cng/CngGcmAuthenticatedEncryptor.cs index bf08a886e1f5..1218c19a66f7 100644 --- a/src/DataProtection/DataProtection/src/Cng/CngGcmAuthenticatedEncryptor.cs +++ b/src/DataProtection/DataProtection/src/Cng/CngGcmAuthenticatedEncryptor.cs @@ -1,11 +1,13 @@ // 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.Buffers; +using System.Security.Cryptography; using Microsoft.AspNetCore.Cryptography; using Microsoft.AspNetCore.Cryptography.Cng; using Microsoft.AspNetCore.Cryptography.SafeHandles; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; -using Microsoft.AspNetCore.DataProtection.Cng.Internal; using Microsoft.AspNetCore.DataProtection.SP800_108; namespace Microsoft.AspNetCore.DataProtection.Cng; @@ -20,7 +22,7 @@ namespace Microsoft.AspNetCore.DataProtection.Cng; // going to the IV. This means that we'll only hit the 2^-32 probability limit after 2^96 encryption // operations, which will realistically never happen. (At the absurd rate of one encryption operation // per nanosecond, it would still take 180 times the age of the universe to hit 2^96 operations.) -internal sealed unsafe class CngGcmAuthenticatedEncryptor : CngAuthenticatedEncryptorBase +internal sealed unsafe class CngGcmAuthenticatedEncryptor : IOptimizedAuthenticatedEncryptor, ISpanAuthenticatedEncryptor, IDisposable { // Having a key modifier ensures with overwhelming probability that no two encryption operations // will ever derive the same (encryption subkey, MAC subkey) pair. This limits an attacker's @@ -50,90 +52,28 @@ public CngGcmAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHand _contextHeader = CreateContextHeader(); } - private byte[] CreateContextHeader() - { - var retVal = new byte[checked( - 1 /* KDF alg */ - + 1 /* chaining mode */ - + sizeof(uint) /* sym alg key size */ - + sizeof(uint) /* GCM nonce size */ - + sizeof(uint) /* sym alg block size */ - + sizeof(uint) /* GCM tag size */ - + TAG_SIZE_IN_BYTES /* tag of GCM-encrypted empty string */)]; - - fixed (byte* pbRetVal = retVal) - { - byte* ptr = pbRetVal; - - // First is the two-byte header - *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF - *(ptr++) = 1; // 0x01 = GCM encryption + authentication - - // Next is information about the symmetric algorithm (key size, nonce size, block size, tag size) - BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmSubkeyLengthInBytes); - BitHelpers.WriteTo(ref ptr, NONCE_SIZE_IN_BYTES); - BitHelpers.WriteTo(ref ptr, TAG_SIZE_IN_BYTES); // block size = tag size - BitHelpers.WriteTo(ref ptr, TAG_SIZE_IN_BYTES); - - // See the design document for an explanation of the following code. - var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes]; - fixed (byte* pbTempKeys = tempKeys) - { - byte dummy; - - // Derive temporary key for encryption. - using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider()) - { - provider.DeriveKey( - pbLabel: &dummy, - cbLabel: 0, - pbContext: &dummy, - cbContext: 0, - pbDerivedKey: pbTempKeys, - cbDerivedKey: (uint)tempKeys.Length); - } - - // Encrypt a zero-length input string with an all-zero nonce and copy the tag to the return buffer. - byte* pbNonce = stackalloc byte[(int)NONCE_SIZE_IN_BYTES]; - UnsafeBufferUtil.SecureZeroMemory(pbNonce, NONCE_SIZE_IN_BYTES); - DoGcmEncrypt( - pbKey: pbTempKeys, - cbKey: _symmetricAlgorithmSubkeyLengthInBytes, - pbNonce: pbNonce, - pbPlaintextData: &dummy, - cbPlaintextData: 0, - pbEncryptedData: &dummy, - pbTag: ptr); - } - - ptr += TAG_SIZE_IN_BYTES; - CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length"); - } - - // retVal := { version || chainingMode || symAlgKeySize || nonceSize || symAlgBlockSize || symAlgTagSize || TAG-of-E("") }. - return retVal; - } - - protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) + public void Decrypt(ReadOnlySpan ciphertext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif { // Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag - if (cbCiphertext < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) + if (ciphertext.Length < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) { throw Error.CryptCommon_PayloadInvalid(); } - // Assumption: pbCipherText := { keyModifier || nonce || encryptedData || authenticationTag } + var plaintextLength = checked(ciphertext.Length - (int)(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES)); - var cbPlaintext = checked(cbCiphertext - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES)); - - var retVal = new byte[cbPlaintext]; - fixed (byte* pbRetVal = retVal) + // Assumption: ciphertext := { keyModifier || nonce || encryptedData || authenticationTag } + fixed (byte* pbCiphertext = ciphertext) + fixed (byte* pbAdditionalAuthenticatedData = additionalAuthenticatedData) { // Calculate offsets byte* pbKeyModifier = pbCiphertext; byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES]; - byte* pbAuthTag = &pbEncryptedData[cbPlaintext]; + byte* pbAuthTag = &pbEncryptedData[plaintextLength]; // Use the KDF to recreate the symmetric block cipher key // We'll need a temporary buffer to hold the symmetric encryption subkey @@ -142,45 +82,50 @@ protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byt { _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( pbLabel: pbAdditionalAuthenticatedData, - cbLabel: cbAdditionalAuthenticatedData, + cbLabel: (uint)additionalAuthenticatedData.Length, contextHeader: _contextHeader, pbContext: pbKeyModifier, cbContext: KEY_MODIFIER_SIZE_IN_BYTES, pbDerivedKey: pbSymmetricDecryptionSubkey, cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes); + // Get buffer from writer with the plaintext size + var buffer = destination.GetSpan(plaintextLength); + // Perform the decryption operation using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) { - byte dummy; - byte* pbPlaintext = (pbRetVal != null) ? pbRetVal : &dummy; // CLR doesn't like pinning empty buffers - - BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo; - BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out authInfo); - authInfo.pbNonce = pbNonce; - authInfo.cbNonce = NONCE_SIZE_IN_BYTES; - authInfo.pbTag = pbAuthTag; - authInfo.cbTag = TAG_SIZE_IN_BYTES; - - // The call to BCryptDecrypt will also validate the authentication tag - uint cbDecryptedBytesWritten; - var ntstatus = UnsafeNativeMethods.BCryptDecrypt( - hKey: decryptionSubkeyHandle, - pbInput: pbEncryptedData, - cbInput: cbPlaintext, - pPaddingInfo: &authInfo, - pbIV: null, // IV not used; nonce provided in pPaddingInfo - cbIV: 0, - pbOutput: pbPlaintext, - cbOutput: cbPlaintext, - pcbResult: out cbDecryptedBytesWritten, - dwFlags: 0); - UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); - CryptoUtil.Assert(cbDecryptedBytesWritten == cbPlaintext, "cbDecryptedBytesWritten == cbPlaintext"); - - // At this point, retVal := { decryptedPayload } - // And we're done! - return retVal; + fixed (byte* pbBuffer = buffer) + { + byte dummy; + byte* pbPlaintext = (plaintextLength > 0) ? pbBuffer : &dummy; // CLR doesn't like pinning empty buffers + + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo; + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out authInfo); + authInfo.pbNonce = pbNonce; + authInfo.cbNonce = NONCE_SIZE_IN_BYTES; + authInfo.pbTag = pbAuthTag; + authInfo.cbTag = TAG_SIZE_IN_BYTES; + + // The call to BCryptDecrypt will also validate the authentication tag + uint cbDecryptedBytesWritten; + var ntstatus = UnsafeNativeMethods.BCryptDecrypt( + hKey: decryptionSubkeyHandle, + pbInput: pbEncryptedData, + cbInput: (uint)plaintextLength, + pPaddingInfo: &authInfo, + pbIV: null, // IV not used; nonce provided in pPaddingInfo + cbIV: 0, + pbOutput: pbPlaintext, + cbOutput: (uint)plaintextLength, + pcbResult: out cbDecryptedBytesWritten, + dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + CryptoUtil.Assert(cbDecryptedBytesWritten == plaintextLength, "cbDecryptedBytesWritten == plaintextLength"); + + // Advance the writer by the number of bytes written + destination.Advance((int)cbDecryptedBytesWritten); + } } } finally @@ -191,12 +136,40 @@ protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byt } } - public override void Dispose() + public byte[] Decrypt(ArraySegment ciphertext, ArraySegment additionalAuthenticatedData) { - _sp800_108_ctr_hmac_provider.Dispose(); + ciphertext.Validate(); + additionalAuthenticatedData.Validate(); - // We don't want to dispose of the underlying algorithm instances because they - // might be reused. + var outputSize = ciphertext.Count - (int)(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES); + if (outputSize <= 0) + { + throw Error.CryptCommon_PayloadInvalid(); + } + +#if NET + var refPooledBuffer = new RefPooledArrayBufferWriter(outputSize); + try + { + Decrypt(ciphertext, additionalAuthenticatedData, ref refPooledBuffer); + return refPooledBuffer.WrittenSpan.ToArray(); + } + finally + { + refPooledBuffer.Dispose(); + } +#else + var pooledArrayBuffer = new PooledArrayBufferWriter(outputSize); + try + { + Decrypt(ciphertext, additionalAuthenticatedData, ref pooledArrayBuffer); + return pooledArrayBuffer.WrittenSpan.ToArray(); + } + finally + { + pooledArrayBuffer.Dispose(); + } +#endif } // 'pbNonce' must point to a 96-bit buffer. @@ -230,57 +203,197 @@ private void DoGcmEncrypt(byte* pbKey, uint cbKey, byte* pbNonce, byte* pbPlaint } } - protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer) + public void Encrypt(ReadOnlySpan plaintext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif { - // Allocate a buffer to hold the key modifier, nonce, encrypted data, and tag. - // In GCM, the encrypted output will be the same length as the plaintext input. - var retVal = new byte[checked(cbPreBuffer + KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + cbPlaintext + TAG_SIZE_IN_BYTES + cbPostBuffer)]; + try + { + // Calculate total required size: keyModifier + nonce + plaintext + tag + // In GCM, ciphertext length equals plaintext length + var totalRequiredSize = checked((int)(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Length + TAG_SIZE_IN_BYTES)); + + // Get buffer from writer with the required total size + var buffer = destination.GetSpan(totalRequiredSize); + + fixed (byte* pbBuffer = buffer) + { + // Calculate offsets + byte* pbKeyModifier = pbBuffer; + byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; + byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES]; + byte* pbAuthTag = &pbEncryptedData[plaintext.Length]; + + // Randomly generate the key modifier and nonce + _genRandom.GenRandom(pbKeyModifier, KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES); + + // Use the KDF to generate a new symmetric block cipher key + // We'll need a temporary buffer to hold the symmetric encryption subkey + byte* pbSymmetricEncryptionSubkey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)]; + try + { + fixed (byte* pbAdditionalAuthenticatedData = additionalAuthenticatedData) + { + _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( + pbLabel: pbAdditionalAuthenticatedData, + cbLabel: (uint)additionalAuthenticatedData.Length, + contextHeader: _contextHeader, + pbContext: pbKeyModifier, + cbContext: KEY_MODIFIER_SIZE_IN_BYTES, + pbDerivedKey: pbSymmetricEncryptionSubkey, + cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes); + } + + // Perform the encryption operation + byte dummy; + fixed (byte* pbPlaintextArray = plaintext) + { + var pbPlaintext = (pbPlaintextArray != null) ? pbPlaintextArray : &dummy; + + DoGcmEncrypt( + pbKey: pbSymmetricEncryptionSubkey, + cbKey: _symmetricAlgorithmSubkeyLengthInBytes, + pbNonce: pbNonce, + pbPlaintextData: pbPlaintext, + cbPlaintextData: (uint)plaintext.Length, + pbEncryptedData: pbEncryptedData, + pbTag: pbAuthTag); + } + + // Advance the writer by the total bytes written + destination.Advance(totalRequiredSize); + } + finally + { + // The buffer contains key material, so delete it. + UnsafeBufferUtil.SecureZeroMemory(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes); + } + } + } + catch (Exception ex) when (ex.RequiresHomogenization()) + { + throw Error.CryptCommon_GenericError(ex); + } + } + + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) + => Encrypt(plaintext, additionalAuthenticatedData, 0, 0); + + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData, uint preBufferSize, uint postBufferSize) + { + plaintext.Validate(); + additionalAuthenticatedData.Validate(); + + var size = checked((int)(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Count + TAG_SIZE_IN_BYTES)); + var outputSize = (int)(preBufferSize + size + postBufferSize); +#if NET + var refPooledBuffer = new RefPooledArrayBufferWriter(outputSize); + try + { + // arrays are pooled. and they MAY contain non-zeros in the pre-buffer and post-buffer regions. + // we could clean them up, but it's not strictly necessary - the important part is that output array + // has those pre/post buffer regions, which will be used by the caller. + refPooledBuffer.Advance(preBufferSize); + Encrypt(plaintext, additionalAuthenticatedData, ref refPooledBuffer); + refPooledBuffer.Advance(postBufferSize); + + var resultSpan = refPooledBuffer.WrittenSpan.ToArray(); + CryptoUtil.Assert(resultSpan.Length == outputSize, "writtenSpan length should equal calculated outputSize"); + return resultSpan; + } + finally + { + refPooledBuffer.Dispose(); + } +#else + var pooledArrayBuffer = new PooledArrayBufferWriter(outputSize); + try + { + pooledArrayBuffer.Advance((int)preBufferSize); + Encrypt(plaintext, additionalAuthenticatedData, ref pooledArrayBuffer); + pooledArrayBuffer.Advance((int)postBufferSize); + + var resultSpan = pooledArrayBuffer.WrittenSpan.ToArray(); + CryptoUtil.Assert(resultSpan.Length == outputSize, "writtenSpan length should equal calculated outputSize"); + return resultSpan; + } + finally + { + pooledArrayBuffer.Dispose(); + } +#endif + } + + private byte[] CreateContextHeader() + { + var retVal = new byte[checked( + 1 /* KDF alg */ + + 1 /* chaining mode */ + + sizeof(uint) /* sym alg key size */ + + sizeof(uint) /* GCM nonce size */ + + sizeof(uint) /* sym alg block size */ + + sizeof(uint) /* GCM tag size */ + + TAG_SIZE_IN_BYTES /* tag of GCM-encrypted empty string */)]; + fixed (byte* pbRetVal = retVal) { - // Calculate offsets - byte* pbKeyModifier = &pbRetVal[cbPreBuffer]; - byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; - byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES]; - byte* pbAuthTag = &pbEncryptedData[cbPlaintext]; + byte* ptr = pbRetVal; - // Randomly generate the key modifier and nonce - _genRandom.GenRandom(pbKeyModifier, KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES); + // First is the two-byte header + *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF + *(ptr++) = 1; // 0x01 = GCM encryption + authentication - // At this point, retVal := { preBuffer | keyModifier | nonce | _____ | _____ | postBuffer } + // Next is information about the symmetric algorithm (key size, nonce size, block size, tag size) + BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmSubkeyLengthInBytes); + BitHelpers.WriteTo(ref ptr, NONCE_SIZE_IN_BYTES); + BitHelpers.WriteTo(ref ptr, TAG_SIZE_IN_BYTES); // block size = tag size + BitHelpers.WriteTo(ref ptr, TAG_SIZE_IN_BYTES); - // Use the KDF to generate a new symmetric block cipher key - // We'll need a temporary buffer to hold the symmetric encryption subkey - byte* pbSymmetricEncryptionSubkey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)]; - try + // See the design document for an explanation of the following code. + var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes]; + fixed (byte* pbTempKeys = tempKeys) { - _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( - pbLabel: pbAdditionalAuthenticatedData, - cbLabel: cbAdditionalAuthenticatedData, - contextHeader: _contextHeader, - pbContext: pbKeyModifier, - cbContext: KEY_MODIFIER_SIZE_IN_BYTES, - pbDerivedKey: pbSymmetricEncryptionSubkey, - cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes); + byte dummy; - // Perform the encryption operation + // Derive temporary key for encryption. + using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider()) + { + provider.DeriveKey( + pbLabel: &dummy, + cbLabel: 0, + pbContext: &dummy, + cbContext: 0, + pbDerivedKey: pbTempKeys, + cbDerivedKey: (uint)tempKeys.Length); + } + + // Encrypt a zero-length input string with an all-zero nonce and copy the tag to the return buffer. + byte* pbNonce = stackalloc byte[(int)NONCE_SIZE_IN_BYTES]; + UnsafeBufferUtil.SecureZeroMemory(pbNonce, NONCE_SIZE_IN_BYTES); DoGcmEncrypt( - pbKey: pbSymmetricEncryptionSubkey, + pbKey: pbTempKeys, cbKey: _symmetricAlgorithmSubkeyLengthInBytes, pbNonce: pbNonce, - pbPlaintextData: pbPlaintext, - cbPlaintextData: cbPlaintext, - pbEncryptedData: pbEncryptedData, - pbTag: pbAuthTag); - - // At this point, retVal := { preBuffer | keyModifier | nonce | encryptedData | authenticationTag | postBuffer } - // And we're done! - return retVal; - } - finally - { - // The buffer contains key material, so delete it. - UnsafeBufferUtil.SecureZeroMemory(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes); + pbPlaintextData: &dummy, + cbPlaintextData: 0, + pbEncryptedData: &dummy, + pbTag: ptr); } + + ptr += TAG_SIZE_IN_BYTES; + CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length"); } + + // retVal := { version || chainingMode || symAlgKeySize || nonceSize || symAlgBlockSize || symAlgTagSize || TAG-of-E("") }. + return retVal; + } + + public void Dispose() + { + _sp800_108_ctr_hmac_provider.Dispose(); + + // We don't want to dispose of the underlying algorithm instances because they + // might be reused. } } diff --git a/src/DataProtection/DataProtection/src/Cng/Internal/CngAuthenticatedEncryptorBase.cs b/src/DataProtection/DataProtection/src/Cng/Internal/CngAuthenticatedEncryptorBase.cs deleted file mode 100644 index 3875f9e6c303..000000000000 --- a/src/DataProtection/DataProtection/src/Cng/Internal/CngAuthenticatedEncryptorBase.cs +++ /dev/null @@ -1,86 +0,0 @@ -// 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.AuthenticatedEncryption; - -namespace Microsoft.AspNetCore.DataProtection.Cng.Internal; - -/// -/// Base class used for all CNG-related authentication encryption operations. -/// -internal abstract unsafe class CngAuthenticatedEncryptorBase : IOptimizedAuthenticatedEncryptor, IDisposable -{ - public byte[] Decrypt(ArraySegment ciphertext, ArraySegment additionalAuthenticatedData) - { - // This wrapper simply converts ArraySegment to byte* and calls the impl method. - - // Input validation - ciphertext.Validate(); - additionalAuthenticatedData.Validate(); - - byte dummy; // used only if plaintext or AAD is empty, since otherwise 'fixed' returns null pointer - fixed (byte* pbCiphertextArray = ciphertext.Array) - { - fixed (byte* pbAdditionalAuthenticatedDataArray = additionalAuthenticatedData.Array) - { - try - { - return DecryptImpl( - pbCiphertext: (pbCiphertextArray != null) ? &pbCiphertextArray[ciphertext.Offset] : &dummy, - cbCiphertext: (uint)ciphertext.Count, - pbAdditionalAuthenticatedData: (pbAdditionalAuthenticatedDataArray != null) ? &pbAdditionalAuthenticatedDataArray[additionalAuthenticatedData.Offset] : &dummy, - cbAdditionalAuthenticatedData: (uint)additionalAuthenticatedData.Count); - } - catch (Exception ex) when (ex.RequiresHomogenization()) - { - // Homogenize to CryptographicException. - throw Error.CryptCommon_GenericError(ex); - } - } - } - } - - protected abstract byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData); - - public abstract void Dispose(); - - public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) - { - return Encrypt(plaintext, additionalAuthenticatedData, 0, 0); - } - - public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData, uint preBufferSize, uint postBufferSize) - { - // This wrapper simply converts ArraySegment to byte* and calls the impl method. - - // Input validation - plaintext.Validate(); - additionalAuthenticatedData.Validate(); - - byte dummy; // used only if plaintext or AAD is empty, since otherwise 'fixed' returns null pointer - fixed (byte* pbPlaintextArray = plaintext.Array) - { - fixed (byte* pbAdditionalAuthenticatedDataArray = additionalAuthenticatedData.Array) - { - try - { - return EncryptImpl( - pbPlaintext: (pbPlaintextArray != null) ? &pbPlaintextArray[plaintext.Offset] : &dummy, - cbPlaintext: (uint)plaintext.Count, - pbAdditionalAuthenticatedData: (pbAdditionalAuthenticatedDataArray != null) ? &pbAdditionalAuthenticatedDataArray[additionalAuthenticatedData.Offset] : &dummy, - cbAdditionalAuthenticatedData: (uint)additionalAuthenticatedData.Count, - cbPreBuffer: preBufferSize, - cbPostBuffer: postBufferSize); - } - catch (Exception ex) when (ex.RequiresHomogenization()) - { - // Homogenize to CryptographicException. - throw Error.CryptCommon_GenericError(ex); - } - } - } - } - - protected abstract byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer); -} diff --git a/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtectionProvider.cs b/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtectionProvider.cs index dd28c84db68d..7ecf458f620c 100644 --- a/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtectionProvider.cs +++ b/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtectionProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal; using Microsoft.AspNetCore.Shared; using Microsoft.Extensions.Logging; @@ -23,6 +24,19 @@ public IDataProtector CreateProtector(string purpose) { ArgumentNullThrowHelper.ThrowIfNull(purpose); + var currentKeyRing = _keyRingProvider.GetCurrentKeyRing(); + var encryptor = currentKeyRing.DefaultAuthenticatedEncryptor; + if (encryptor is ISpanAuthenticatedEncryptor) + { + // allows caller to check if dataProtector supports Span APIs + // and use more performant APIs + return new KeyRingBasedSpanDataProtector( + logger: _logger, + keyRingProvider: _keyRingProvider, + originalPurposes: null, + newPurpose: purpose); + } + return new KeyRingBasedDataProtector( logger: _logger, keyRingProvider: _keyRingProvider, diff --git a/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtector.cs b/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtector.cs index 9f19e137a48f..b27c74484344 100644 --- a/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtector.cs +++ b/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtector.cs @@ -21,18 +21,19 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement; -internal sealed unsafe class KeyRingBasedDataProtector : IDataProtector, IPersistedDataProtector +internal unsafe class KeyRingBasedDataProtector : IDataProtector, IPersistedDataProtector { // This magic header identifies a v0 protected data blob. It's the high 28 bits of the SHA1 hash of // "Microsoft.AspNet.DataProtection.KeyManagement.KeyRingBasedDataProtector" [US-ASCII], big-endian. // The last nibble reserved for version information. There's also the nice property that "F0 C9" // can never appear in a well-formed UTF8 sequence, so attempts to treat a protected payload as a // UTF8-encoded string will fail, and devs can catch the mistake early. - private const uint MAGIC_HEADER_V0 = 0x09F0C9F0; + protected const uint MAGIC_HEADER_V0 = 0x09F0C9F0; + protected static readonly int _magicHeaderKeyIdSize = sizeof(uint) + sizeof(Guid); - private AdditionalAuthenticatedDataTemplate _aadTemplate; - private readonly IKeyRingProvider _keyRingProvider; - private readonly ILogger? _logger; + protected AdditionalAuthenticatedDataTemplate _aadTemplate; + protected readonly IKeyRingProvider _keyRingProvider; + protected readonly ILogger? _logger; public KeyRingBasedDataProtector(IKeyRingProvider keyRingProvider, ILogger? logger, string[]? originalPurposes, string newPurpose) { @@ -65,6 +66,19 @@ public IDataProtector CreateProtector(string purpose) { ArgumentNullThrowHelper.ThrowIfNull(purpose); + var currentKeyRing = _keyRingProvider.GetCurrentKeyRing(); + var encryptor = currentKeyRing.DefaultAuthenticatedEncryptor; + if (encryptor is ISpanAuthenticatedEncryptor) + { + // allows caller to check if dataProtector supports Span APIs + // and use more performant APIs + return new KeyRingBasedSpanDataProtector( + logger: _logger, + keyRingProvider: _keyRingProvider, + originalPurposes: Purposes, + newPurpose: purpose); + } + return new KeyRingBasedDataProtector( logger: _logger, keyRingProvider: _keyRingProvider, @@ -72,24 +86,11 @@ public IDataProtector CreateProtector(string purpose) newPurpose: purpose); } - private static string JoinPurposesForLog(IEnumerable purposes) + protected static string JoinPurposesForLog(IEnumerable purposes) { return "(" + String.Join(", ", purposes.Select(p => "'" + p + "'")) + ")"; } - // allows decrypting payloads whose keys have been revoked - public byte[] DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors, out bool requiresMigration, out bool wasRevoked) - { - // argument & state checking - ArgumentNullThrowHelper.ThrowIfNull(protectedData); - - UnprotectStatus status; - var retVal = UnprotectCore(protectedData, ignoreRevocationErrors, status: out status); - requiresMigration = (status != UnprotectStatus.Ok); - wasRevoked = (status == UnprotectStatus.DecryptionKeyWasRevoked); - return retVal; - } - public byte[] Protect(byte[] plaintext) { ArgumentNullThrowHelper.ThrowIfNull(plaintext); @@ -140,7 +141,7 @@ public byte[] Protect(byte[] plaintext) } } - private static Guid ReadGuid(void* ptr) + protected static Guid ReadGuid(void* ptr) { #if NETCOREAPP // Performs appropriate endianness fixups @@ -153,15 +154,7 @@ private static Guid ReadGuid(void* ptr) #endif } - private static uint ReadBigEndian32BitInteger(byte* ptr) - { - return ((uint)ptr[0] << 24) - | ((uint)ptr[1] << 16) - | ((uint)ptr[2] << 8) - | ((uint)ptr[3]); - } - - private static bool TryGetVersionFromMagicHeader(uint magicHeader, out int version) + protected static bool TryGetVersionFromMagicHeader(uint magicHeader, out int version) { const uint MAGIC_HEADER_VERSION_MASK = 0xFU; if ((magicHeader & ~MAGIC_HEADER_VERSION_MASK) == MAGIC_HEADER_V0) @@ -187,14 +180,26 @@ public byte[] Unprotect(byte[] protectedData) wasRevoked: out _); } + // allows decrypting payloads whose keys have been revoked + public byte[] DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors, out bool requiresMigration, out bool wasRevoked) + { + // argument & state checking + ArgumentNullThrowHelper.ThrowIfNull(protectedData); + + UnprotectStatus status; + var retVal = UnprotectCore(protectedData, ignoreRevocationErrors, status: out status); + requiresMigration = (status != UnprotectStatus.Ok); + wasRevoked = (status == UnprotectStatus.DecryptionKeyWasRevoked); + return retVal; + } + private byte[] UnprotectCore(byte[] protectedData, bool allowOperationsOnRevokedKeys, out UnprotectStatus status) { Debug.Assert(protectedData != null); try { - // argument & state checking - if (protectedData.Length < sizeof(uint) /* magic header */ + sizeof(Guid) /* key id */) + if (protectedData.Length < _magicHeaderKeyIdSize) { // payload must contain at least the magic header and key id throw Error.ProtectionProvider_BadMagicHeader(); @@ -203,17 +208,15 @@ private byte[] UnprotectCore(byte[] protectedData, bool allowOperationsOnRevoked // Need to check that protectedData := { magicHeader || keyId || encryptorSpecificProtectedPayload } // Parse the payload version number and key id. - uint magicHeaderFromPayload; + var magicHeaderFromPayload = BinaryPrimitives.ReadUInt32BigEndian(protectedData.AsSpan(0, sizeof(uint))); Guid keyIdFromPayload; fixed (byte* pbInput = protectedData) { - magicHeaderFromPayload = ReadBigEndian32BitInteger(pbInput); keyIdFromPayload = ReadGuid(&pbInput[sizeof(uint)]); } // Are the magic header and version information correct? - int payloadVersion; - if (!TryGetVersionFromMagicHeader(magicHeaderFromPayload, out payloadVersion)) + if (!TryGetVersionFromMagicHeader(magicHeaderFromPayload, out var payloadVersion)) { throw Error.ProtectionProvider_BadMagicHeader(); } @@ -293,7 +296,7 @@ private byte[] UnprotectCore(byte[] protectedData, bool allowOperationsOnRevoked } } - private static void WriteGuid(void* ptr, Guid value) + protected static void WriteGuid(void* ptr, Guid value) { #if NETCOREAPP var span = new Span(ptr, sizeof(Guid)); @@ -309,7 +312,7 @@ private static void WriteGuid(void* ptr, Guid value) #endif } - private static void WriteBigEndianInteger(byte* ptr, uint value) + protected static void WriteBigEndianInteger(byte* ptr, uint value) { ptr[0] = (byte)(value >> 24); ptr[1] = (byte)(value >> 16); diff --git a/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedSpanDataProtector.cs b/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedSpanDataProtector.cs new file mode 100644 index 000000000000..fa6636a54fe6 --- /dev/null +++ b/src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedSpanDataProtector.cs @@ -0,0 +1,166 @@ +// 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.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using Microsoft.AspNetCore.Cryptography; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal; +using Microsoft.AspNetCore.Shared; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.DataProtection.KeyManagement; + +internal unsafe class KeyRingBasedSpanDataProtector : KeyRingBasedDataProtector, ISpanDataProtector +{ + public KeyRingBasedSpanDataProtector(IKeyRingProvider keyRingProvider, ILogger? logger, string[]? originalPurposes, string newPurpose) + : base(keyRingProvider, logger, originalPurposes, newPurpose) + { + } + + public void Protect(ReadOnlySpan plaintext, ref TWriter destination) where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif + { + try + { + // Perform the encryption operation using the current default encryptor. + var currentKeyRing = _keyRingProvider.GetCurrentKeyRing(); + var defaultKeyId = currentKeyRing.DefaultKeyId; + var defaultEncryptor = (ISpanAuthenticatedEncryptor)currentKeyRing.DefaultAuthenticatedEncryptor!; + CryptoUtil.Assert(defaultEncryptor != null, "DefaultAuthenticatedEncryptor != null"); + + if (_logger.IsDebugLevelEnabled()) + { + _logger.PerformingProtectOperationToKeyWithPurposes(defaultKeyId, JoinPurposesForLog(Purposes)); + } + + // We'll need to apply the default key id to the template if it hasn't already been applied. + // If the default key id has been updated since the last call to Protect, also write back the updated template. + var aad = _aadTemplate.GetAadForKey(defaultKeyId, isProtecting: true); + var preBufferSize = _magicHeaderKeyIdSize; + + // Step 1: Write the magic header and key id + var headerBuffer = destination.GetSpan(preBufferSize); +#if NET + BinaryPrimitives.WriteUInt32BigEndian(headerBuffer.Slice(0, sizeof(uint)), MAGIC_HEADER_V0); + var writeKeyIdResult = defaultKeyId.TryWriteBytes(headerBuffer.Slice(sizeof(uint), sizeof(Guid))); + Debug.Assert(writeKeyIdResult, "Failed to write Guid to destination."); +#else + fixed (byte* pbBuffer = headerBuffer) + { + WriteBigEndianInteger(pbBuffer, MAGIC_HEADER_V0); + WriteGuid(&pbBuffer[sizeof(uint)], defaultKeyId); + } +#endif + destination.Advance(preBufferSize); + + // Step 2: Perform encryption into the destination writer + defaultEncryptor.Encrypt(plaintext, aad, ref destination); + + // At this point, destination := { magicHeader || keyId || encryptorSpecificProtectedPayload } + // And we're done! + } + catch (Exception ex) when (ex.RequiresHomogenization()) + { + // homogenize all errors to CryptographicException + throw Error.Common_EncryptionFailed(ex); + } + } + + public void Unprotect(ReadOnlySpan protectedData, ref TWriter destination) where TWriter : IBufferWriter +#if NET + , allows ref struct +#endif + { + try + { + if (protectedData.Length < _magicHeaderKeyIdSize) + { + // payload must contain at least the magic header and key id + throw Error.ProtectionProvider_BadMagicHeader(); + } + + // Parse the payload version number and key id. + var magicHeaderFromPayload = BinaryPrimitives.ReadUInt32BigEndian(protectedData.Slice(0, sizeof(uint))); +#if NET + var keyIdFromPayload = new Guid(protectedData.Slice(sizeof(uint), sizeof(Guid))); +#else + Guid keyIdFromPayload; + fixed (byte* pbProtectedData = protectedData) + { + keyIdFromPayload = ReadGuid(&pbProtectedData[sizeof(uint)]); + } +#endif + + // Are the magic header and version information correct? + if (!TryGetVersionFromMagicHeader(magicHeaderFromPayload, out var payloadVersion)) + { + throw Error.ProtectionProvider_BadMagicHeader(); + } + + if (payloadVersion != 0) + { + throw Error.ProtectionProvider_BadVersion(); + } + + if (_logger.IsDebugLevelEnabled()) + { + _logger.PerformingUnprotectOperationToKeyWithPurposes(keyIdFromPayload, JoinPurposesForLog(Purposes)); + } + + // Find the correct encryptor in the keyring. + var currentKeyRing = _keyRingProvider.GetCurrentKeyRing(); + var requestedEncryptor = currentKeyRing.GetAuthenticatedEncryptorByKeyId(keyIdFromPayload, out bool keyWasRevoked); + if (requestedEncryptor is null) + { + if (_keyRingProvider is KeyRingProvider provider && provider.InAutoRefreshWindow()) + { + currentKeyRing = provider.RefreshCurrentKeyRing(); + requestedEncryptor = currentKeyRing.GetAuthenticatedEncryptorByKeyId(keyIdFromPayload, out keyWasRevoked); + } + + if (requestedEncryptor is null) + { + if (_logger.IsTraceLevelEnabled()) + { + _logger.KeyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed(keyIdFromPayload); + } + + throw Error.Common_KeyNotFound(keyIdFromPayload); + } + } + + // Check if key was revoked - for simplified version, we disallow revoked keys + if (keyWasRevoked) + { + if (_logger.IsDebugLevelEnabled()) + { + _logger.KeyWasRevokedUnprotectOperationCannotProceed(keyIdFromPayload); + } + + throw Error.Common_KeyRevoked(keyIdFromPayload); + } + + // Perform the decryption operation. + ReadOnlySpan actualCiphertext = protectedData.Slice(sizeof(uint) + sizeof(Guid)); // chop off magic header + key id + ReadOnlySpan aad = _aadTemplate.GetAadForKey(keyIdFromPayload, isProtecting: false); + + // At this point, actualCiphertext := { encryptorSpecificPayload }, + // so all that's left is to invoke the decryption routine directly. + var spanEncryptor = (ISpanAuthenticatedEncryptor)requestedEncryptor; + spanEncryptor.Decrypt(actualCiphertext, aad, ref destination); + + // At this point, destination contains the decrypted plaintext + // And we're done! + } + catch (Exception ex) when (ex.RequiresHomogenization()) + { + // homogenize all errors to CryptographicException + throw Error.DecryptionFailed(ex); + } + } +} diff --git a/src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs b/src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs index 413adfb4825f..ea3678c6932e 100644 --- a/src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs +++ b/src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs @@ -3,6 +3,7 @@ #if NETCOREAPP using System; +using System.Buffers; using System.IO; using System.Security.Cryptography; using Microsoft.AspNetCore.Cryptography; @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.DataProtection.Managed; // An encryptor that uses AesGcm to do encryption -internal sealed unsafe class AesGcmAuthenticatedEncryptor : IOptimizedAuthenticatedEncryptor, IDisposable +internal sealed unsafe class AesGcmAuthenticatedEncryptor : IOptimizedAuthenticatedEncryptor, ISpanAuthenticatedEncryptor, IDisposable { // Having a key modifier ensures with overwhelming probability that no two encryption operations // will ever derive the same (encryption subkey, MAC subkey) pair. This limits an attacker's @@ -64,73 +65,73 @@ public AesGcmAuthenticatedEncryptor(ISecret keyDerivationKey, int derivedKeySize _genRandom = genRandom ?? ManagedGenRandomImpl.Instance; } - public byte[] Decrypt(ArraySegment ciphertext, ArraySegment additionalAuthenticatedData) + public void Decrypt(ReadOnlySpan ciphertext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) + where TWriter : IBufferWriter, allows ref struct { - ciphertext.Validate(); - additionalAuthenticatedData.Validate(); - - // Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag - if (ciphertext.Count < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) + try { - throw Error.CryptCommon_PayloadInvalid(); - } + // Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag + if (ciphertext.Length < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) + { + throw Error.CryptCommon_PayloadInvalid(); + } - // Assumption: pbCipherText := { keyModifier || nonce || encryptedData || authenticationTag } - var plaintextBytes = ciphertext.Count - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES); - var plaintext = new byte[plaintextBytes]; + var plaintextBytes = ciphertext.Length - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES); - try - { - // Step 1: Extract the key modifier from the payload. + // Calculate offsets in the ciphertext + var keyModifierOffset = 0; + var nonceOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES; + var encryptedDataOffset = nonceOffset + NONCE_SIZE_IN_BYTES; + var tagOffset = encryptedDataOffset + plaintextBytes; - int keyModifierOffset; // position in ciphertext.Array where key modifier begins - int nonceOffset; // position in ciphertext.Array where key modifier ends / nonce begins - int encryptedDataOffset; // position in ciphertext.Array where nonce ends / encryptedData begins - int tagOffset; // position in ciphertext.Array where encrypted data ends + // Extract spans for each component + var keyModifier = ciphertext.Slice(keyModifierOffset, KEY_MODIFIER_SIZE_IN_BYTES); + var nonce = ciphertext.Slice(nonceOffset, NONCE_SIZE_IN_BYTES); + var encrypted = ciphertext.Slice(encryptedDataOffset, plaintextBytes); + var tag = ciphertext.Slice(tagOffset, TAG_SIZE_IN_BYTES); - checked - { - keyModifierOffset = ciphertext.Offset; - nonceOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES; - encryptedDataOffset = nonceOffset + NONCE_SIZE_IN_BYTES; - tagOffset = encryptedDataOffset + plaintextBytes; - } + // Get buffer from writer with the plaintext size + var buffer = destination.GetSpan(plaintextBytes); - var keyModifier = new ArraySegment(ciphertext.Array!, keyModifierOffset, KEY_MODIFIER_SIZE_IN_BYTES); + // Get the plaintext destination + var plaintext = buffer.Slice(0, plaintextBytes); - // 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. + // Decrypt the KDK and use it to restore the original encryption key + // We pin all unencrypted keys to limit their exposure via GC relocation + Span decryptedKdk = _keyDerivationKey.Length <= 256 + ? stackalloc byte[256].Slice(0, _keyDerivationKey.Length) + : new byte[_keyDerivationKey.Length]; - var decryptedKdk = new byte[_keyDerivationKey.Length]; - var derivedKey = new byte[_derivedkeySizeInBytes]; + Span derivedKey = _derivedkeySizeInBytes <= 256 + ? stackalloc byte[256].Slice(0, _derivedkeySizeInBytes) + : new byte[_derivedkeySizeInBytes]; - fixed (byte* __unused__1 = decryptedKdk) - fixed (byte* __unused__2 = derivedKey) + fixed (byte* decryptedKdkUnsafe = decryptedKdk) + fixed (byte* derivedKeyUnsafe = derivedKey) { try { - _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment(decryptedKdk)); + _keyDerivationKey.WriteSecretIntoBuffer(decryptedKdkUnsafe, decryptedKdk.Length); ManagedSP800_108_CTR_HMACSHA512.DeriveKeys( kdk: decryptedKdk, label: additionalAuthenticatedData, contextHeader: _contextHeader, contextData: keyModifier, operationSubkey: derivedKey, - validationSubkey: Span.Empty /* filling in derivedKey only */ ); + validationSubkey: Span.Empty /* filling in derivedKey only */); - // Perform the decryption operation - var nonce = new Span(ciphertext.Array, nonceOffset, NONCE_SIZE_IN_BYTES); - var tag = new Span(ciphertext.Array, tagOffset, TAG_SIZE_IN_BYTES); - var encrypted = new Span(ciphertext.Array, encryptedDataOffset, plaintextBytes); + // Perform the decryption operation directly into destination using var aes = new AesGcm(derivedKey, TAG_SIZE_IN_BYTES); aes.Decrypt(nonce, encrypted, tag, plaintext); - return plaintext; + + // Advance the writer by the number of bytes written + destination.Advance(plaintextBytes); } finally { // delete since these contain secret material - Array.Clear(decryptedKdk, 0, decryptedKdk.Length); - Array.Clear(derivedKey, 0, derivedKey.Length); + decryptedKdk.Clear(); + derivedKey.Clear(); } } } @@ -141,72 +142,122 @@ public byte[] Decrypt(ArraySegment ciphertext, ArraySegment addition } } + public byte[] Decrypt(ArraySegment ciphertext, ArraySegment additionalAuthenticatedData) + { + ciphertext.Validate(); + additionalAuthenticatedData.Validate(); + + var outputSize = ciphertext.Count - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES); + if (outputSize < 0) + { + throw Error.CryptCommon_PayloadInvalid(); + } + + var refPooledBuffer = new RefPooledArrayBufferWriter(outputSize); + try + { + Decrypt(ciphertext, additionalAuthenticatedData, ref refPooledBuffer); + return refPooledBuffer.WrittenSpan.ToArray(); + } + finally + { + refPooledBuffer.Dispose(); + } + } + + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) + => Encrypt(plaintext, additionalAuthenticatedData, 0, 0); + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData, uint preBufferSize, uint postBufferSize) { plaintext.Validate(); additionalAuthenticatedData.Validate(); + var size = checked(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Count + TAG_SIZE_IN_BYTES); + var outputSize = (int)(preBufferSize + size + postBufferSize); + var refPooledBuffer = new RefPooledArrayBufferWriter(outputSize); try { - // Allocate a buffer to hold the key modifier, nonce, encrypted data, and tag. - // In GCM, the encrypted output will be the same length as the plaintext input. - var retVal = new byte[checked(preBufferSize + KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Count + TAG_SIZE_IN_BYTES + postBufferSize)]; - int keyModifierOffset; // position in ciphertext.Array where key modifier begins - int nonceOffset; // position in ciphertext.Array where key modifier ends / nonce begins - int encryptedDataOffset; // position in ciphertext.Array where nonce ends / encryptedData begins - int tagOffset; // position in ciphertext.Array where encrypted data ends - - checked - { - keyModifierOffset = plaintext.Offset + (int)preBufferSize; - nonceOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES; - encryptedDataOffset = nonceOffset + NONCE_SIZE_IN_BYTES; - tagOffset = encryptedDataOffset + plaintext.Count; - } + // arrays are pooled. and they MAY contain non-zeros in the pre-buffer and post-buffer regions. + // we could clean them up, but it's not strictly necessary - the important part is that output array + // has those pre/post buffer regions, which will be used by the caller. + refPooledBuffer.Advance(preBufferSize); + Encrypt(plaintext, additionalAuthenticatedData, ref refPooledBuffer); + refPooledBuffer.Advance(postBufferSize); + + CryptoUtil.Assert(refPooledBuffer.WrittenSpan.Length == outputSize, "writtenSpan length should equal calculated outputSize"); + return refPooledBuffer.WrittenSpan.ToArray(); + } + finally + { + refPooledBuffer.Dispose(); + } + } - // Randomly generate the key modifier and nonce + public void Encrypt(ReadOnlySpan plaintext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) + where TWriter : IBufferWriter, allows ref struct + { + try + { + // Calculate total required size: keyModifier + nonce + plaintext + tag + // In GCM, ciphertext length equals plaintext length + var totalRequiredSize = checked(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Length + TAG_SIZE_IN_BYTES); + + // Get buffer from writer with the required total size + var buffer = destination.GetSpan(totalRequiredSize); + + // Generate random key modifier and nonce var keyModifier = _genRandom.GenRandom(KEY_MODIFIER_SIZE_IN_BYTES); var nonceBytes = _genRandom.GenRandom(NONCE_SIZE_IN_BYTES); - Buffer.BlockCopy(keyModifier, 0, retVal, (int)preBufferSize, keyModifier.Length); - Buffer.BlockCopy(nonceBytes, 0, retVal, (int)preBufferSize + keyModifier.Length, nonceBytes.Length); + // Copy keyModifier and nonce to buffer + keyModifier.CopyTo(buffer.Slice(0, KEY_MODIFIER_SIZE_IN_BYTES)); + nonceBytes.CopyTo(buffer.Slice(KEY_MODIFIER_SIZE_IN_BYTES, NONCE_SIZE_IN_BYTES)); - // At this point, retVal := { preBuffer | keyModifier | nonce | _____ | _____ | postBuffer } + // At this point, buffer := { keyModifier | nonce | _____ | _____ } // Use the KDF to generate a new symmetric block cipher key // We'll need a temporary buffer to hold the symmetric encryption subkey - var decryptedKdk = new byte[_keyDerivationKey.Length]; - var derivedKey = new byte[_derivedkeySizeInBytes]; - fixed (byte* __unused__1 = decryptedKdk) + Span decryptedKdk = _keyDerivationKey.Length <= 256 + ? stackalloc byte[256].Slice(0, _keyDerivationKey.Length) + : new byte[_keyDerivationKey.Length]; + + Span derivedKey = _derivedkeySizeInBytes <= 256 + ? stackalloc byte[256].Slice(0, _derivedkeySizeInBytes) + : new byte[_derivedkeySizeInBytes]; + + fixed (byte* decryptedKdkUnsafe = decryptedKdk) fixed (byte* __unused__2 = derivedKey) { try { - _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment(decryptedKdk)); + _keyDerivationKey.WriteSecretIntoBuffer(decryptedKdkUnsafe, decryptedKdk.Length); ManagedSP800_108_CTR_HMACSHA512.DeriveKeys( kdk: decryptedKdk, label: additionalAuthenticatedData, contextHeader: _contextHeader, contextData: keyModifier, operationSubkey: derivedKey, - validationSubkey: Span.Empty /* filling in derivedKey only */ ); + validationSubkey: Span.Empty /* filling in derivedKey only */); + + // Perform GCM encryption. Buffer expected structure: + // { keyModifier | nonce | encryptedData | authenticationTag } + var nonce = buffer.Slice(KEY_MODIFIER_SIZE_IN_BYTES, NONCE_SIZE_IN_BYTES); + var encrypted = buffer.Slice(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES, plaintext.Length); + var tag = buffer.Slice(KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Length, TAG_SIZE_IN_BYTES); - // do gcm - var nonce = new Span(retVal, nonceOffset, NONCE_SIZE_IN_BYTES); - var tag = new Span(retVal, tagOffset, TAG_SIZE_IN_BYTES); - var encrypted = new Span(retVal, encryptedDataOffset, plaintext.Count); using var aes = new AesGcm(derivedKey, TAG_SIZE_IN_BYTES); aes.Encrypt(nonce, plaintext, encrypted, tag); - // At this point, retVal := { preBuffer | keyModifier | nonce | encryptedData | authenticationTag | postBuffer } + // At this point, buffer := { keyModifier | nonce | encryptedData | authenticationTag } // And we're done! - return retVal; + destination.Advance(totalRequiredSize); } finally { // delete since these contain secret material - Array.Clear(decryptedKdk, 0, decryptedKdk.Length); - Array.Clear(derivedKey, 0, derivedKey.Length); + decryptedKdk.Clear(); + derivedKey.Clear(); } } } @@ -216,10 +267,6 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona throw Error.CryptCommon_GenericError(ex); } } - - public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) - => Encrypt(plaintext, additionalAuthenticatedData, 0, 0); - public void Dispose() { _keyDerivationKey.Dispose(); diff --git a/src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs b/src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs index e11e3862b53e..b994e554754e 100644 --- a/src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs +++ b/src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Diagnostics; +using System.Drawing; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -18,6 +19,9 @@ namespace Microsoft.AspNetCore.DataProtection.Managed; // The payloads produced by this encryptor should be compatible with the payloads // produced by the CNG-based Encrypt(CBC) + HMAC authenticated encryptor. internal sealed unsafe class ManagedAuthenticatedEncryptor : IAuthenticatedEncryptor, IDisposable +#if NET + , ISpanAuthenticatedEncryptor +#endif { // Even when IVs are chosen randomly, CBC is susceptible to IV collisions within a single // key. For a 64-bit block cipher (like 3DES), we'd expect a collision after 2^32 block @@ -69,158 +73,148 @@ public ManagedAuthenticatedEncryptor(Secret keyDerivationKey, Func(ReadOnlySpan ciphertext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) + where TWriter : IBufferWriter, allows ref struct { - var EMPTY_ARRAY = Array.Empty(); - var EMPTY_ARRAY_SEGMENT = new ArraySegment(EMPTY_ARRAY); + try + { + // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC + if (ciphertext.Length < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes)) + { + throw Error.CryptCommon_PayloadInvalid(); + } - var retVal = new byte[checked( - 1 /* KDF alg */ - + 1 /* chaining mode */ - + sizeof(uint) /* sym alg key size */ - + sizeof(uint) /* sym alg block size */ - + sizeof(uint) /* hmac alg key size */ - + sizeof(uint) /* hmac alg digest size */ - + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */ - + _validationAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)]; + // Calculate the maximum possible plaintext size + var estimatedDecryptedSize = ciphertext.Length - (KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes); - var idx = 0; + // Get buffer from writer with the estimated plaintext size + var buffer = destination.GetSpan(estimatedDecryptedSize); - // First is the two-byte header - retVal[idx++] = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF - retVal[idx++] = 0; // 0x00 = CBC encryption + HMAC authentication + // Calculate offsets in the ciphertext + var keyModifierOffset = 0; + var ivOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES; + var ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes; + var macOffset = ciphertext.Length - _validationAlgorithmDigestLengthInBytes; - // Next is information about the symmetric algorithm (key size followed by block size) - BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmSubkeyLengthInBytes); - BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmBlockSizeInBytes); + // Extract spans for each component + var keyModifier = ciphertext.Slice(keyModifierOffset, KEY_MODIFIER_SIZE_IN_BYTES); - // Next is information about the keyed hash algorithm (key size followed by digest size) - BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmSubkeyLengthInBytes); - BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmDigestLengthInBytes); + // Decrypt the KDK and use it to restore the original encryption and MAC keys + Span decryptedKdk = _keyDerivationKey.Length <= 256 + ? stackalloc byte[256].Slice(0, _keyDerivationKey.Length) + : new byte[_keyDerivationKey.Length]; - // See the design document for an explanation of the following code. - var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _validationAlgorithmSubkeyLengthInBytes]; - ManagedSP800_108_CTR_HMACSHA512.DeriveKeys( - kdk: EMPTY_ARRAY, - label: EMPTY_ARRAY_SEGMENT, - contextHeader: EMPTY_ARRAY_SEGMENT, - contextData: EMPTY_ARRAY_SEGMENT, - operationSubkey: tempKeys.AsSpan(0, _symmetricAlgorithmSubkeyLengthInBytes), - validationSubkey: tempKeys.AsSpan(_symmetricAlgorithmSubkeyLengthInBytes, _validationAlgorithmSubkeyLengthInBytes)); + byte[]? validationSubkeyArray = null; + var validationSubkey = _validationAlgorithmSubkeyLengthInBytes <= 128 + ? stackalloc byte[128].Slice(0, _validationAlgorithmSubkeyLengthInBytes) + : (validationSubkeyArray = new byte[_validationAlgorithmSubkeyLengthInBytes]); - // At this point, tempKeys := { K_E || K_H }. + Span decryptionSubkey = _symmetricAlgorithmSubkeyLengthInBytes <= 128 + ? stackalloc byte[128].Slice(0, _symmetricAlgorithmSubkeyLengthInBytes) + : new byte[_symmetricAlgorithmSubkeyLengthInBytes]; - // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer. - using (var symmetricAlg = CreateSymmetricAlgorithm()) - { - using (var cryptoTransform = symmetricAlg.CreateEncryptor( - rgbKey: new ArraySegment(tempKeys, 0, _symmetricAlgorithmSubkeyLengthInBytes).AsStandaloneArray(), - rgbIV: new byte[_symmetricAlgorithmBlockSizeInBytes])) + fixed (byte* decryptedKdkUnsafe = decryptedKdk) + fixed (byte* __unused__2 = decryptionSubkey) + fixed (byte* __unused__3 = validationSubkeyArray) { - var ciphertext = cryptoTransform.TransformFinalBlock(EMPTY_ARRAY, 0, 0); - CryptoUtil.Assert(ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes, "ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes"); - Buffer.BlockCopy(ciphertext, 0, retVal, idx, ciphertext.Length); - } - } - - idx += _symmetricAlgorithmBlockSizeInBytes; - - // MAC a zero-length input string and copy the digest to the return buffer. - using (var hashAlg = CreateValidationAlgorithm(new ArraySegment(tempKeys, _symmetricAlgorithmSubkeyLengthInBytes, _validationAlgorithmSubkeyLengthInBytes).AsStandaloneArray())) - { - var digest = hashAlg.ComputeHash(EMPTY_ARRAY); - CryptoUtil.Assert(digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes, "digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes"); - Buffer.BlockCopy(digest, 0, retVal, idx, digest.Length); - } - - idx += _validationAlgorithmDigestLengthInBytes; - CryptoUtil.Assert(idx == retVal.Length, "idx == retVal.Length"); + try + { + _keyDerivationKey.WriteSecretIntoBuffer(decryptedKdkUnsafe, decryptedKdk.Length); + ManagedSP800_108_CTR_HMACSHA512.DeriveKeys( + kdk: decryptedKdk, + label: additionalAuthenticatedData, + contextHeader: _contextHeader, + contextData: keyModifier, + operationSubkey: decryptionSubkey, + validationSubkey: validationSubkey); - // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || macAlgKeySize || macAlgDigestSize || E("") || MAC("") }. - return retVal; - } + // Validate the MAC provided as part of the payload + var ivAndCiphertextSpan = ciphertext.Slice(ivOffset, macOffset - ivOffset); + var providedMac = ciphertext.Slice(macOffset, _validationAlgorithmDigestLengthInBytes); - private SymmetricAlgorithm CreateSymmetricAlgorithm() - { - var retVal = _symmetricAlgorithmFactory(); - CryptoUtil.Assert(retVal != null, "retVal != null"); + if (!ValidateMac(ivAndCiphertextSpan, providedMac, validationSubkey, validationSubkeyArray)) + { + throw Error.CryptCommon_PayloadInvalid(); + } - retVal.Mode = CipherMode.CBC; - retVal.Padding = PaddingMode.PKCS7; + // If the integrity check succeeded, decrypt the payload directly into destination + var ciphertextSpan = ciphertext.Slice(ciphertextOffset, macOffset - ciphertextOffset); + var iv = ciphertext.Slice(ivOffset, _symmetricAlgorithmBlockSizeInBytes); - return retVal; - } + using var symmetricAlgorithm = CreateSymmetricAlgorithm(); + symmetricAlgorithm.SetKey(decryptionSubkey); - private KeyedHashAlgorithm CreateValidationAlgorithm(byte[]? key = null) - { - var retVal = _validationAlgorithmFactory(); - CryptoUtil.Assert(retVal != null, "retVal != null"); + // Decrypt directly into destination buffer + var actualDecryptedBytes = symmetricAlgorithm.DecryptCbc(ciphertextSpan, iv, buffer); - if (key is not null) + // Advance the writer by the actual number of bytes written + destination.Advance(actualDecryptedBytes); + } + finally + { + // delete since these contain secret material + validationSubkey.Clear(); + decryptedKdk.Clear(); + decryptionSubkey.Clear(); + } + } + } + catch (Exception ex) when (ex.RequiresHomogenization()) { - retVal.Key = key; + // Homogenize all exceptions to CryptographicException. + throw Error.CryptCommon_GenericError(ex); } - return retVal; } - public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment additionalAuthenticatedData) + public void Encrypt(ReadOnlySpan plaintext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) + where TWriter : IBufferWriter, allows ref struct { - // Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) } - protectedPayload.Validate(); - additionalAuthenticatedData.Validate(); - - // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC - if (protectedPayload.Count < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes)) - { - throw Error.CryptCommon_PayloadInvalid(); - } - 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 - int macOffset; // position in protectedPayload.Array where ciphertext ends / MAC begins - int eofOffset; // position in protectedPayload.Array where MAC ends + var keyModifierLength = KEY_MODIFIER_SIZE_IN_BYTES; + var ivLength = _symmetricAlgorithmBlockSizeInBytes; - checked - { - keyModifierOffset = protectedPayload.Offset; - ivOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES; - ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes; - } + // Calculate the required total size upfront + using var symmetricAlgorithmForSizing = CreateSymmetricAlgorithm(); + var cipherTextLength = symmetricAlgorithmForSizing.GetCiphertextLengthCbc(plaintext.Length); + var macLength = _validationAlgorithmDigestLengthInBytes; + var totalRequiredSize = keyModifierLength + ivLength + cipherTextLength + macLength; - ReadOnlySpan keyModifier = protectedPayload.Array!.AsSpan(keyModifierOffset, ivOffset - keyModifierOffset); + // Get buffer from writer with the required total size + var buffer = destination.GetSpan(totalRequiredSize); - // Step 2: Decrypt the KDK and use it to restore the original encryption and MAC keys. -#if NET10_0_OR_GREATER + // Generate random key modifier and IV + Span keyModifier = keyModifierLength <= 128 + ? stackalloc byte[128].Slice(0, keyModifierLength) + : new byte[keyModifierLength]; + + _genRandom.GenRandom(keyModifier); + + // Copy key modifier to buffer + keyModifier.CopyTo(buffer.Slice(0, keyModifierLength)); + + // Generate IV directly into buffer + var iv = buffer.Slice(keyModifierLength, ivLength); + _genRandom.GenRandom(iv); + + // Use the KDF to generate a new symmetric block cipher key Span decryptedKdk = _keyDerivationKey.Length <= 256 ? stackalloc byte[256].Slice(0, _keyDerivationKey.Length) : new byte[_keyDerivationKey.Length]; -#else - var decryptedKdk = new byte[_keyDerivationKey.Length]; -#endif byte[]? validationSubkeyArray = null; - var validationSubkey = _validationAlgorithmSubkeyLengthInBytes <= 128 + Span validationSubkey = _validationAlgorithmSubkeyLengthInBytes <= 128 ? stackalloc byte[128].Slice(0, _validationAlgorithmSubkeyLengthInBytes) : (validationSubkeyArray = new byte[_validationAlgorithmSubkeyLengthInBytes]); -#if NET10_0_OR_GREATER - Span decryptionSubkey = - _symmetricAlgorithmSubkeyLengthInBytes <= 128 + Span encryptionSubkey = _symmetricAlgorithmSubkeyLengthInBytes <= 128 ? stackalloc byte[128].Slice(0, _symmetricAlgorithmSubkeyLengthInBytes) - : new byte[_symmetricAlgorithmBlockSizeInBytes]; -#else - byte[] decryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes]; -#endif + : new byte[_symmetricAlgorithmSubkeyLengthInBytes]; - // 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 = validationSubkeyArray) + fixed (byte* __unused__1 = encryptionSubkey) + fixed (byte* __unused__2 = validationSubkeyArray) { try { @@ -230,60 +224,49 @@ public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment ad label: additionalAuthenticatedData, contextHeader: _contextHeader, contextData: keyModifier, - operationSubkey: decryptionSubkey, + operationSubkey: encryptionSubkey, validationSubkey: validationSubkey); - // Step 3: Calculate the correct MAC for this payload. - // correctHash := MAC(IV || ciphertext) - checked - { - eofOffset = protectedPayload.Offset + protectedPayload.Count; - macOffset = eofOffset - _validationAlgorithmDigestLengthInBytes; - } - - // Step 4: Validate the MAC provided as part of the payload. - 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 + symmetricAlgorithm.SetKey(encryptionSubkey); - // 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); + using var validationAlgorithm = CreateValidationAlgorithm(); - // symmetricAlgorithm is created with CBC mode (see CreateSymmetricAlgorithm()) - return symmetricAlgorithm.DecryptCbc(ciphertext, iv); -#else - var iv = protectedPayload.Array!.AsSpan(ivOffset, _symmetricAlgorithmBlockSizeInBytes).ToArray(); + // Perform the encryption operation + var ciphertextDestination = buffer.Slice(keyModifierLength + ivLength, cipherTextLength); + symmetricAlgorithm.EncryptCbc(plaintext, iv, ciphertextDestination); - using (var symmetricAlgorithm = CreateSymmetricAlgorithm()) - using (var cryptoTransform = symmetricAlgorithm.CreateDecryptor(decryptionSubkey, iv)) + // Calculate the digest over the IV and ciphertext + var ivAndCipherTextSpan = buffer.Slice(keyModifierLength, ivLength + cipherTextLength); + var macDestinationSpan = buffer.Slice(keyModifierLength + ivLength + cipherTextLength, macLength); + + // Use optimized method for specific algorithms when possible + if (validationAlgorithm is HMACSHA256) { - var outputStream = new MemoryStream(); - using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write)) + 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(); + if (!validationAlgorithm.TryComputeHash(source: ivAndCipherTextSpan, destination: macDestinationSpan, out _)) { - cryptoStream.Write(protectedPayload.Array!, ciphertextOffset, macOffset - ciphertextOffset); - cryptoStream.FlushFinalBlock(); - - // At this point, outputStream := { plaintext }, and we're done! - return outputStream.ToArray(); + throw Error.CryptCommon_GenericError(new InvalidOperationException("Failed to compute HMAC")); } } -#endif + + // Advance the writer by the total bytes written + destination.Advance(totalRequiredSize); } finally { - // delete since these contain secret material - validationSubkey.Clear(); - -#if NET10_0_OR_GREATER + keyModifier.Clear(); decryptedKdk.Clear(); - decryptionSubkey.Clear(); -#else - Array.Clear(decryptedKdk, 0, decryptedKdk.Length); -#endif + validationSubkey.Clear(); + encryptionSubkey.Clear(); } } } @@ -293,6 +276,7 @@ public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment ad throw Error.CryptCommon_GenericError(ex); } } +#endif public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) { @@ -300,36 +284,32 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona additionalAuthenticatedData.Validate(); var plainTextSpan = plaintext.AsSpan(); +#if NET + var symmetricAlgorithm = CreateSymmetricAlgorithm(); + var cipherTextLength = symmetricAlgorithm.GetCiphertextLengthCbc(plaintext.Count); + var outputSize = KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes /* IV */ + cipherTextLength + _validationAlgorithmDigestLengthInBytes /* MAC */; + + var refPooledBuffer = new RefPooledArrayBufferWriter(outputSize); + try + { + Encrypt(plaintext, additionalAuthenticatedData, ref refPooledBuffer); + CryptoUtil.Assert(refPooledBuffer.WrittenSpan.Length == outputSize, "bytesWritten == size"); + return refPooledBuffer.WrittenSpan.ToArray(); + } + finally + { + refPooledBuffer.Dispose(); + } +#else try { 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]; -#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) @@ -337,15 +317,7 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona { // 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 { @@ -359,78 +331,15 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona 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)); - - // At this point, outputStream := { keyModifier || IV || ciphertext } - - // 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)) @@ -457,17 +366,11 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona return outputStream.ToArray(); } } -#endif } finally { -#if NET10_0_OR_GREATER - keyModifier.Clear(); - decryptedKdk.Clear(); -#else Array.Clear(keyModifier, 0, keyModifierLength); Array.Clear(decryptedKdk, 0, decryptedKdk.Length); -#endif } } } @@ -476,13 +379,11 @@ public byte[] Encrypt(ArraySegment plaintext, ArraySegment additiona // Homogenize all exceptions to CryptographicException. throw Error.CryptCommon_GenericError(ex); } +#endif } - private void CalculateAndValidateMac( - byte[] payloadArray, - int ivOffset, int macOffset, int eofOffset, // offsets to slice the payload array - ReadOnlySpan validationSubkey, - byte[]? validationSubkeyArray) +#if NET + private bool ValidateMac(ReadOnlySpan dataToValidate, ReadOnlySpan providedMac, ReadOnlySpan validationSubkey, byte[]? validationSubkeyArray) { using var validationAlgorithm = CreateValidationAlgorithm(); var hashSize = validationAlgorithm.GetDigestSizeInBytes(); @@ -494,32 +395,51 @@ private void CalculateAndValidateMac( 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); + bytesWritten = HMACSHA256.HashData(key: validationSubkey, source: dataToValidate, destination: correctHash); } else if (validationAlgorithm is HMACSHA512) { - bytesWritten = HMACSHA512.HashData(key: validationSubkey, source: hashSource, destination: correctHash); + bytesWritten = HMACSHA512.HashData(key: validationSubkey, source: dataToValidate, 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); + var success = validationAlgorithm.TryComputeHash(dataToValidate, correctHash, out bytesWritten); Debug.Assert(success); } - Debug.Assert(bytesWritten == hashSize); + + return CryptoUtil.TimeConstantBuffersAreEqual(correctHash, providedMac); + } + finally + { + correctHash.Clear(); + } + } #else + 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 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); @@ -533,6 +453,219 @@ private void CalculateAndValidateMac( correctHash.Clear(); } } +#endif + + public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment additionalAuthenticatedData) + { + // Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) } + protectedPayload.Validate(); + additionalAuthenticatedData.Validate(); + +#if NET + var outputSize = protectedPayload.Count - (KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes); + if (outputSize <= 0) + { + throw Error.CryptCommon_PayloadInvalid(); + } + + var refPooledBuffer = new RefPooledArrayBufferWriter(outputSize); + try + { + Decrypt(protectedPayload, additionalAuthenticatedData, ref refPooledBuffer); + return refPooledBuffer.WrittenSpan.ToArray(); + } + finally + { + refPooledBuffer.Dispose(); + } +#else + // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC + if (protectedPayload.Count < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes)) + { + throw Error.CryptCommon_PayloadInvalid(); + } + + 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 + int macOffset; // position in protectedPayload.Array where ciphertext ends / MAC begins + int eofOffset; // position in protectedPayload.Array where MAC ends + + checked + { + keyModifierOffset = protectedPayload.Offset; + ivOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES; + ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes; + } + + ReadOnlySpan keyModifier = protectedPayload.Array!.AsSpan(keyModifierOffset, ivOffset - keyModifierOffset); + + // Step 2: Decrypt the KDK and use it to restore the original encryption and MAC keys. + var decryptedKdk = new byte[_keyDerivationKey.Length]; + + byte[]? validationSubkeyArray = null; + var validationSubkey = _validationAlgorithmSubkeyLengthInBytes <= 128 + ? stackalloc byte[128].Slice(0, _validationAlgorithmSubkeyLengthInBytes) + : (validationSubkeyArray = new byte[_validationAlgorithmSubkeyLengthInBytes]); + + byte[] decryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes]; + // 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 = validationSubkeyArray) + { + try + { + _keyDerivationKey.WriteSecretIntoBuffer(decryptedKdkUnsafe, decryptedKdk.Length); + ManagedSP800_108_CTR_HMACSHA512.DeriveKeys( + kdk: decryptedKdk, + label: additionalAuthenticatedData, + contextHeader: _contextHeader, + contextData: keyModifier, + operationSubkey: decryptionSubkey, + validationSubkey: validationSubkey); + + // Step 3: Calculate the correct MAC for this payload. + // correctHash := MAC(IV || ciphertext) + checked + { + eofOffset = protectedPayload.Offset + protectedPayload.Count; + macOffset = eofOffset - _validationAlgorithmDigestLengthInBytes; + } + + // Step 4: Validate the MAC provided as part of the payload. + CalculateAndValidateMac(protectedPayload.Array!, ivOffset, macOffset, eofOffset, validationSubkey, validationSubkeyArray); + + // Step 5: Decipher the ciphertext and return it to the caller. + var iv = protectedPayload.Array!.AsSpan(ivOffset, _symmetricAlgorithmBlockSizeInBytes).ToArray(); + + using (var symmetricAlgorithm = CreateSymmetricAlgorithm()) + using (var cryptoTransform = symmetricAlgorithm.CreateDecryptor(decryptionSubkey, iv)) + { + var outputStream = new MemoryStream(); + using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write)) + { + cryptoStream.Write(protectedPayload.Array!, ciphertextOffset, macOffset - ciphertextOffset); + cryptoStream.FlushFinalBlock(); + + // At this point, outputStream := { plaintext }, and we're done! + return outputStream.ToArray(); + } + } + } + finally + { + // delete since these contain secret material + validationSubkey.Clear(); + Array.Clear(decryptedKdk, 0, decryptedKdk.Length); + } + } + } + catch (Exception ex) when (ex.RequiresHomogenization()) + { + // Homogenize all exceptions to CryptographicException. + throw Error.CryptCommon_GenericError(ex); + } +#endif + } + + private byte[] CreateContextHeader() + { + var EMPTY_ARRAY = Array.Empty(); + var EMPTY_ARRAY_SEGMENT = new ArraySegment(EMPTY_ARRAY); + + var retVal = new byte[checked( + 1 /* KDF alg */ + + 1 /* chaining mode */ + + sizeof(uint) /* sym alg key size */ + + sizeof(uint) /* sym alg block size */ + + sizeof(uint) /* hmac alg key size */ + + sizeof(uint) /* hmac alg digest size */ + + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */ + + _validationAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)]; + + var idx = 0; + + // First is the two-byte header + retVal[idx++] = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF + retVal[idx++] = 0; // 0x00 = CBC encryption + HMAC authentication + + // Next is information about the symmetric algorithm (key size followed by block size) + BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmSubkeyLengthInBytes); + BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmBlockSizeInBytes); + + // Next is information about the keyed hash algorithm (key size followed by digest size) + BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmSubkeyLengthInBytes); + BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmDigestLengthInBytes); + + // See the design document for an explanation of the following code. + var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _validationAlgorithmSubkeyLengthInBytes]; + ManagedSP800_108_CTR_HMACSHA512.DeriveKeys( + kdk: EMPTY_ARRAY, + label: EMPTY_ARRAY_SEGMENT, + 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 }. + + // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer. + using (var symmetricAlg = CreateSymmetricAlgorithm()) + { + using (var cryptoTransform = symmetricAlg.CreateEncryptor( + rgbKey: new ArraySegment(tempKeys, 0, _symmetricAlgorithmSubkeyLengthInBytes).AsStandaloneArray(), + rgbIV: new byte[_symmetricAlgorithmBlockSizeInBytes])) + { + var ciphertext = cryptoTransform.TransformFinalBlock(EMPTY_ARRAY, 0, 0); + CryptoUtil.Assert(ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes, "ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes"); + Buffer.BlockCopy(ciphertext, 0, retVal, idx, ciphertext.Length); + } + } + + idx += _symmetricAlgorithmBlockSizeInBytes; + + // MAC a zero-length input string and copy the digest to the return buffer. + using (var hashAlg = CreateValidationAlgorithm(new ArraySegment(tempKeys, _symmetricAlgorithmSubkeyLengthInBytes, _validationAlgorithmSubkeyLengthInBytes).AsStandaloneArray())) + { + var digest = hashAlg.ComputeHash(EMPTY_ARRAY); + CryptoUtil.Assert(digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes, "digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes"); + Buffer.BlockCopy(digest, 0, retVal, idx, digest.Length); + } + + idx += _validationAlgorithmDigestLengthInBytes; + CryptoUtil.Assert(idx == retVal.Length, "idx == retVal.Length"); + + // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || macAlgKeySize || macAlgDigestSize || E("") || MAC("") }. + return retVal; + } + + private SymmetricAlgorithm CreateSymmetricAlgorithm() + { + var retVal = _symmetricAlgorithmFactory(); + CryptoUtil.Assert(retVal != null, "retVal != null"); + + retVal.Mode = CipherMode.CBC; + retVal.Padding = PaddingMode.PKCS7; + + return retVal; + } + + private KeyedHashAlgorithm CreateValidationAlgorithm(byte[]? key = null) + { + var retVal = _validationAlgorithmFactory(); + CryptoUtil.Assert(retVal != null, "retVal != null"); + + if (key is not null) + { + retVal.Key = key; + } + return retVal; + } public void Dispose() { diff --git a/src/DataProtection/DataProtection/src/Microsoft.AspNetCore.DataProtection.csproj b/src/DataProtection/DataProtection/src/Microsoft.AspNetCore.DataProtection.csproj index 92fa8a526a84..e8dd808926e6 100644 --- a/src/DataProtection/DataProtection/src/Microsoft.AspNetCore.DataProtection.csproj +++ b/src/DataProtection/DataProtection/src/Microsoft.AspNetCore.DataProtection.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core logic to protect and unprotect data, similar to DPAPI. @@ -16,9 +16,10 @@ - + + + diff --git a/src/DataProtection/DataProtection/src/PublicAPI.Unshipped.txt b/src/DataProtection/DataProtection/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..cd97461b85cf 100644 --- a/src/DataProtection/DataProtection/src/PublicAPI.Unshipped.txt +++ b/src/DataProtection/DataProtection/src/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ISpanAuthenticatedEncryptor +Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ISpanAuthenticatedEncryptor.Decrypt(System.ReadOnlySpan ciphertext, System.ReadOnlySpan additionalAuthenticatedData, TWriter destination) -> void +Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ISpanAuthenticatedEncryptor.Encrypt(System.ReadOnlySpan plaintext, System.ReadOnlySpan additionalAuthenticatedData, TWriter destination) -> void diff --git a/src/DataProtection/DataProtection/src/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs b/src/DataProtection/DataProtection/src/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs index cd20c176b67a..31c64b6a18af 100644 --- a/src/DataProtection/DataProtection/src/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs +++ b/src/DataProtection/DataProtection/src/SP800_108/SP800_108_CTR_HMACSHA512Extensions.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.Runtime.CompilerServices; using Microsoft.AspNetCore.Cryptography; namespace Microsoft.AspNetCore.DataProtection.SP800_108; @@ -24,9 +25,9 @@ public static void DeriveKeyWithContextHeader(this ISP800_108_CTR_HMACSHA512Prov fixed (byte* pbContextHeader = contextHeader) { - UnsafeBufferUtil.BlockCopy(from: pbContextHeader, to: pbCombinedContext, byteCount: contextHeader.Length); + Unsafe.CopyBlock(pbCombinedContext, pbContextHeader, (uint)contextHeader.Length); } - UnsafeBufferUtil.BlockCopy(from: pbContext, to: &pbCombinedContext[contextHeader.Length], byteCount: cbContext); + Unsafe.CopyBlock(&pbCombinedContext[contextHeader.Length], pbContext, cbContext); // At this point, combinedContext := { contextHeader || context } provider.DeriveKey(pbLabel, cbLabel, pbCombinedContext, cbCombinedContext, pbDerivedKey, cbDerivedKey); diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Aes/AesAuthenticatedEncryptorTests.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Aes/AesAuthenticatedEncryptorTests.cs new file mode 100644 index 000000000000..458205fe40c9 --- /dev/null +++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Aes/AesAuthenticatedEncryptorTests.cs @@ -0,0 +1,27 @@ +// 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.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNetCore.DataProtection.Managed; +using Microsoft.AspNetCore.DataProtection.Tests.Internal; + +namespace Microsoft.AspNetCore.DataProtection.Tests.Aes; +public class AesAuthenticatedEncryptorTests +{ + [Theory] + [InlineData(128)] + [InlineData(192)] + [InlineData(256)] + public void Roundtrip_AesGcm_TryEncryptDecrypt_CorrectlyEstimatesDataLength(int symmetricKeySizeBits) + { + Secret kdk = new Secret(new byte[512 / 8]); + IAuthenticatedEncryptor encryptor = new AesGcmAuthenticatedEncryptor(kdk, derivedKeySizeInBytes: symmetricKeySizeBits / 8); + ArraySegment plaintext = new ArraySegment(Encoding.UTF8.GetBytes("plaintext")); + ArraySegment aad = new ArraySegment(Encoding.UTF8.GetBytes("aad")); + + RoundtripEncryptionHelpers.AssertTryEncryptTryDecryptParity(encryptor, plaintext, aad); + } +} diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs index e8131de80b71..f0f812c42917 100644 --- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs +++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs @@ -39,13 +39,13 @@ public void CreateAuthenticatedEncryptor_RoundTripsData_CngCbcImplementation(Enc symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC, symmetricAlgorithmKeySizeInBytes: (uint)(keyLengthInBits / 8), hmacAlgorithmHandle: BCryptAlgorithmHandle.OpenAlgorithmHandle(hashAlgorithm, hmac: true)); - var test = CreateEncryptorInstanceFromDescriptor(CreateDescriptor(encryptionAlgorithm, validationAlgorithm, masterKey)); + var encryptor = CreateEncryptorInstanceFromDescriptor(CreateDescriptor(encryptionAlgorithm, validationAlgorithm, masterKey)); // Act & assert - data round trips properly from control to test byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 }; byte[] aad = new byte[] { 2, 4, 6, 8, 0 }; byte[] ciphertext = control.Encrypt(new ArraySegment(plaintext), new ArraySegment(aad)); - byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(aad)); + byte[] roundTripPlaintext = encryptor.Decrypt(new ArraySegment(ciphertext), new ArraySegment(aad)); Assert.Equal(plaintext, roundTripPlaintext); } diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/CbcAuthenticatedEncryptorTests.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/CbcAuthenticatedEncryptorTests.cs index ef8a921f2bae..35ea14eab7fe 100644 --- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/CbcAuthenticatedEncryptorTests.cs +++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/CbcAuthenticatedEncryptorTests.cs @@ -1,15 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Cryptography.Cng; +using Microsoft.AspNetCore.Cryptography.SafeHandles; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; using Microsoft.AspNetCore.DataProtection.Test.Shared; +using Microsoft.AspNetCore.DataProtection.Tests.Internal; using Microsoft.AspNetCore.InternalTesting; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.DataProtection.Cng; -public class CbcAuthenticatedEncryptorTests +public class CbcAuthenticatedEncryptorTests(ITestOutputHelper outputHelper) { [ConditionalFact] [ConditionalRunTestOnlyOnWindows] @@ -93,24 +99,63 @@ public void Encrypt_KnownKey() ArraySegment plaintext = new ArraySegment(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, 2, 3); ArraySegment aad = new ArraySegment(new byte[] { 7, 6, 5, 4, 3, 2, 1, 0 }, 1, 4); + var preBufferSize = 3; + var postBufferSize = 4; + // Act byte[] retVal = encryptor.Encrypt( plaintext: plaintext, additionalAuthenticatedData: aad, - preBufferSize: 3, - postBufferSize: 4); + preBufferSize: (uint)preBufferSize, + postBufferSize: (uint)postBufferSize); // Assert - // retVal := 00 00 00 (preBuffer) + // retVal := 00 00 00 (preBuffer) (no requirement to have exactly zeros in the preBuffer) // | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F (keyModifier) // | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F (IV) // | B7 EA 3E 32 58 93 A3 06 03 89 C6 66 03 63 08 4B (encryptedData) // | 9D 8A 85 C7 0F BD 98 D8 7F 72 E7 72 3E B5 A6 26 (HMAC) // | 6C 38 77 F7 66 19 A2 C9 2C BB AD DA E7 62 00 00 - // | 00 00 00 00 (postBuffer) + // | 00 00 00 00 (postBuffer) (no requirement to have exactly zeros in the preBuffer) + + Assert.Equal(80 + preBufferSize + postBufferSize, retVal.Length); + + var buffer = retVal.AsSpan(preBufferSize, retVal.Length - preBufferSize - postBufferSize); + var retValAsString = Convert.ToBase64String(buffer); + Assert.Equal("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh+36j4yWJOjBgOJxmYDYwhLnYqFxw+9mNh/cudyPrWmJmw4d/dmGaLJLLut2udiAAA=", retValAsString); + } + + [ConditionalTheory] + [ConditionalRunTestOnlyOnWindows] + [InlineData(128, "SHA256", "")] + [InlineData(128, "SHA256", "This is a small text")] + [InlineData(128, "SHA256", "This is a very long plaintext message that spans multiple blocks and should test the encryption and size estimation with larger payloads to ensure everything works correctly")] + [InlineData(192, "SHA256", "")] + [InlineData(192, "SHA256", "This is a small text")] + [InlineData(192, "SHA256", "This is a very long plaintext message that spans multiple blocks and should test the encryption and size estimation with larger payloads to ensure everything works correctly")] + [InlineData(256, "SHA256", "")] + [InlineData(256, "SHA256", "This is a small text")] + [InlineData(256, "SHA256", "This is a very long plaintext message that spans multiple blocks and should test the encryption and size estimation with larger payloads to ensure everything works correctly")] + [InlineData(128, "SHA512", "")] + [InlineData(128, "SHA512", "This is a small text")] + [InlineData(128, "SHA512", "This is a very long plaintext message that spans multiple blocks and should test the encryption and size estimation with larger payloads to ensure everything works correctly")] + [InlineData(192, "SHA512", "")] + [InlineData(192, "SHA512", "This is a small text")] + [InlineData(192, "SHA512", "This is a very long plaintext message that spans multiple blocks and should test the encryption and size estimation with larger payloads to ensure everything works correctly")] + [InlineData(256, "SHA512", "")] + [InlineData(256, "SHA512", "This is a small text")] + [InlineData(256, "SHA512", "This is a very long plaintext message that spans multiple blocks and should test the encryption and size estimation with larger payloads to ensure everything works correctly")] + public void Roundtrip_TryEncryptDecrypt_CorrectlyEstimatesDataLength(int symmetricKeySizeBits, string hmacAlgorithm, string plainText) + { + Secret kdk = new Secret(new byte[512 / 8]); + IAuthenticatedEncryptor encryptor = new CbcAuthenticatedEncryptor(kdk, + symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC, + symmetricAlgorithmKeySizeInBytes: (uint)(symmetricKeySizeBits / 8), + hmacAlgorithmHandle: BCryptAlgorithmHandle.OpenAlgorithmHandle(hmacAlgorithm, hmac: true)); + ArraySegment plaintext = new ArraySegment(Encoding.UTF8.GetBytes(plainText)); + ArraySegment aad = new ArraySegment(Encoding.UTF8.GetBytes("aad")); - string retValAsString = Convert.ToBase64String(retVal); - Assert.Equal("AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh+36j4yWJOjBgOJxmYDYwhLnYqFxw+9mNh/cudyPrWmJmw4d/dmGaLJLLut2udiAAAAAAAA", retValAsString); + RoundtripEncryptionHelpers.AssertTryEncryptTryDecryptParity(encryptor, plaintext, aad); } } diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/CngAuthenticatedEncryptorBaseTests.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/CngAuthenticatedEncryptorBaseTests.cs deleted file mode 100644 index 8357894f8a01..000000000000 --- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/CngAuthenticatedEncryptorBaseTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Moq; - -namespace Microsoft.AspNetCore.DataProtection.Cng.Internal; - -public unsafe class CngAuthenticatedEncryptorBaseTests -{ - [Fact] - public void Decrypt_ForwardsArraySegment() - { - // Arrange - var ciphertext = new ArraySegment(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, 3, 2); - var aad = new ArraySegment(new byte[] { 0x10, 0x11, 0x12, 0x13, 0x14 }, 1, 4); - - var encryptorMock = new Mock(); - encryptorMock - .Setup(o => o.DecryptHook(It.IsAny(), 2, It.IsAny(), 4)) - .Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) => - { - // ensure that pointers started at the right place - Assert.Equal((byte)0x03, *(byte*)pbCiphertext); - Assert.Equal((byte)0x11, *(byte*)pbAdditionalAuthenticatedData); - return new byte[] { 0x20, 0x21, 0x22 }; - }); - - // Act - var retVal = encryptorMock.Object.Decrypt(ciphertext, aad); - - // Assert - Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal); - } - - [Fact] - public void Decrypt_HandlesEmptyAADPointerFixup() - { - // Arrange - var ciphertext = new ArraySegment(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, 3, 2); - var aad = new ArraySegment(new byte[0]); - - var encryptorMock = new Mock(); - encryptorMock - .Setup(o => o.DecryptHook(It.IsAny(), 2, It.IsAny(), 0)) - .Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) => - { - // ensure that pointers started at the right place - Assert.Equal((byte)0x03, *(byte*)pbCiphertext); - Assert.NotEqual(IntPtr.Zero, pbAdditionalAuthenticatedData); // CNG will complain if this pointer is zero - return new byte[] { 0x20, 0x21, 0x22 }; - }); - - // Act - var retVal = encryptorMock.Object.Decrypt(ciphertext, aad); - - // Assert - Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal); - } - - [Fact] - public void Decrypt_HandlesEmptyCiphertextPointerFixup() - { - // Arrange - var ciphertext = new ArraySegment(new byte[0]); - var aad = new ArraySegment(new byte[] { 0x10, 0x11, 0x12, 0x13, 0x14 }, 1, 4); - - var encryptorMock = new Mock(); - encryptorMock - .Setup(o => o.DecryptHook(It.IsAny(), 0, It.IsAny(), 4)) - .Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) => - { - // ensure that pointers started at the right place - Assert.NotEqual(IntPtr.Zero, pbCiphertext); // CNG will complain if this pointer is zero - Assert.Equal((byte)0x11, *(byte*)pbAdditionalAuthenticatedData); - return new byte[] { 0x20, 0x21, 0x22 }; - }); - - // Act - var retVal = encryptorMock.Object.Decrypt(ciphertext, aad); - - // Assert - Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal); - } - - internal abstract class MockableEncryptor : CngAuthenticatedEncryptorBase - { - public override void Dispose() - { - } - - public abstract byte[] DecryptHook(IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData); - - protected sealed override unsafe byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) - { - return DecryptHook((IntPtr)pbCiphertext, cbCiphertext, (IntPtr)pbAdditionalAuthenticatedData, cbAdditionalAuthenticatedData); - } - - public abstract byte[] EncryptHook(IntPtr pbPlaintext, uint cbPlaintext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer); - - protected sealed override unsafe byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer) - { - return EncryptHook((IntPtr)pbPlaintext, cbPlaintext, (IntPtr)pbAdditionalAuthenticatedData, cbAdditionalAuthenticatedData, cbPreBuffer, cbPostBuffer); - } - } -} diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/GcmAuthenticatedEncryptorTests.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/GcmAuthenticatedEncryptorTests.cs index 15b8a2fd1bb1..5c9ff8124149 100644 --- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/GcmAuthenticatedEncryptorTests.cs +++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/GcmAuthenticatedEncryptorTests.cs @@ -4,7 +4,10 @@ using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Cryptography.Cng; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNetCore.DataProtection.Managed; using Microsoft.AspNetCore.DataProtection.Test.Shared; +using Microsoft.AspNetCore.DataProtection.Tests.Internal; using Microsoft.AspNetCore.InternalTesting; namespace Microsoft.AspNetCore.DataProtection.Cng; @@ -102,4 +105,21 @@ public void Encrypt_KnownKey() string retValAsString = Convert.ToBase64String(retVal); Assert.Equal("AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaG0O2kY0NZtmh2UQtXY5B2jlgnOgAAAAA", retValAsString); } + + [ConditionalTheory] + [ConditionalRunTestOnlyOnWindows] + [InlineData(128)] + [InlineData(192)] + [InlineData(256)] + public void Roundtrip_CngGcm_TryEncryptDecrypt_CorrectlyEstimatesDataLength(int symmetricKeySizeBits) + { + Secret kdk = new Secret(new byte[512 / 8]); + IAuthenticatedEncryptor encryptor = new CngGcmAuthenticatedEncryptor(kdk, + symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_GCM, + symmetricAlgorithmKeySizeInBytes: (uint)(symmetricKeySizeBits / 8)); + ArraySegment plaintext = new ArraySegment(Encoding.UTF8.GetBytes("plaintext")); + ArraySegment aad = new ArraySegment(Encoding.UTF8.GetBytes("aad")); + + RoundtripEncryptionHelpers.AssertTryEncryptTryDecryptParity(encryptor, plaintext, aad); + } } diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Internal/RoundtripEncryptionHelpers.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Internal/RoundtripEncryptionHelpers.cs new file mode 100644 index 000000000000..bd6f79905a39 --- /dev/null +++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Internal/RoundtripEncryptionHelpers.cs @@ -0,0 +1,91 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; + +namespace Microsoft.AspNetCore.DataProtection.Tests.Internal; + +internal static class RoundtripEncryptionHelpers +{ + /// + /// and APIs should do the same steps + /// as and APIs. + ///
+ /// Method ensures that the two APIs are equivalent in terms of their behavior by performing a roundtrip encrypt-decrypt test. + ///
+ public static void AssertTryEncryptTryDecryptParity(IAuthenticatedEncryptor encryptor, ArraySegment plaintext, ArraySegment aad) + { + var spanAuthenticatedEncryptor = encryptor as ISpanAuthenticatedEncryptor; + Debug.Assert(spanAuthenticatedEncryptor != null, "ISpanAuthenticatedEncryptor is not supported by the encryptor"); + + // assert "allocatey" Encrypt/Decrypt APIs roundtrip correctly + byte[] ciphertext = encryptor.Encrypt(plaintext, aad); + byte[] decipheredtext = encryptor.Decrypt(new ArraySegment(ciphertext), aad); + Assert.Equal(plaintext.AsSpan(), decipheredtext.AsSpan()); + + // perform TryEncrypt and Decrypt roundtrip - ensures cross operation compatibility + var buffer = new ArrayBufferWriter(); + spanAuthenticatedEncryptor.Encrypt(plaintext, aad, ref buffer); + var encryptResult = buffer.WrittenSpan.ToArray(); + Assert.Equal(ciphertext.Length, encryptResult.Length); + // we can't sequence equal here, because the ciphertext will differ due to random IVs + + buffer = new ArrayBufferWriter(); + spanAuthenticatedEncryptor.Decrypt(encryptResult, aad, ref buffer); + var decryptedResult = buffer.WrittenSpan.ToArray(); + Assert.Equal(decipheredtext.Length, decryptedResult.Length); + Assert.True(decryptedResult.SequenceEqual(decipheredtext)); + + // perform Encrypt and TryDecrypt roundtrip - ensures cross operation compatibility + var encrypted = spanAuthenticatedEncryptor.Encrypt(plaintext, aad); + + buffer = new ArrayBufferWriter(); + spanAuthenticatedEncryptor.Decrypt(encrypted, aad, ref buffer); + var decryptedResult2 = buffer.WrittenSpan; + Assert.Equal(decipheredtext.Length, decryptedResult2.Length); + Assert.True(decryptedResult2.SequenceEqual(decipheredtext)); + } + + /// + /// and APIs should do the same steps + /// as and APIs. + ///
+ /// Method ensures that the two APIs are equivalent in terms of their behavior by performing a roundtrip protect-unprotect test. + ///
+ public static void AssertTryProtectTryUnprotectParity(ISpanDataProtector protector, ReadOnlySpan plaintext) + { + // assert "allocatey" Protect/Unprotect APIs roundtrip correctly + byte[] protectedData = protector.Protect(plaintext.ToArray()); + byte[] unprotectedData = protector.Unprotect(protectedData); + Assert.Equal(plaintext, unprotectedData.AsSpan()); + + // perform TryProtect and Unprotect roundtrip - ensures cross operation compatibility + var buffer = new ArrayBufferWriter(); + protector.Protect(plaintext, ref buffer); + var protectedResult = buffer.WrittenSpan; + Assert.Equal(protectedData.Length, protectedResult.Length); + // we can't sequence equal here, because the ciphertext will differ due to random IVs + + buffer = new ArrayBufferWriter(); + protector.Unprotect(protectedResult, ref buffer); + var unProtectedResult = buffer.WrittenSpan; + Assert.Equal(unprotectedData.Length, unProtectedResult.Length); + Assert.True(unProtectedResult.SequenceEqual(unprotectedData)); + + // perform Protect and TryUnprotect roundtrip - ensures cross operation compatibility + // Note: This test is limited because we can't easily access the correct AAD from outside the protector + // But we can test basic functionality with empty AAD and expect it to fail gracefully + var protectedByProtect = protector.Protect(plaintext.ToArray()); + + buffer = new ArrayBufferWriter(); + protector.Unprotect(protectedByProtect, ref buffer); + var unProtectedResult2 = buffer.WrittenSpan; + Assert.Equal(unprotectedData.Length, unProtectedResult.Length); + Assert.True(unProtectedResult.SequenceEqual(unprotectedData)); + } +} 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 1a17c3b44215..d6e7c43a0441 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 @@ -1,12 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; using System.Globalization; using System.Net; +using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel; using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal; +using Microsoft.AspNetCore.DataProtection.Managed; +using Microsoft.AspNetCore.DataProtection.Tests.Internal; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -619,6 +625,157 @@ public void CreateProtector_ChainsPurposes() Assert.Equal(expectedProtectedData, retVal); } + [Theory] + [InlineData("", EncryptionAlgorithm.AES_128_CBC, ValidationAlgorithm.HMACSHA256)] + [InlineData("small", EncryptionAlgorithm.AES_128_CBC, ValidationAlgorithm.HMACSHA256)] + [InlineData("This is a medium length plaintext message", EncryptionAlgorithm.AES_128_CBC, ValidationAlgorithm.HMACSHA256)] + [InlineData("This is a very long plaintext message that spans multiple blocks and should test the encryption and size estimation with larger payloads to ensure everything works correctly", EncryptionAlgorithm.AES_128_CBC, ValidationAlgorithm.HMACSHA256)] + [InlineData("small", EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm.HMACSHA256)] + [InlineData("This is a medium length plaintext message", EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm.HMACSHA512)] + [InlineData("small", EncryptionAlgorithm.AES_128_GCM, ValidationAlgorithm.HMACSHA256)] + [InlineData("This is a medium length plaintext message", EncryptionAlgorithm.AES_256_GCM, ValidationAlgorithm.HMACSHA256)] + public void GetProtectedSize_TryProtectUnprotect_CorrectlyEstimatesDataLength_MultipleScenarios(string plaintextStr, EncryptionAlgorithm encryptionAlgorithm, ValidationAlgorithm validationAlgorithm) + { + byte[] plaintext = Encoding.UTF8.GetBytes(plaintextStr); + var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance); + + var configuration = new AuthenticatedEncryptorConfiguration + { + EncryptionAlgorithm = encryptionAlgorithm, + ValidationAlgorithm = validationAlgorithm + }; + + Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, configuration.CreateNewDescriptor(), new[] { encryptorFactory }); + var keyRing = new KeyRing(key, [ key ]); + var mockKeyRingProvider = new Mock(); + mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); + + var protector = new KeyRingBasedSpanDataProtector( + keyRingProvider: mockKeyRingProvider.Object, + logger: GetLogger(), + originalPurposes: null, + newPurpose: "purpose"); + + RoundtripEncryptionHelpers.AssertTryProtectTryUnprotectParity(protector, plaintext); + } + + [Theory] + [InlineData(16)] // 16 bytes + [InlineData(32)] // 32 bytes + [InlineData(64)] // 64 bytes + [InlineData(128)] // 128 bytes + [InlineData(256)] // 256 bytes + [InlineData(512)] // 512 bytes + [InlineData(1024)] // 1 KB + [InlineData(4096)] // 4 KB + public void GetProtectedSize_TryProtect_VariousPlaintextSizes(int plaintextSize) + { + byte[] plaintext = new byte[plaintextSize]; + for (int i = 0; i < plaintextSize; i++) + { + plaintext[i] = (byte)(i % 256); + } + + var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance); + Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration().CreateNewDescriptor(), new[] { encryptorFactory }); + var keyRing = new KeyRing(key, new[] { key }); + var mockKeyRingProvider = new Mock(); + mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); + + var protector = new KeyRingBasedSpanDataProtector( + keyRingProvider: mockKeyRingProvider.Object, + logger: GetLogger(), + originalPurposes: null, + newPurpose: "purpose"); + + RoundtripEncryptionHelpers.AssertTryProtectTryUnprotectParity(protector, plaintext); + } + + [Theory] + [InlineData(16)] // 16 bytes + [InlineData(32)] // 32 bytes + [InlineData(64)] // 64 bytes + [InlineData(128)] // 128 bytes + [InlineData(256)] // 256 bytes + [InlineData(512)] // 512 bytes + [InlineData(1024)] // 1 KB + [InlineData(4096)] // 4 KB + public void GetUnprotectedSize_EstimatesCorrectly_VariousPlaintextSizes(int plaintextSize) + { + // Arrange + byte[] plaintext = new byte[plaintextSize]; + // Fill with a pattern to make debugging easier if needed + for (int i = 0; i < plaintextSize; i++) + { + plaintext[i] = (byte)(i % 256); + } + + var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance); + Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration().CreateNewDescriptor(), new[] { encryptorFactory }); + var keyRing = new KeyRing(key, new[] { key }); + var mockKeyRingProvider = new Mock(); + mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); + + var protector = new KeyRingBasedSpanDataProtector( + keyRingProvider: mockKeyRingProvider.Object, + logger: GetLogger(), + originalPurposes: null, + newPurpose: "purpose"); + + // Act - first protect the data + byte[] protectedData = protector.Protect(plaintext); + + var arrayBufferWriter = new ArrayBufferWriter(plaintextSize); + protector.Unprotect(protectedData, ref arrayBufferWriter); + var unprotectedData = arrayBufferWriter.WrittenSpan; + Assert.Equal(plaintextSize, unprotectedData.Length); + Assert.Equal(plaintext, unprotectedData); + } + + [Fact] + public void TryUnprotect_WithTooShortCiphertext_ReturnsFalse() + { + var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance); + Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration().CreateNewDescriptor(), new[] { encryptorFactory }); + var keyRing = new KeyRing(key, new[] { key }); + var mockKeyRingProvider = new Mock(); + mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); + + var protector = new KeyRingBasedSpanDataProtector( + keyRingProvider: mockKeyRingProvider.Object, + logger: GetLogger(), + originalPurposes: null, + newPurpose: "purpose"); + + // Act - try to unprotect with too short ciphertext (shorter than magic header + key id) + byte[] shortCiphertext = new byte[10]; // Less than 20 bytes (magic header + key id) + var destination = new ArrayBufferWriter(100); + + var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Unprotect(shortCiphertext, ref destination)); + Assert.Equal(Resources.ProtectionProvider_BadMagicHeader, ex.Message); + } + + [Fact] + public void GetUnprotectedSize_WithTooShortCiphertext_ThrowsException() + { + var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance); + Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration().CreateNewDescriptor(), new[] { encryptorFactory }); + var keyRing = new KeyRing(key, [ key ]); + var mockKeyRingProvider = new Mock(); + mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); + + var protector = new KeyRingBasedSpanDataProtector( + keyRingProvider: mockKeyRingProvider.Object, + logger: GetLogger(), + originalPurposes: null, + newPurpose: "purpose"); + + // Less than magic header + key id size + var buffer = new ArrayBufferWriter(); + var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Unprotect(new byte[10], ref buffer)); + Assert.Equal(Resources.ProtectionProvider_BadMagicHeader, ex.Message); + } + private static byte[] BuildAadFromPurposeStrings(Guid keyId, params string[] purposes) { var expectedAad = new byte[] { 0x09, 0xF0, 0xC9, 0xF0 } // magic header diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Managed/ManagedAuthenticatedEncryptorTests.cs b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Managed/ManagedAuthenticatedEncryptorTests.cs index e49a4ef4cfa8..deaa07a244bc 100644 --- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Managed/ManagedAuthenticatedEncryptorTests.cs +++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Managed/ManagedAuthenticatedEncryptorTests.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Security.Cryptography; using System.Text; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNetCore.DataProtection.Tests.Internal; namespace Microsoft.AspNetCore.DataProtection.Managed; @@ -103,4 +106,33 @@ public void Encrypt_KnownKey() string retValAsString = Convert.ToBase64String(retVal); Assert.Equal("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh+36j4yWJOjBgOJxmYDYwhLnYqFxw+9mNh/cudyPrWmJmw4d/dmGaLJLLut2udiAAA=", retValAsString); } + + [Theory] + [InlineData(128, "SHA256")] + [InlineData(192, "SHA256")] + [InlineData(256, "SHA256")] + [InlineData(128, "SHA512")] + [InlineData(192, "SHA512")] + [InlineData(256, "SHA512")] + public void Roundtrip_TryEncryptDecrypt_CorrectlyEstimatesDataLength(int symmetricKeySizeBits, string hmacAlgorithm) + { + Secret kdk = new Secret(new byte[512 / 8]); + + Func validationAlgorithmFactory = hmacAlgorithm switch + { + "SHA256" => () => new HMACSHA256(), + "SHA512" => () => new HMACSHA512(), + _ => throw new ArgumentException($"Unsupported HMAC algorithm: {hmacAlgorithm}") + }; + + IAuthenticatedEncryptor encryptor = new ManagedAuthenticatedEncryptor(kdk, + symmetricAlgorithmFactory: Aes.Create, + symmetricAlgorithmKeySizeInBytes: symmetricKeySizeBits / 8, + validationAlgorithmFactory: validationAlgorithmFactory); + + ArraySegment plaintext = new ArraySegment(Encoding.UTF8.GetBytes("plaintext")); + ArraySegment aad = new ArraySegment(Encoding.UTF8.GetBytes("aad")); + + RoundtripEncryptionHelpers.AssertTryEncryptTryDecryptParity(encryptor, plaintext, aad); + } } diff --git a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Microsoft.AspNetCore.DataProtection.Tests.csproj b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Microsoft.AspNetCore.DataProtection.Tests.csproj index ca7b11a50c55..22cad1bc2158 100644 --- a/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Microsoft.AspNetCore.DataProtection.Tests.csproj +++ b/src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Microsoft.AspNetCore.DataProtection.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) diff --git a/src/DataProtection/Extensions/src/TimeLimitedDataProtector.cs b/src/DataProtection/Extensions/src/TimeLimitedDataProtector.cs index 6cdddd7c88b6..e60a464c1acf 100644 --- a/src/DataProtection/Extensions/src/TimeLimitedDataProtector.cs +++ b/src/DataProtection/Extensions/src/TimeLimitedDataProtector.cs @@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.DataProtection; internal sealed class TimeLimitedDataProtector : ITimeLimitedDataProtector { private const string MyPurposeString = "Microsoft.AspNetCore.DataProtection.TimeLimitedDataProtector.v1"; + private const int ExpirationTimeHeaderSize = 8; // size of the expiration time header in bytes (64-bit UTC tick count) private readonly IDataProtector _innerProtector; private IDataProtector? _innerProtectorWithTimeLimitedPurpose; // created on-demand @@ -50,9 +51,9 @@ public byte[] Protect(byte[] plaintext, DateTimeOffset expiration) ArgumentNullThrowHelper.ThrowIfNull(plaintext); // We prepend the expiration time (as a 64-bit UTC tick count) to the unprotected data. - byte[] plaintextWithHeader = new byte[checked(8 + plaintext.Length)]; + byte[] plaintextWithHeader = new byte[checked(ExpirationTimeHeaderSize + plaintext.Length)]; BitHelpers.WriteUInt64(plaintextWithHeader, 0, (ulong)expiration.UtcTicks); - Buffer.BlockCopy(plaintext, 0, plaintextWithHeader, 8, plaintext.Length); + Buffer.BlockCopy(plaintext, 0, plaintextWithHeader, ExpirationTimeHeaderSize, plaintext.Length); return GetInnerProtectorWithTimeLimitedPurpose().Protect(plaintextWithHeader); } @@ -71,7 +72,7 @@ internal byte[] UnprotectCore(byte[] protectedData, DateTimeOffset now, out Date try { byte[] plaintextWithHeader = GetInnerProtectorWithTimeLimitedPurpose().Unprotect(protectedData); - if (plaintextWithHeader.Length < 8) + if (plaintextWithHeader.Length < ExpirationTimeHeaderSize) { // header isn't present throw new CryptographicException(Resources.TimeLimitedDataProtector_PayloadInvalid); @@ -88,8 +89,8 @@ internal byte[] UnprotectCore(byte[] protectedData, DateTimeOffset now, out Date } // Not expired - split and return payload - byte[] retVal = new byte[plaintextWithHeader.Length - 8]; - Buffer.BlockCopy(plaintextWithHeader, 8, retVal, 0, retVal.Length); + byte[] retVal = new byte[plaintextWithHeader.Length - ExpirationTimeHeaderSize]; + Buffer.BlockCopy(plaintextWithHeader, ExpirationTimeHeaderSize, retVal, 0, retVal.Length); expiration = embeddedExpiration; return retVal; } diff --git a/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/AssemblyInfo.cs b/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/AssemblyInfo.cs new file mode 100644 index 000000000000..09f49228e9e6 --- /dev/null +++ b/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/AssemblyInfo.cs @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark] diff --git a/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/Benchmarks/SpanDataProtectorComparison.cs b/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/Benchmarks/SpanDataProtectorComparison.cs new file mode 100644 index 000000000000..ede96f97c1e6 --- /dev/null +++ b/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/Benchmarks/SpanDataProtectorComparison.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.DataProtection.MicroBenchmarks.Benchmarks; + +/* + + BenchmarkDotNet=v0.13.0, OS=Windows 10.0.26100 + AMD Ryzen 9 7950X3D, 1 CPU, 32 logical and 16 physical cores + .NET SDK=10.0.100-rc.1.25420.111 + [Host] : .NET 10.0.0 (10.0.25.42121), X64 RyuJIT + DefaultJob : .NET 10.0.0 (10.0.25.42111), X64 RyuJIT + Job-UEQIYD : .NET 10.0.0 (10.0.25.42111), X64 RyuJIT + + Server=True + +| Method | Job | Toolchain | RunStrategy | PlaintextLength | Mean | Error | StdDev | Median | Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated | +|--------------------------------------- |----------- |--------------- |------------ |---------------- |---------:|----------:|----------:|---------:|----------:|-------:|------:|------:|----------:| +| ByteArray_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 5 | 3.643 us | 0.0397 us | 0.0372 us | 3.637 us | 274,473.3 | - | - | - | 360 B | +| PooledWriter_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 5 | 3.490 us | 0.0265 us | 0.0207 us | 3.490 us | 286,505.4 | 0.0038 | - | - | 224 B | +| RefWriter_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 5 | 3.958 us | 0.1572 us | 0.4585 us | 3.791 us | 252,681.2 | - | - | - | 160 B | +| ByteArray_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 5 | 3.896 us | 0.0708 us | 0.1163 us | 3.847 us | 256,706.2 | - | - | - | 360 B | +| PooledWriter_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 5 | 3.715 us | 0.0242 us | 0.0189 us | 3.716 us | 269,177.8 | - | - | - | 224 B | +| RefWriter_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 5 | 3.735 us | 0.0741 us | 0.1153 us | 3.709 us | 267,729.7 | - | - | - | 160 B | +| ByteArray_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 50 | 4.020 us | 0.0790 us | 0.0700 us | 3.998 us | 248,760.2 | 0.0076 | - | - | 456 B | +| PooledWriter_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 50 | 3.750 us | 0.0507 us | 0.0423 us | 3.761 us | 266,700.7 | - | - | - | 224 B | +| RefWriter_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 50 | 3.856 us | 0.0737 us | 0.1231 us | 3.875 us | 259,344.1 | - | - | - | 160 B | +| ByteArray_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 50 | 4.347 us | 0.1277 us | 0.3764 us | 4.207 us | 230,042.8 | - | - | - | 456 B | +| PooledWriter_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 50 | 3.938 us | 0.0785 us | 0.1454 us | 3.903 us | 253,935.7 | - | - | - | 224 B | +| RefWriter_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 50 | 3.898 us | 0.0780 us | 0.2286 us | 3.828 us | 256,567.4 | - | - | - | 160 B | +| ByteArray_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 100 | 4.088 us | 0.0816 us | 0.2329 us | 4.051 us | 244,610.9 | 0.0076 | - | - | 552 B | +| PooledWriter_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 100 | 3.895 us | 0.0779 us | 0.0765 us | 3.877 us | 256,752.7 | 0.0038 | - | - | 224 B | +| RefWriter_ProtectUnprotectRoundtrip | DefaultJob | Default | Default | 100 | 4.041 us | 0.0843 us | 0.2377 us | 3.981 us | 247,485.8 | - | - | - | 160 B | +| ByteArray_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 100 | 4.352 us | 0.0835 us | 0.2001 us | 4.280 us | 229,762.9 | - | - | - | 552 B | +| PooledWriter_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 100 | 3.960 us | 0.0768 us | 0.1051 us | 3.961 us | 252,506.3 | - | - | - | 224 B | +| RefWriter_ProtectUnprotectRoundtrip | Job-UEQIYD | .NET Core 10.0 | Throughput | 100 | 3.980 us | 0.0788 us | 0.1227 us | 3.939 us | 251,236.0 | - | - | - | 160 B | + + */ + +[SimpleJob, MemoryDiagnoser] +public class SpanDataProtectorComparison +{ + private IDataProtector _dataProtector = null!; + private ISpanDataProtector _spanDataProtector = null!; + + private byte[] _plaintext5 = null!; + private byte[] _plaintext50 = null!; + private byte[] _plaintext100 = null!; + + [Params(5, 50, 100)] + public int PlaintextLength { get; set; } + + [GlobalSetup] + public void Setup() + { + // Setup DataProtection as in DI + var services = new ServiceCollection(); + services.AddDataProtection(); + var serviceProvider = services.BuildServiceProvider(); + + _dataProtector = serviceProvider.GetDataProtector("benchmark", "test"); + _spanDataProtector = (ISpanDataProtector)_dataProtector; + + // Setup test data for different lengths + var random = new Random(42); // Fixed seed for consistent results + + _plaintext5 = new byte[5]; + random.NextBytes(_plaintext5); + + _plaintext50 = new byte[50]; + random.NextBytes(_plaintext50); + + _plaintext100 = new byte[100]; + random.NextBytes(_plaintext100); + } + + private byte[] GetPlaintext() + { + return PlaintextLength switch + { + 5 => _plaintext5, + 50 => _plaintext50, + 100 => _plaintext100, + _ => throw new ArgumentException("Invalid plaintext length") + }; + } + + [Benchmark] + public int ByteArray_ProtectUnprotectRoundtrip() + { + var plaintext = GetPlaintext(); + + // Traditional approach with allocations + var protectedData = _dataProtector.Protect(plaintext); + var unprotectedData = _dataProtector.Unprotect(protectedData); + return protectedData.Length + unprotectedData.Length; + } + + [Benchmark] + public int PooledWriter_ProtectUnprotectRoundtrip() + { + var plaintext = GetPlaintext(); + + var protectBuffer = new PooledArrayBufferWriter(initialCapacity: 255); + var unprotectBuffer = new PooledArrayBufferWriter(initialCapacity: PlaintextLength); + try + { + _spanDataProtector.Protect(plaintext, ref protectBuffer); + var protectedSpan = protectBuffer.WrittenSpan; + + _spanDataProtector.Unprotect(protectedSpan, ref unprotectBuffer); + var unProtectedSpan = protectBuffer.WrittenSpan; + + return protectedSpan.Length + unProtectedSpan.Length; + } + finally + { + protectBuffer.Dispose(); + unprotectBuffer.Dispose(); + } + } + + [Benchmark] + public int RefWriter_ProtectUnprotectRoundtrip() + { + var plaintext = GetPlaintext(); + + var protectBuffer = new RefPooledArrayBufferWriter(initialCapacity: 255); + var unprotectBuffer = new RefPooledArrayBufferWriter(initialCapacity: PlaintextLength); + try + { + _spanDataProtector.Protect(plaintext, ref protectBuffer); + var protectedSpan = protectBuffer.WrittenSpan; + + _spanDataProtector.Unprotect(protectedSpan, ref unprotectBuffer); + var unProtectedSpan = unprotectBuffer.WrittenSpan; + + return protectedSpan.Length + unProtectedSpan.Length; + } + finally + { + protectBuffer.Dispose(); + unprotectBuffer.Dispose(); + } + } +} diff --git a/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks.csproj b/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks.csproj new file mode 100644 index 000000000000..85d042105faa --- /dev/null +++ b/src/DataProtection/benchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks/Microsoft.AspNetCore.DataProtection.MicroBenchmarks.csproj @@ -0,0 +1,27 @@ + + + + $(DefaultNetCoreTargetFramework) + Exe + true + true + false + $(DefineConstants);IS_BENCHMARKS + true + false + enable + + + + + + + + + + + + + + + diff --git a/src/DataProtection/samples/KeyManagementSimulator/Program.cs b/src/DataProtection/samples/KeyManagementSimulator/Program.cs index 44622358227e..899a7445740f 100644 --- a/src/DataProtection/samples/KeyManagementSimulator/Program.cs +++ b/src/DataProtection/samples/KeyManagementSimulator/Program.cs @@ -277,10 +277,26 @@ sealed class MockActivator(IXmlDecryptor decryptor, IAuthenticatedEncryptorDescr /// /// A mock authenticated encryptor that only applies the identity function (i.e. does nothing). /// -sealed class MockAuthenticatedEncryptor : IAuthenticatedEncryptor +sealed class MockAuthenticatedEncryptor : ISpanAuthenticatedEncryptor { - byte[] IAuthenticatedEncryptor.Decrypt(ArraySegment ciphertext, ArraySegment _additionalAuthenticatedData) => ciphertext.ToArray(); - byte[] IAuthenticatedEncryptor.Encrypt(ArraySegment plaintext, ArraySegment _additionalAuthenticatedData) => plaintext.ToArray(); + public byte[] Encrypt(ArraySegment plaintext, ArraySegment _additionalAuthenticatedData) => plaintext.ToArray(); + + public void Encrypt(ReadOnlySpan plainttext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) where TWriter : System.Buffers.IBufferWriter, allows ref struct + { + var destinationSpan = destination.GetSpan(plainttext.Length); + plainttext.CopyTo(destinationSpan); + destination.Advance(destinationSpan.Length); + } + + public byte[] Decrypt(ArraySegment ciphertext, ArraySegment _additionalAuthenticatedData) => ciphertext.ToArray(); + + public void Decrypt(ReadOnlySpan ciphertext, ReadOnlySpan additionalAuthenticatedData, ref TWriter destination) + where TWriter : System.Buffers.IBufferWriter, allows ref struct + { + var destinationSpan = destination.GetSpan(ciphertext.Length); + ciphertext.CopyTo(destinationSpan); + destination.Advance(destinationSpan.Length); + } } /// diff --git a/src/Shared/Buffers/RefPooledArrayBufferWriter.cs b/src/Shared/Buffers/RefPooledArrayBufferWriter.cs new file mode 100644 index 000000000000..c1677e5f4abc --- /dev/null +++ b/src/Shared/Buffers/RefPooledArrayBufferWriter.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied from https://github.com/dotnet/corefx/blob/b0751dcd4a419ba6731dcaa7d240a8a1946c934c/src/System.Text.Json/src/System/Text/Json/Serialization/ArrayBufferWriter.cs + +using System.Runtime.CompilerServices; + +namespace System.Buffers; + +/// +/// A high-performance struct-based IBufferWriter<byte> implementation that uses ArrayPool for allocations. +/// Designed for zero-allocation scenarios when used with generic methods via `allows ref struct` constraint. +/// +internal ref struct RefPooledArrayBufferWriter : IBufferWriter, IDisposable +{ + private byte[] _buffer; + private int _index; + + /// + /// Initializes a new instance of StructArrayBufferWriter with a specified initial capacity. + /// + /// The initial capacity to rent from the ArrayPool. + public RefPooledArrayBufferWriter(int initialCapacity) + { + if (initialCapacity < 1) + { + throw new ArgumentOutOfRangeException(nameof(initialCapacity), "Initial capacity should be positive."); + } + + _buffer = ArrayPool.Shared.Rent(initialCapacity); + _index = 0; + } + + /// + /// Clears the buffer contents and returns the rented array to the ArrayPool. + /// This must be called to properly clean up resources. + /// + public void Dispose() + { + if (_buffer == null) + { + return; + } + + // Optionally clear the buffer before returning to pool (security) + // Uncomment if needed for sensitive data: + // _buffer.AsSpan(0, _index).Clear(); + + ArrayPool.Shared.Return(_buffer, clearArray: false); + _buffer = null!; + } + + /// + /// Gets the number of bytes written to the buffer. + /// + public int WrittenCount + { + get + { + ThrowIfDisposed(); + return _index; + } + } + + /// + /// Gets the capacity of the underlying buffer. + /// + public int Capacity + { + get + { + ThrowIfDisposed(); + return _buffer.Length; + } + } + + /// + /// Gets the available space remaining in the buffer. + /// + public int FreeCapacity + { + get + { + ThrowIfDisposed(); + return _buffer.Length - _index; + } + } + + /// + /// Gets a memory segment representing the available space for writing. + /// + /// A hint about the minimum size needed. Ignored in this implementation as resizing is not performed. + /// A Memory<byte> segment for the available space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Memory GetMemory(int sizeHint = 0) + { + ThrowIfDisposed(); + return _buffer.AsMemory(_index); + } + + /// + /// Gets a span representing the available space for writing. + /// + /// A hint about the minimum size needed. Ignored in this implementation as resizing is not performed. + /// A Span<byte> for the available space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int sizeHint = 0) + { + ThrowIfDisposed(); + return _buffer.AsSpan(_index); + } + + public void Advance(uint count) + => Advance((int)count); + + /// + /// Advances the write position by the specified count. + /// + /// The number of bytes written. + /// Thrown if count is negative. + /// Thrown if advancing would exceed buffer capacity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + ThrowIfDisposed(); + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be negative."); + } + + if (_index > _buffer.Length - count) + { + throw new InvalidOperationException($"Cannot advance past the end of the buffer. Current position: {_index}, Capacity: {_buffer.Length}, Requested advance: {count}."); + } + + _index += count; + } + + /// + /// Gets a span of the written data. + /// + public ReadOnlySpan WrittenSpan + { + get + { + ThrowIfDisposed(); + return _buffer.AsSpan(0, _index); + } + } + + /// + /// Gets a memory segment of the written data. + /// + public ReadOnlyMemory WrittenMemory + { + get + { + ThrowIfDisposed(); + return _buffer.AsMemory(0, _index); + } + } + + /// + /// Clears the buffer, resetting the write position to zero without deallocating. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + ThrowIfDisposed(); + _index = 0; + } + + /// + /// Resets the writer state, returning the buffer to the pool if it exists. + /// Useful when reusing the struct in a loop. + /// + public void Reset() + { + Dispose(); + _index = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ThrowIfDisposed() + { + if (_buffer is null) + { + throw new ObjectDisposedException(nameof(RefPooledArrayBufferWriter), "The buffer writer has been disposed."); + } + } +} diff --git a/src/Shared/PooledArrayBufferWriter.cs b/src/Shared/PooledArrayBufferWriter.cs index cf580650d4ce..0c9b299943f6 100644 --- a/src/Shared/PooledArrayBufferWriter.cs +++ b/src/Shared/PooledArrayBufferWriter.cs @@ -22,18 +22,32 @@ public PooledArrayBufferWriter() public PooledArrayBufferWriter(int initialCapacity) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialCapacity); + if (initialCapacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(initialCapacity), actualValue: initialCapacity, $"{nameof(initialCapacity)} ('{initialCapacity}') must be a non-negative and non-zero value."); + } _rentedBuffer = ArrayPool.Shared.Rent(initialCapacity); _index = 0; } - public ReadOnlyMemory WrittenMemory + /// + /// Gets a span of the written data. + /// + public ReadOnlySpan WrittenSpan { get { CheckIfDisposed(); + return _rentedBuffer.AsSpan(0, _index); + } + } + public ReadOnlyMemory WrittenMemory + { + get + { + CheckIfDisposed(); return _rentedBuffer.AsMemory(0, _index); } } @@ -106,14 +120,29 @@ private void CheckIfDisposed() private static void ThrowObjectDisposedException() { +#if NET throw new ObjectDisposedException(nameof(ArrayBufferWriter)); +#else + throw new ObjectDisposedException(nameof(IBufferWriter)); +#endif } + public void Advance(uint count) + => Advance((int)count); + public void Advance(int count) { - CheckIfDisposed(); + if (count == 0) + { + // no-op + return; + } - ArgumentOutOfRangeException.ThrowIfNegative(count); + CheckIfDisposed(); + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), actualValue: count, $"{nameof(count)} ('{count}') must be a non-negative value."); + } if (_index > _rentedBuffer.Length - count) { @@ -143,7 +172,10 @@ private void CheckAndResizeBuffer(int sizeHint) { Debug.Assert(_rentedBuffer != null); - ArgumentOutOfRangeException.ThrowIfNegative(sizeHint); + if (sizeHint <= 0) + { + throw new ArgumentOutOfRangeException(nameof(sizeHint), actualValue: sizeHint, $"{nameof(sizeHint)} ('{sizeHint}') must be a non-negative value."); + } if (sizeHint == 0) {