-
Notifications
You must be signed in to change notification settings - Fork 10.3k
perf: improve ManagedAuthenticatedEncryptor Decrypt() and Encrypt() flow #59424
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
Conversation
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/KeyManagement/KeyManagementOptions.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Outdated
Show resolved
Hide resolved
Code review notes
Scenario notesIs the goal to improve the performance of the [real-world?] AntiForgery benchmark or to improve the performance of DataProtection in a standalone benchmark? The PR description (and attached graph) make it sound like improving the performance of the crank-based benchmark is the goal, but no throughput measurement is provided for the changes in this PR. Please provide that graph. It would supply evidence that these changes have real-world impact and aren't just microbenchmark improvements. |
Thanks for detailed answer @GrabYourPitchforks! Firstly, I ran the Antiforgery benchmark multiple times, and I provided the results in the PR description. Re №3: I ran a BenchmarkDotNet for stackalloc with dynamic \ constant length of stackalloc (also with or without Re №2: Thanks for clarifying it, I will create issues on the dotnet/runtime explaining what API I would like to have to make DataProtection's flow dont use Re №1: Could you please describe how attack surface of the application is increased if pool buffers are used? Does that mean that pooling is easier to inject into via reflection for example? Actually, even if we will not be using pooling byte arrays, if I work with corelib to introduce APIs supporting
|
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 5 out of 15 changed files in this pull request and generated no comments.
Files not reviewed (10)
- eng/Dependencies.props: Language not supported
- eng/Version.Details.xml: Language not supported
- eng/Versions.props: Language not supported
- src/DataProtection/Cryptography.Internal/src/Microsoft.AspNetCore.Cryptography.Internal.csproj: Language not supported
- src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/KeyRingBasedDataProtectorTests.cs: Evaluated as low risk
- src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs: Evaluated as low risk
- src/DataProtection/Cryptography.Internal/test/CryptoUtilTests.cs: Evaluated as low risk
- src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SP800_108/SP800_108Tests.cs: Evaluated as low risk
- src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/SequentialGenRandom.cs: Evaluated as low risk
- src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs: Evaluated as low risk
Just saw the latest update:
👀 nice work! |
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Show resolved
Hide resolved
public class E2ETests | ||
{ | ||
[Fact] | ||
public void ProtectAndUnprotect_ForSampleAntiforgeryToken() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
src/DataProtection/DataProtection/src/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
Outdated
Show resolved
Hide resolved
public class E2ETests | ||
{ | ||
[Fact] | ||
public void ProtectAndUnprotect_ForSampleAntiforgeryToken() |
There was a problem hiding this comment.
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.
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs
Outdated
Show resolved
Hide resolved
just as a history note: @BrennanConroy noticed that 111k RPS was before the openssl update, so we dont see 45% RPS improvement, but >10% however allocation rate is still going lower significantly even for a 15 sec run: |
The goal of PR is to bring linux performance closer to windows performance for

DataProtection
scenario. Below is the picture of Antiforgery benchmarks on win vs lin machines.Results: DataProtection Benchmark
The benchmark I am relying on to show the result numbers is here, which is basically building default
ServiceProvider
, adding DataProtection via.AddDataProtection()
and callingIDataProtector.Protect()
orIDataProtector.Unprotect()
.example crank run:
Results: Antiforgery Benchmark
However, since we are originally looking at improving the Antiforgery performance on linux, I ran the Antiforgery benchmark including locally built dll's from this PR.
example crank run:
current aspnet core gives these stats on the benchmark (avg on 4 runs):
RPS of run with changed dll's varies from run to run, therefore I ran it 10 times
But memory usage is stable with such values:
Which provide an evidence of ~10% of max app allocation size and ~46% RPS improvement.
Optimization details
Another improvement can be achieved after new APIs introduced in dotnet/runtime: dotnet/runtime#111154
I looked into
Unprotect
method forManagedAuthenticatedEncryptor
and spottedMemoryStream
usage and multipleBuffer.BlockCopy
usages. Also I saw that there is some shuffling ofbyte[]
data, which I think can be skipped and performed in such a way, that some allocations are skipped.In order to be as safe as possible, I created a separate
DataProtectionPool
which provides API to rent and return byte arrays. It is not intersecting withArrayPool<byte>.Shared
.ManagedSP800_108_CTR_HMACSHA512.DeriveKeys
is changed to explicit usageManagedSP800_108_CTR_HMACSHA512.DeriveKeysHMACSHA512
, because _kdkPrfFactory is anyway hardcoded to useHMACSHA512
. There is a static API allowing to hash without allocating kdkbyte[]
which is rented from the buffer:HMACSHA512.TryHashData(kdk, prfInput, prfOutput, out _);
Avoided usage of
DeriveKeysWithContextHeader
which allocates a separate intermediate array forcontextHeader
andcontext
. Instead passing the spansoperationSubkey
andvalidationSubkey
directly intoManagedSP800_108_CTR_HMACSHA512.DeriveKeys
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysHMACSHA512
had 2 more arrays (prfInput
andprfOutput
), which now I am renting (viaDataProtectionPool
) or evenstackalloc
'ing. They are returned to the pool withclearArray: true
flag to make sure key material is removed from the memory after usage.In
Decrypt()
flow I am again using HashAlgorithm.TryComputeHash overload, which works based on theSpan<byte>
types, compared to previously used HashAlgorithm.ComputeHashIn
Decrypt()
flow changed usage to SymmetricAlgorithm.DecryptCbc() instead of CryptoTransform.TransformBlock() with same idea to useSpan<byte>
API instead of anotherbyte[]
allocation.Encrypt()
flow is reusing №1, №2 and №3 optimizations as wellEncrypt()
before was relying on theMemoryStream
andCryptoStream
to write data in the result buffer, but I am pre-calculating the length, and then doing a single allocation of result array:var outputArray = new byte[keyModifierLength + ivLength + cipherTextLength + macLength];
All required data is copied into the outputArray via APIs supportingSpan<byte>
.All listed optimizations are included in the
net10
TFM, but only some (№ 2, №3 and №6) are used innetstandard2.0
andnetFx
TFMs which DataProtection also targets.Related to #59287