Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
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
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()
Copy link
Member

Choose a reason for hiding this comment

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

What is this test doing that's different from the current unit tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

just yet another check to see if specifically Antiforgery tokens are parsed correctly. I thought it's better to check than not to. let me know if it makes sense

Copy link
Member

Choose a reason for hiding this comment

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

But you're just calling protect then unprotect on the value, so all you're really testing is a random string is round trip-able which I'm sure is already tested.

{
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