Skip to content

feat: "Spanify" DataProtector #62903

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

Open
wants to merge 51 commits into
base: main
Choose a base branch
from
Open

Conversation

DeagleGross
Copy link
Member

@DeagleGross DeagleGross commented Jul 24, 2025

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

This PR introduces 2 new interfaces which allow usage of DataProtection via Span<byte>: ISpanDataProtector and IAuthenticatedEncryptor.

Details

Current PR proposes extra interfaces ISpanDataProtector and ISpanAuthenticatedEncryptor (Inspiration taken from ISpanFormattable : IFormattable). The APIs are:

namespace Microsoft.AspNetCore.DataProtection;

+ public interface ISpanDataProtector : IDataProtector
+ {
+     int GetProtectedSize(int plainTextLength);
+     bool TryProtect(ReadOnlySpan<byte> plainText, Span<byte> destination, out int bytesWritten);

+     int GetUnprotectedSize(int cipherTextLength);
+     bool TryUnprotect(ReadOnlySpan<byte> cipherText, Span<byte> destination, out int bytesWritten);
+ }

and

namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;

+public interface ISpanAuthenticatedEncryptor : IAuthenticatedEncryptor
+ {
+     int GetEncryptedSize(int plainTextLength);
+     bool TryEncrypt(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> additionalAuthenticatedData, Span<byte> destination, out int bytesWritten);

+     int GetDecryptedSize(int cipherTextLength);
+     bool TryDecrypt(ReadOnlySpan<byte> cipherText, ReadOnlySpan<byte> additionalAuthenticatedData, Span<byte> destination, out int bytesWritten);
+ }

From the implementation standpoint, these APIs are basically doing what existing Protect() / Unprotect() and Encrypt() / Decrypt() do, but without allocating a result array and instead filling in the Span<byte> destination.

For IAuthenticatedEncryptors I tried to reuse the TryEncrypt/TryDecrypt from Encrypt/Decrypt where possible to reduce code duplication.

Registrations

IDataProtector also should implement IDataProtectionProvider which provides the protector instance:

public interface IDataProtectionProvider
{
  IDataProtector CreateProtector(string purpose);
}

And in default DI (AddDataProtection) IDataProtector is created in such a manner:
https://github.com/dotnet/aspnetcore/blob/b52c80fef2b8bf5239c7643d9c79d1029bb6c988/src/DataProtection/DataProtection/src/DataProtectionServiceCollectionExtensions.cs#L89C13-L95C14

I dont want to introduce more APIs and interfaces for providing ISpanDataProtector and instead when providing I am doing a lookup to determine whether it is possible to return ISpanDataProtector or IDataProtector. Users will then be able to differentiate based on their needs and registrations.

Usage

Having IDataProtector:

void Do(IDataProtector protector, ReadOnlySpan<byte> plaintext)
{
    if (protector is ISpanDataProtector spanDataProtector)
    {
        // Use the Span API
        
        var destination = new byte[spanDataProtector.GetProtectedSize(plaintext.Length)];
        if (spanDataProtector.TryProtect(plaintext, destination, out var bytesWritten))
        {
            Console.WriteLine($"Protected {bytesWritten} bytes using Span API.");
        }
    }
    else
    {
        var ciphertext = protector.Protect(plaintext.ToArray());
        Console.WriteLine($"Protected {ciphertext.Length} bytes using regular API.");
    }
}

and the same goes for IAuthenticatedEncryptor, but I doubt majority of users implement their own IAuthenticatedEncryptor.

Testing

In order to verify correctness of the changes I've created RoundtripEncryptionHelpers.AssertTryEncryptTryDecryptParity where I try to call Encrypt/Decrypt in different order for IDataProtector and IAuthenticatedEncryptor to ensure any order of operations give a correct roundtrip result.

I will also change any other place in aspnetcore to use this API to make sure new APIs behave correctly and in the same way as existing APIs

Fixes #44758

@github-actions github-actions bot added the area-dataprotection Includes: DataProtection label Jul 24, 2025
@DeagleGross DeagleGross changed the title feat: "Spanify" DataProtector Protect/Unprotect feat: "Spanify" DataProtector Protect [part 1] Jul 24, 2025
@DeagleGross DeagleGross marked this pull request as ready for review July 24, 2025 13:54
@Copilot Copilot AI review requested due to automatic review settings July 24, 2025 13:54
@DeagleGross DeagleGross self-assigned this Jul 24, 2025
Copy link
Contributor

@Copilot 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 adds new non-allocating "Span-based" APIs to the DataProtection interfaces, specifically implementing the Protect/Encrypt functionality without requiring heap allocations. The new APIs allow callers to get the size of protected data upfront and then encrypt directly into a provided buffer.

Key changes:

  • Added GetProtectedSize() and TryProtect() methods to IDataProtector interface
  • Added GetEncryptedSize() and TryEncrypt() methods to IAuthenticatedEncryptor interface
  • Implemented these methods across all encryptor types (AES-GCM, CBC, CNG variants, Managed implementations)

Reviewed Changes

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

Show a summary per file
File Description
src/DataProtection/Abstractions/src/IDataProtector.cs Added new span-based APIs to the main data protector interface
src/DataProtection/DataProtection/src/AuthenticatedEncryption/IAuthenticatedEncryptor.cs Added new span-based APIs to the authenticated encryptor interface
src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtector.cs Core implementation of new APIs with proper header management
src/DataProtection/DataProtection/src/Managed/ManagedAuthenticatedEncryptor.cs Implementation for managed (non-CNG) encryption algorithms
src/DataProtection/DataProtection/src/Managed/AesGcmAuthenticatedEncryptor.cs Implementation for AES-GCM encryption
src/DataProtection/DataProtection/src/Cng/CbcAuthenticatedEncryptor.cs Implementation for CNG-based CBC encryption
src/DataProtection/DataProtection/src/Cng/CngGcmAuthenticatedEncryptor.cs Implementation for CNG-based GCM encryption
src/DataProtection/Extensions/src/TimeLimitedDataProtector.cs Updated time-limited protector to support new APIs
Multiple test files Comprehensive test coverage for the new functionality

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 think we should update our existing code that calls into IDataProtecter to use the new methods to get good test coverage of the new methods.

@martincostello
Copy link
Member

Was just looking at some profiles for one of my apps and spotted that DataProtection was on the hot path for rendering a page:

image

I think that ultimately comes down to here:

var bytes = _cryptoSystem.Protect(stream.ToArray());

Might be an interesting code path to try out the new APIs added here once merged.

@DeagleGross
Copy link
Member Author

@martincostello that was the initial idea - to improve Antiforgery and other places where DataProtection is used under the hood - for example Blazor.

I have some starters for Antiforgery perf improvements, but this one was the biggest blocker to make an alloc-free flow and I hope this and follow-up changes will help there

@DeagleGross DeagleGross changed the title feat: "Spanify" DataProtector Protect [part 1] feat: "Spanify" DataProtector Aug 4, 2025
@DeagleGross
Copy link
Member Author

Pushed the dotnetbenchmark project showing the updated numbers. Results on my machine:
image

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