Skip to content

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.

@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

@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 Aug 18, 2025
@DeagleGross
Copy link
Member Author

/azp run

@dotnet-policy-service dotnet-policy-service bot removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Oct 9, 2025
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

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
4 participants