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