Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e47b41d
a bit of improvement
DeagleGross Dec 9, 2024
bb6d3c0
another buffer.blockCopy removal
DeagleGross Dec 10, 2024
384c3d5
use `DecryptCbc` instead?
DeagleGross Dec 10, 2024
90bf754
tests and remove another array
DeagleGross Dec 10, 2024
a37b0d5
fix the slicing of IV
DeagleGross Dec 11, 2024
268ee44
slice correctly!
DeagleGross Dec 12, 2024
eb6cb1b
try with encrypt
DeagleGross Dec 16, 2024
d090882
finish encrypt
DeagleGross Dec 17, 2024
6ec76d3
use same overload in decrypt()
DeagleGross Dec 17, 2024
49eab48
dont allocate for prf-output as well
DeagleGross Dec 17, 2024
4ba9cc2
address PR comments
DeagleGross Dec 17, 2024
919d003
remove spaces
DeagleGross Dec 17, 2024
87d118b
prfInput and prfOutput rented
DeagleGross Dec 18, 2024
2524a15
correctHash as rented
DeagleGross Dec 18, 2024
ec70b6e
more details
DeagleGross Dec 18, 2024
0d30a5b
use static `TryHashData` for specific hash implementation
DeagleGross Dec 18, 2024
bab196a
prettify
DeagleGross Dec 19, 2024
e96ad0c
dont rent (only stackalloc or allocate new byte array) + address othe…
DeagleGross Jan 7, 2025
cfbf7a5
Merge branch 'main' into dmkorolev/dataprotection/lin-perf-2
DeagleGross Jan 22, 2025
c9aa2e3
address PR comment
DeagleGross Jan 24, 2025
560c79e
use ReadOnlySpan only in CryptoUtils
DeagleGross Jan 26, 2025
8e3df1a
move to "manual" section
DeagleGross Jan 26, 2025
4434822
reduce to single implementation ManagedSP800_108_CTR_HMACSHA512.Deriv…
DeagleGross Jan 27, 2025
aed8e9d
adjust a comment
DeagleGross Jan 27, 2025
a846056
change decrypt to single implementation
DeagleGross Jan 27, 2025
e72f716
single impl for encrypt()
DeagleGross Jan 27, 2025
cf48cc8
no pool at all
DeagleGross Jan 27, 2025
0f3d1aa
remove leftover
DeagleGross Jan 27, 2025
51bfe77
oops stackoverflow
DeagleGross Jan 28, 2025
ab25e0f
merge main
DeagleGross Jan 28, 2025
6f035d4
correct merge
DeagleGross Jan 28, 2025
22224dc
merge main
DeagleGross Mar 7, 2025
c8019fb
no-alloc setKey for algorithm
DeagleGross Mar 10, 2025
d87aeaf
raise stackalloc to 256
DeagleGross Mar 10, 2025
489c5cd
Revert "raise stackalloc to 256"
DeagleGross Mar 10, 2025
26ee6ad
address ManagedSP800_108_CTR_HMACSHA512
DeagleGross Mar 10, 2025
9ce2b17
Merge branch 'main' into dmkorolev/dataprotection/lin-perf-2
DeagleGross Mar 17, 2025
f50c7fb
merga main
DeagleGross Mar 18, 2025
f45a948
address PR comments
DeagleGross Mar 18, 2025
66b2170
merge main
DeagleGross Mar 18, 2025
66cd115
remove test + address nits
DeagleGross Mar 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ internal static TimeSpan MaxServerClockSkew
/// <remarks>
/// Settable for testing.
/// </remarks>
internal TimeSpan DefaultKeyResolverRetryDelay { get; set; } = TimeSpan.FromMilliseconds(200);
internal TimeSpan DefaultKeyResolverRetryDelay { get; set; } = TimeSpan.FromMilliseconds(200.0);

/// <summary>
/// Controls the lifetime (number of days before expiration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,18 @@ private byte[] CreateContextHeader()
return retVal;
}

private SymmetricAlgorithm CreateSymmetricAlgorithm()
private SymmetricAlgorithm CreateSymmetricAlgorithm(byte[]? key = null)
{
var retVal = _symmetricAlgorithmFactory();
CryptoUtil.Assert(retVal != null, "retVal != null");

retVal.Mode = CipherMode.CBC;
retVal.Padding = PaddingMode.PKCS7;
if (key is not null)
{
retVal.Key = key;
}

return retVal;
}

