Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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);
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved

/// <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();
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved

// 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];
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved
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(
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved
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);
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved
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
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved
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);
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved
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);
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved
#else
var iv = new byte[_symmetricAlgorithmBlockSizeInBytes];
Buffer.BlockCopy(protectedPayload.Array!, ivOffset, iv, 0, iv.Length);
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved

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
Member

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);
DeagleGross marked this conversation as resolved.
Show resolved Hide resolved
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