-
Notifications
You must be signed in to change notification settings - Fork 10.5k
feat: "Spanify" DataProtector with IBufferWriter<byte> #64262
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
base: main
Are you sure you want to change the base?
Conversation
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.
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
ISpanDataProtectorandISpanAuthenticatedEncryptorwithIBufferWriter<byte>destination parameters - Implementations across all encryptors (Managed, CNG CBC, CNG GCM, AES GCM)
- Two buffer writer implementations:
PooledArrayBufferWriter<T>(class) andRefPooledArrayBufferWriter<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 |
| if (sizeHint <= 0) | ||
| { | ||
| throw new ArgumentOutOfRangeException(nameof(sizeHint), actualValue: sizeHint, $"{nameof(sizeHint)} ('{sizeHint}') must be a non-negative value."); | ||
| } |
Copilot
AI
Nov 11, 2025
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.
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.
| if (sizeHint <= 0) | ||
| { | ||
| throw new ArgumentOutOfRangeException(nameof(sizeHint), actualValue: sizeHint, $"{nameof(sizeHint)} ('{sizeHint}') must be a non-negative value."); |
Copilot
AI
Nov 11, 2025
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.
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.
| 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 |
Copilot
AI
Nov 11, 2025
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.
Typo in parameter name: should be plaintext instead of plainttext (double 't').
| using System; | ||
| using System.Buffers; | ||
| using System.Diagnostics; | ||
| using System.Drawing; |
Copilot
AI
Nov 11, 2025
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.
Unnecessary using directive System.Drawing was added. This namespace is not used in the file and should be removed.
| using System.Drawing; |
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 introduceIBufferWriter<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:
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:
and
New API has a few benefits and intentions:
IBufferWriter<byte>avoids an undeterministic scenario where buffer may be not of enough sizeGetSize()thenProtect()/Unprotect()with not 100% guarantee that works, which means a fallback to allocatey API is required).TWriterwith a constraint to beIBufferWriter<byte>to allow passing in structs for even further optimization (reduction of allocation of a ref-type for buffer instance)refto 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.
Fixes #44758