Expand All @@ -161,6 +166,8 @@ public byte[] Decrypt(ArraySegment<byte> protectedPayload, ArraySegment<byte> ad
protectedPayload.Validate();
additionalAuthenticatedData.Validate();

var protectedPayloadSpan = protectedPayload.AsSpan();

// 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))
{
Expand All @@ -172,7 +179,6 @@ public byte[] Decrypt(ArraySegment<byte> protectedPayload, ArraySegment<byte> ad
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
Expand All @@ -187,8 +193,6 @@ public byte[] Decrypt(ArraySegment<byte> protectedPayload, ArraySegment<byte> ad
}

ArraySegment<byte> keyModifier = new ArraySegment<byte>(protectedPayload.Array!, keyModifierOffset, ivOffset - keyModifierOffset);
var iv = new byte[_symmetricAlgorithmBlockSizeInBytes];
Buffer.BlockCopy(protectedPayload.Array!, ivOffset, iv, 0, iv.Length);

// 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.
Expand All @@ -206,16 +210,16 @@ public byte[] Decrypt(ArraySegment<byte> protectedPayload, ArraySegment<byte> ad
try
{
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
context: keyModifier,
contextData: keyModifier,
prfFactory: _kdkPrfFactory,
output: new ArraySegment<byte>(derivedKeysBuffer));

Buffer.BlockCopy(derivedKeysBuffer, 0, decryptionSubkey, 0, decryptionSubkey.Length);
Buffer.BlockCopy(derivedKeysBuffer, decryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length);
derivedKeysBuffer.AsSpan().Slice(start: 0, length: decryptionSubkey.Length).CopyTo(decryptionSubkey);
derivedKeysBuffer.AsSpan().Slice(start: decryptionSubkey.Length, length: validationSubkey.Length).CopyTo(validationSubkey);

// Step 3: Calculate the correct MAC for this payload.
// correctHash := MAC(IV || ciphertext)
Expand All @@ -233,27 +237,33 @@ public byte[] Decrypt(ArraySegment<byte> protectedPayload, ArraySegment<byte> ad
}

// Step 4: Validate the MAC provided as part of the payload.

if (!CryptoUtil.TimeConstantBuffersAreEqual(correctHash, 0, correctHash.Length, protectedPayload.Array!, macOffset, eofOffset - macOffset))
{
throw Error.CryptCommon_PayloadInvalid(); // integrity check failure
}

// Step 5: Decipher the ciphertext and return it to the caller.
#if NET10_0_OR_GREATER
using var symmetricAlgorithm = CreateSymmetricAlgorithm(key: decryptionSubkey);

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();
// note: here protectedPayload.Array is taken without an offset (cant use AsSpan() on ArraySegment)
var ciphertext = protectedPayload.Array.AsSpan().Slice(ciphertextOffset, macOffset - ciphertextOffset);
var iv = protectedPayload.Array.AsSpan().Slice(ivOffset, _symmetricAlgorithmBlockSizeInBytes);

// At this point, outputStream := { plaintext }, and we're done!
return outputStream.ToArray();
}
return symmetricAlgorithm.DecryptCbc(ciphertext, iv);
#else
var iv = new byte[_symmetricAlgorithmBlockSizeInBytes];
Buffer.BlockCopy(protectedPayload.Array!, ivOffset, iv, 0, iv.Length);

using var symmetricAlgorithm = CreateSymmetricAlgorithm();
using (var cryptoTransform = symmetricAlgorithm.CreateDecryptor(decryptionSubkey, iv))
{
var length = macOffset - ciphertextOffset;
var result = new byte[length];
_ = cryptoTransform.TransformBlock(protectedPayload.Array!, ciphertextOffset, length, result, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be TransformFinalBlock? or a combination of both? I genuinely don't know, but: this needs to be 100% right. Perhaps check exactly what Write and FlushFinalBlock would do here.

return result;
}
#endif
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,55 @@ public static void DeriveKeys(byte[] kdk, ArraySegment<byte> label, ArraySegment
}
}

public static void DeriveKeys(byte[] kdk, ReadOnlySpan<byte> label, ReadOnlySpan<byte> contextHeader, ReadOnlySpan<byte> contextData, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
{
// make copies so we can mutate these local vars
var outputOffset = output.Offset;
var outputCount = output.Count;

var contextSharedLength = contextHeader.Length + contextData.Length;

using (var prf = prfFactory(kdk))
{
// See SP800-108, Sec. 5.1 for the format of the input to the PRF routine.
var prfInput = new byte[checked(sizeof(uint) /* [i]_2 */ + label.Length + 1 /* 0x00 */ + contextSharedLength + sizeof(uint) /* [K]_2 */)];

// Copy [L]_2 to prfInput since it's stable over all iterations
uint outputSizeInBits = (uint)checked((int)outputCount * 8);
prfInput[prfInput.Length - 4] = (byte)(outputSizeInBits >> 24);
prfInput[prfInput.Length - 3] = (byte)(outputSizeInBits >> 16);
prfInput[prfInput.Length - 2] = (byte)(outputSizeInBits >> 8);
prfInput[prfInput.Length - 1] = (byte)(outputSizeInBits);

// Copy label and context to prfInput since they're stable over all iterations
label.CopyTo(prfInput.AsSpan(sizeof(uint)));
contextHeader.CopyTo(prfInput.AsSpan(sizeof(uint) + label.Length + 1));
contextData.CopyTo(prfInput.AsSpan(sizeof(uint) + label.Length + 1 + contextHeader.Length));

var prfOutputSizeInBytes = prf.GetDigestSizeInBytes();
for (uint i = 1; outputCount > 0; i++)
{
// Copy [i]_2 to prfInput since it mutates with each iteration
prfInput[0] = (byte)(i >> 24);
prfInput[1] = (byte)(i >> 16);
prfInput[2] = (byte)(i >> 8);
prfInput[3] = (byte)(i);

// Run the PRF and copy the results to the output buffer
var prfOutput = prf.ComputeHash(prfInput);
CryptoUtil.Assert(prfOutputSizeInBytes == prfOutput.Length, "prfOutputSizeInBytes == prfOutput.Length");
var numBytesToCopyThisIteration = Math.Min(prfOutputSizeInBytes, outputCount);

prfOutput.AsSpan().Slice(0, numBytesToCopyThisIteration).CopyTo(output.Array!.AsSpan().Slice(start: outputOffset));
Array.Clear(prfOutput, 0, prfOutput.Length); // contains key material, so delete it

// adjust offsets
outputOffset += numBytesToCopyThisIteration;
outputCount -= numBytesToCopyThisIteration;
}
}
}

public static void DeriveKeysWithContextHeader(byte[] kdk, ArraySegment<byte> label, byte[] contextHeader, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
{
var combinedContext = new byte[checked(contextHeader.Length + context.Count)];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -618,6 +619,23 @@ public void CreateProtector_ChainsPurposes()
Assert.Equal(expectedProtectedData, retVal);
}

[Fact]
public void Test()
{
const string sampleToken = "CfDJ8H5oH_fp1QNBmvs-OWXxsVoV30hrXeI4-PI4p1VZytjsgd0DTstMdtTZbFtm2dKHvsBlDCv7TiEWKztZf8fb48pUgBgUE2SeYV3eOUXvSfNWU0D8SmHLy5KEnwKKkZKqudDhCnjQSIU7mhDliJJN1e4";

var dataProtector = GetServiceCollectionBuiltDataProtector();
var encrypted = dataProtector.Protect(sampleToken);
var decrypted = dataProtector.Unprotect(encrypted);
Assert.Equal(sampleToken, decrypted);
}

private static IDataProtector GetServiceCollectionBuiltDataProtector()
=> new ServiceCollection()
.AddDataProtection()
.Services.BuildServiceProvider()
.GetDataProtector("SamplePurpose");

private static byte[] BuildAadFromPurposeStrings(Guid keyId, params string[] purposes)
{
var expectedAad = new byte[] { 0x09, 0xF0, 0xC9, 0xF0 } // magic header
Expand Down
Loading