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

@DeagleGross DeagleGross marked this pull request as ready for review November 11, 2025 00:07
Copilot AI review requested due to automatic review settings November 11, 2025 00:07
Copilot finished reviewing on behalf of DeagleGross November 11, 2025 00:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces span-based APIs for DataProtection using IBufferWriter<byte> to improve performance and reduce memory allocations. The implementation adds ISpanDataProtector and ISpanAuthenticatedEncryptor interfaces that enable zero-allocation scenarios through generic type parameters with allows ref struct constraints.

Key changes:

  • New span-based interfaces ISpanDataProtector and ISpanAuthenticatedEncryptor with IBufferWriter<byte> destination parameters
  • Implementations across all encryptors (Managed, CNG CBC, CNG GCM, AES GCM)
  • Two buffer writer implementations: PooledArrayBufferWriter<T> (class) and RefPooledArrayBufferWriter<T> (ref struct)
  • Comprehensive test coverage and benchmarking infrastructure

Reviewed Changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Shared/PooledArrayBufferWriter.cs Adds class-based pooled buffer writer implementation with validation logic issue
src/Shared/Buffers/RefPooledArrayBufferWriter.cs Adds ref struct pooled buffer writer with similar validation issue
src/DataProtection/Abstractions/src/ISpanDataProtector.cs Defines span-based data protector interface
src/DataProtection/DataProtection/src/AuthenticatedEncryption/ISpanAuthenticatedEncryptor.cs Defines span-based authenticated encryptor interface
src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedSpanDataProtector.cs Implements span-based data protector using key ring
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs Adds span API implementations with unnecessary using directive
src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs Implements span-based GCM encryption
src/DataProtection/DataProtection/src/Cng/*.cs Implements span-based APIs for CNG encryptors
src/DataProtection/samples/KeyManagementSimulator/Program.cs Updates mock encryptor with typo in parameter name
src/DataProtection/DataProtection/test/**/*.cs Adds comprehensive roundtrip tests
src/DataProtection/benchmarks/** Adds benchmarking project showing performance improvements

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-dataprotection Includes: DataProtection

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants