Skip to content

Conversation

@DeagleGross
Copy link
Member

@DeagleGross DeagleGross commented Nov 6, 2025

Please, review with caution! This is cryptography and failures here can be pretty dramatic...

This is a 2nd iteration over spanification of DataProtection. 1st iteration used a different pattern: GetSize() followed by actual operation (protection or unprotection). Here we introduce IBufferWriter<byte> as destination parameter.

The following idea came up due to the internals of DataProtection: it may happen (though it's rare) that key changes during the user's interaction with DataProtection API, and suddenly the destination buffer can be of an insufficient size. Consider:

var protectBufferSize = dataProtector.GetSize();
var destination = ArrayPool<byte>.Shared.Rent(protectBufferSize);
// >>> IMAGINE key is changed under the hood, and now destination buffer will be twice bigger <<<
if (dataProtector.TryProtect(plainText, destination))
{
...
}

However, we can simply solve it by using IBufferWriter<byte> - since it has an ability to expand its size on-the-fly.

We have experimented with the API form a lot, but decided to go with such API. Note that those APIs will be available only in NET11 or later.

#if NET

+ public interface ISpanDataProtector : IDataProtector
{
+     void Protect<TWriter>(ReadOnlySpan<byte> plaintext, ref TWriter destination) 
+          where TWriter : IBufferWriter<byte>, allows ref struct;

+     void Unprotect<TWriter>(ReadOnlySpan<byte> protectedData, ref TWriter destination)
+          where TWriter : IBufferWriter<byte>, allows ref struct;
}

#endif

and

#if NET

+public interface ISpanAuthenticatedEncryptor : IAuthenticatedEncryptor
{
+     void Encrypt<TWriter>(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> additionalAuthenticatedData, ref TWriter destination)
+          where TWriter : IBufferWriter<byte>, allows ref struct;

+     void Decrypt<TWriter>(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> additionalAuthenticatedData, ref TWriter destination)
+          where TWriter : IBufferWriter<byte>, allows ref struct;
}

#endif

New API has a few benefits and intentions:

  1. As described above, passing IBufferWriter<byte> avoids an undeterministic scenario where buffer may be not of enough size
  2. Usage is simplier (no need for doing GetSize() then Protect()/Unprotect() with not 100% guarantee that works, which means a fallback to allocatey API is required).
  3. It has a generic parameter TWriter with a constraint to be IBufferWriter<byte> to allow passing in structs for even further optimization (reduction of allocation of a ref-type for buffer instance)
  4. It has an anti-constraint and passes the buffer with ref to allows passing in a mutable ref struct. That should help in most of the cases DataProtection was designed to be used for - like Antiforgery where the input is quite small (under 100 bytes for instance), and buffer for encryption can be stackalloc'ed (which is probably the most performant mechanism to operate with a temporary buffer)

Artificial benchmarks show expected results: ref struct which uses stackalloc'ed buffer is the fastest way to operate.

That is also shown in the real benchmarks I've done on windows machine comparing original implementation, passing in a buffer which is a class, and a buffer whcih is a ref struct. With ref struct we get the most perf and use the least allocations.

Method Job Toolchain RunStrategy PlaintextLength Mean Error StdDev Median Op/s Gen 0 Gen 1 Gen 2 Allocated
ByteArray_ProtectUnprotectRoundtrip Job-REJWKR .NET Core 10.0 Throughput 80 4.039 us 0.0805 us 0.2078 us 3.939 us 247,555.9 - - - 512 B
PooledWriter_ProtectUnprotectRoundtrip Job-REJWKR .NET Core 10.0 Throughput 80 3.809 us 0.0751 us 0.0835 us 3.783 us 262,504.4 - - - 224 B
RefWriter_ProtectUnprotectRoundtrip Job-REJWKR .NET Core 10.0 Throughput 80 3.718 us 0.0711 us 0.0665 us 3.717 us 268,936.9 - - - 160 B

Fixes #44758

Copy link
Contributor

Copilot AI commented Nov 13, 2025

@halter73 I've opened a new pull request, #64339, to work on those changes. Once the pull request is ready, I'll request review from you.

@DeagleGross DeagleGross requested review from a team and tdykstra as code owners November 18, 2025 17:32
@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Nov 26, 2025
Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

I'm excited to see ant forgery take advantage of this! Thanks for your considerable effort evaluating all the different approaches.

@DeagleGross DeagleGross merged commit 249b29c into main Dec 5, 2025
30 checks passed
@DeagleGross DeagleGross deleted the dmkorolev/dataprotection-ibufferwriter-spans branch December 5, 2025 19:26
@dotnet-policy-service dotnet-policy-service bot added this to the 11.0-preview1 milestone Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-dataprotection Includes: DataProtection pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API proposal: Add ReadOnlySpan<byte> to IDataProtector (un)Protect

4 participants