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:

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

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

and

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

+     void Decrypt<TWriter>(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> additionalAuthenticatedData, ref TWriter destination)
+         where TWriter : IBufferWriter<byte>
+ #if NET
+         , 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

Comment on lines +175 to +178
if (sizeHint <= 0)
{
throw new ArgumentOutOfRangeException(nameof(sizeHint), actualValue: sizeHint, $"{nameof(sizeHint)} ('{sizeHint}') must be a non-negative value.");
}
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The condition sizeHint <= 0 on line 175 is incorrect. It should check if sizeHint < 0 (negative) instead. The next condition on line 180 checks sizeHint == 0, which means the first condition will always throw for sizeHint == 0 before reaching the second condition. This creates unreachable code.

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +192
if (sizeHint <= 0)
{
throw new ArgumentOutOfRangeException(nameof(sizeHint), actualValue: sizeHint, $"{nameof(sizeHint)} ('{sizeHint}') must be a non-negative value.");
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The same issue exists here: checking sizeHint <= 0 followed by sizeHint == 0 creates unreachable code. The condition should check sizeHint < 0 instead to allow the zero case to be handled by the subsequent condition.

Copilot uses AI. Check for mistakes.
byte[] IAuthenticatedEncryptor.Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> _additionalAuthenticatedData) => plaintext.ToArray();
public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> _additionalAuthenticatedData) => plaintext.ToArray();

public void Encrypt<TWriter>(ReadOnlySpan<byte> plainttext, ReadOnlySpan<byte> additionalAuthenticatedData, ref TWriter destination) where TWriter : System.Buffers.IBufferWriter<byte>, allows ref struct
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

Typo in parameter name: should be plaintext instead of plainttext (double 't').

Copilot uses AI. Check for mistakes.
using System;
using System.Buffers;
using System.Diagnostics;
using System.Drawing;
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

Unnecessary using directive System.Drawing was added. This namespace is not used in the file and should be removed.

Suggested change
using System.Drawing;

Copilot uses AI. Check for mistakes.
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

2 participants