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 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,21 @@ public static bool TimeConstantBuffersAreEqual(byte* bufA, byte* bufB, uint coun
#endif
}

#if NET10_0_OR_GREATER
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static bool TimeConstantBuffersAreEqual(ReadOnlySpan<byte> bufA, ReadOnlySpan<byte> bufB)
{
// Technically this is an early exit scenario, but it means that the caller did something bizarre.
// An error at the call site isn't usable for timing attacks.
Assert(bufA.Length == bufB.Length, "bufA.Length == bufB.Length");

unsafe
{
return CryptographicOperations.FixedTimeEquals(bufA, bufB);
}
}
#endif

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static bool TimeConstantBuffersAreEqual(byte[] bufA, int offsetA, int countA, byte[] bufB, int offsetB, int countB)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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.Internal;

/// <summary>
/// Used for pooling secret data (e.g. Protect()/Unprotect() flow).
/// Main goal is not to intersect with the <see cref="ArrayPool{T}.Shared"/>
/// </summary>
internal static class DataProtectionPool
{
private static readonly ArrayPool<byte> _pool = ArrayPool<byte>.Create();

public static byte[] Rent(int length) => _pool.Rent(length);
public static void Return(byte[] array, bool clearArray = false) => _pool.Return(array, clearArray);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.AspNetCore.DataProtection.Managed;

internal interface IManagedGenRandom
{
byte[] GenRandom(int numBytes);

#if NET10_0_OR_GREATER
void GenRandom(Span<byte> target);
#endif
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Security.Cryptography;

namespace Microsoft.AspNetCore.DataProtection.Managed;
Expand All @@ -16,6 +17,10 @@ private ManagedGenRandomImpl()
{
}

#if NET10_0_OR_GREATER
public void GenRandom(Span<byte> target) => RandomNumberGenerator.Fill(target);
#endif

public byte[] GenRandom(int numBytes)
{
var bytes = new byte[numBytes];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// 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.DataProtection.Internal;
using Microsoft.AspNetCore.DataProtection.Managed;

namespace Microsoft.AspNetCore.DataProtection.SP800_108;
Expand Down Expand Up @@ -55,6 +57,217 @@ public static void DeriveKeys(byte[] kdk, ArraySegment<byte> label, ArraySegment
}
}

#if NET10_0_OR_GREATER
Copy link
Member

Choose a reason for hiding this comment

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

.NET already has SP800108 in the box. We should probably just use the one that is built-in to .NET if it is available.

https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.sp800108hmaccounterkdf

public static void DeriveKeysHMACSHA512(
ReadOnlySpan<byte> kdk,
ReadOnlySpan<byte> label,
ReadOnlySpan<byte> contextHeader,
ReadOnlySpan<byte> contextData,
Span<byte> operationSubKey,
Span<byte> validationSubKey)
{
var operationSubKeyIndex = 0;
var validationSubKeyIndex = 0;
var outputCount = operationSubKey.Length + validationSubKey.Length;

byte[]? prfOutput = null;

// See SP800-108, Sec. 5.1 for the format of the input to the PRF routine.
var prfInputLength = checked(sizeof(uint) /* [i]_2 */ + label.Length + 1 /* 0x00 */ + (contextHeader.Length + contextData.Length) + sizeof(uint) /* [K]_2 */);

byte[]? prfInputLease = null;
Span<byte> prfInput = prfInputLength <= 128
? stackalloc byte[prfInputLength]
: (prfInputLease = DataProtectionPool.Rent(prfInputLength)).AsSpan(0, prfInputLength);

try
{
// Copy [L]_2 to prfInput since it's stable over all iterations
uint outputSizeInBits = (uint)checked((int)outputCount * 8);
prfInput[prfInput.Length - 4] = (byte)(outputSizeInBits >> 24);
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.Slice(sizeof(uint)));
contextHeader.CopyTo(prfInput.Slice(sizeof(uint) + label.Length + 1));
contextData.CopyTo(prfInput.Slice(sizeof(uint) + label.Length + 1 + contextHeader.Length));

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
// not using stackalloc here, because we are in a loop
// and potentially can exhaust the stack memory: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2014
prfOutput = DataProtectionPool.Rent(HMACSHA512.HashSizeInBytes);
HMACSHA512.TryHashData(kdk, prfInput, prfOutput, out _);

CryptoUtil.Assert(HMACSHA512.HashSizeInBytes == prfOutput.Length, "prfOutputSizeInBytes == prfOutput.Length");
var numBytesToCopyThisIteration = Math.Min(HMACSHA512.HashSizeInBytes, outputCount);

// we need to write into the operationSubkey
// but it may be the case that we need to split the output
// so lets count how many bytes we can write into the operationSubKey
var bytesToWrite = Math.Min(numBytesToCopyThisIteration, operationSubKey.Length - operationSubKeyIndex);
var leftOverBytes = numBytesToCopyThisIteration - bytesToWrite;
if (operationSubKeyIndex < operationSubKey.Length) // meaning we need to write to operationSubKey
{
var destination = operationSubKey.Slice(operationSubKeyIndex, bytesToWrite);
prfOutput.AsSpan(0, bytesToWrite).CopyTo(destination);
operationSubKeyIndex += bytesToWrite;
}
if (operationSubKeyIndex == operationSubKey.Length && leftOverBytes != 0) // we have filled the operationSubKey. It's time for the validationSubKey
{
var destination = validationSubKey.Slice(validationSubKeyIndex, leftOverBytes);
prfOutput.AsSpan(bytesToWrite, leftOverBytes).CopyTo(destination);
validationSubKeyIndex += leftOverBytes;
}

outputCount -= numBytesToCopyThisIteration;
}
}
finally
{
if (prfOutput is not null)
{
DataProtectionPool.Return(prfOutput, clearArray: true); // contains key material, so delete it
}

if (prfInputLease is not null)
{
DataProtectionPool.Return(prfInputLease, clearArray: true); // contains key material, so delete it
}
else
{
// to be extra careful - clear the stackalloc memory
prfInput.Clear();
}
}
}
#endif

public static void DeriveKeys(
byte[] kdk,
ReadOnlySpan<byte> label,
ReadOnlySpan<byte> contextHeader,
ReadOnlySpan<byte> contextData,
Func<byte[], HashAlgorithm> prfFactory,
Span<byte> operationSubKey,
Span<byte> validationSubKey)
{
var operationSubKeyIndex = 0;
var validationSubKeyIndex = 0;
var outputCount = operationSubKey.Length + validationSubKey.Length;

using (var prf = prfFactory(kdk))
{
byte[]? prfOutput = null;

// See SP800-108, Sec. 5.1 for the format of the input to the PRF routine.
var prfInputLength = checked(sizeof(uint) /* [i]_2 */ + label.Length + 1 /* 0x00 */ + (contextHeader.Length + contextData.Length) + sizeof(uint) /* [K]_2 */);

#if NET10_0_OR_GREATER
byte[]? prfInputLease = null;
Span<byte> prfInput = prfInputLength <= 128
? stackalloc byte[prfInputLength]
: (prfInputLease = DataProtectionPool.Rent(prfInputLength)).AsSpan(0, prfInputLength);
#else
var prfInputArray = new byte[prfInputLength];
var prfInput = prfInputArray.AsSpan();
#endif

try
{
// Copy [L]_2 to prfInput since it's stable over all iterations
uint outputSizeInBits = (uint)checked((int)outputCount * 8);
prfInput[prfInput.Length - 4] = (byte)(outputSizeInBits >> 24);
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.Slice(sizeof(uint)));
contextHeader.CopyTo(prfInput.Slice(sizeof(uint) + label.Length + 1));
contextData.CopyTo(prfInput.Slice(sizeof(uint) + label.Length + 1 + contextHeader.Length));

var prfOutputSizeInBytes = prf.GetDigestSizeInBytes();
for (uint i = 1; outputCount > 0; i++)
{
// Copy [i]_2 to prfInput since it mutates with each iteration
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
#if NET10_0_OR_GREATER
// not using stackalloc here, because we are in a loop
// and potentially can exhaust the stack memory: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2014
prfOutput = DataProtectionPool.Rent(prfOutputSizeInBytes);
prf.TryComputeHash(prfInput, prfOutput, out _);
#else
prfOutput = prf.ComputeHash(prfInputArray);
#endif

CryptoUtil.Assert(prfOutputSizeInBytes == prfOutput.Length, "prfOutputSizeInBytes == prfOutput.Length");
var numBytesToCopyThisIteration = Math.Min(prfOutputSizeInBytes, outputCount);

// we need to write into the operationSubkey
// but it may be the case that we need to split the output
// so lets count how many bytes we can write into the operationSubKey
var bytesToWrite = Math.Min(numBytesToCopyThisIteration, operationSubKey.Length - operationSubKeyIndex);
var leftOverBytes = numBytesToCopyThisIteration - bytesToWrite;
if (operationSubKeyIndex < operationSubKey.Length) // meaning we need to write to operationSubKey
{
var destination = operationSubKey.Slice(operationSubKeyIndex, bytesToWrite);
prfOutput.AsSpan(0, bytesToWrite).CopyTo(destination);
operationSubKeyIndex += bytesToWrite;
}
if (operationSubKeyIndex == operationSubKey.Length && leftOverBytes != 0) // we have filled the operationSubKey. It's time for the validationSubKey
{
var destination = validationSubKey.Slice(validationSubKeyIndex, leftOverBytes);
prfOutput.AsSpan(bytesToWrite, leftOverBytes).CopyTo(destination);
validationSubKeyIndex += leftOverBytes;
}

outputCount -= numBytesToCopyThisIteration;
}
}
finally
{
#if NET10_0_OR_GREATER
if (prfOutput is not null)
{
DataProtectionPool.Return(prfOutput, clearArray: true); // contains key material, so delete it
}

if (prfInputLease is not null)
{
DataProtectionPool.Return(prfInputLease, clearArray: true); // contains key material, so delete it
}
else
{
// to be extra careful - clear the stackalloc memory
prfInput.Clear();
}
#else
Array.Clear(prfInputArray, 0, prfInputArray.Length); // contains key material, so delete it
Array.Clear(prfOutput, 0, prfOutput.Length); // contains key material, so delete it
#endif
}
}
}

/// <remarks>
/// Probably, you would want to use similar method <see cref="DeriveKeys(byte[], ReadOnlySpan{byte}, ReadOnlySpan{byte}, ReadOnlySpan{byte}, Func{byte[], HashAlgorithm}, Span{byte}, Span{byte})"/>.
/// It is more efficient allowing to skip an allocation of `combinedContext` and writing directly into passed Spans
/// </remarks>
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
@@ -0,0 +1,28 @@
// 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.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.DataProtection.Tests;
public class E2ETests
{
[Fact]
public void ProtectAndUnprotect_ForSampleAntiforgeryToken()
{
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(string purpose = "samplePurpose")
=> new ServiceCollection()
.AddDataProtection()
.Services.BuildServiceProvider()
.GetDataProtector(purpose);
}
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.AspNetCore.DataProtection.Cng;
using Microsoft.AspNetCore.DataProtection.Managed;

Expand All @@ -27,4 +28,12 @@ public void GenRandom(byte* pbBuffer, uint cbBuffer)
pbBuffer[i] = _value++;
}
}

public void GenRandom(Span<byte> target)
{
for (var i = 0; i < target.Length; i++)
{
target[i] = _value++;
}
}
}
Loading