Skip to content

Commit b0fc525

Browse files
committed
refactor to ISpan interfaces
1 parent bbfce53 commit b0fc525

File tree

9 files changed

+64
-42
lines changed

9 files changed

+64
-42
lines changed

src/DataProtection/Abstractions/src/IOptimizedDataProtector.cs renamed to src/DataProtection/Abstractions/src/ISpanDataProtector.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,21 @@
99

1010
namespace Microsoft.AspNetCore.DataProtection;
1111

12-
#if NET10_0_OR_GREATER
13-
1412
/// <summary>
1513
/// An interface that can provide data protection services.
1614
/// Is an optimized version of <see cref="IDataProtector"/>.
1715
/// </summary>
18-
public interface IOptimizedDataProtector : IDataProtector
16+
public interface ISpanDataProtector : IDataProtector
1917
{
2018
/// <summary>
21-
/// Returns the size of the encrypted data for a given plaintext length.
19+
/// Determines the size of the protected data in order to then use <see cref="TryProtect(ReadOnlySpan{byte}, Span{byte}, out int)"/>."/>.
20+
/// <br/> Returns the boolean representing if current implementation of data protector supports <see cref="ISpanDataProtector"/> or not.
21+
/// If it does not (returns false), then one needs to fallback to <see cref="IDataProtector"/> and use <see cref="IDataProtector.Protect(byte[])"/> and <see cref="IDataProtector.Unprotect(byte[])"/> methods instead.
2222
/// </summary>
2323
/// <param name="plainText">The plain text that will be encrypted later</param>
24-
/// <returns>The length of the encrypted data</returns>
25-
int GetProtectedSize(ReadOnlySpan<byte> plainText);
24+
/// <param name="cipherTextLength">The length of the expected cipher text.</param>
25+
/// <returns>true, if <see cref="ISpanDataProtector"/> is supported. False if a fallback to <see cref="IDataProtector"/> is required.</returns>
26+
bool TryGetProtectedSize(ReadOnlySpan<byte> plainText, out int cipherTextLength);
2627

2728
/// <summary>
2829
/// Attempts to encrypt and tamper-proof a piece of data.
@@ -33,5 +34,3 @@ public interface IOptimizedDataProtector : IDataProtector
3334
/// <returns>true if destination is long enough to receive the encrypted data; otherwise, false.</returns>
3435
bool TryProtect(ReadOnlySpan<byte> plainText, Span<byte> destination, out int bytesWritten);
3536
}
36-
37-
#endif
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#nullable enable
2-
Microsoft.AspNetCore.DataProtection.IOptimizedDataProtector
3-
Microsoft.AspNetCore.DataProtection.IOptimizedDataProtector.GetProtectedSize(System.ReadOnlySpan<byte> plainText) -> int
4-
Microsoft.AspNetCore.DataProtection.IOptimizedDataProtector.TryProtect(System.ReadOnlySpan<byte> plainText, System.Span<byte> destination, out int bytesWritten) -> bool
2+
Microsoft.AspNetCore.DataProtection.ISpanDataProtector
3+
Microsoft.AspNetCore.DataProtection.ISpanDataProtector.TryGetProtectedSize(System.ReadOnlySpan<byte> plainText, out int cipherTextLength) -> bool
4+
Microsoft.AspNetCore.DataProtection.ISpanDataProtector.TryProtect(System.ReadOnlySpan<byte> plainText, System.Span<byte> destination, out int bytesWritten) -> bool

src/DataProtection/DataProtection/src/AuthenticatedEncryption/ISpanAuthenticatedEncryptor.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
1111

12+
/// <summary>
13+
/// Provides an authenticated encryption and decryption routine via a span-based API.
14+
/// </summary>
1215
public interface ISpanAuthenticatedEncryptor : IAuthenticatedEncryptor
1316
{
1417
/// <summary>

src/DataProtection/DataProtection/src/KeyManagement/KeyRingBasedDataProtector.cs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
namespace Microsoft.AspNetCore.DataProtection.KeyManagement;
2323

24-
internal sealed unsafe class KeyRingBasedDataProtector : IDataProtector, IPersistedDataProtector
24+
internal sealed unsafe class KeyRingBasedDataProtector : ISpanDataProtector, IPersistedDataProtector
2525
{
2626
// This magic header identifies a v0 protected data blob. It's the high 28 bits of the SHA1 hash of
2727
// "Microsoft.AspNet.DataProtection.KeyManagement.KeyRingBasedDataProtector" [US-ASCII], big-endian.
@@ -92,21 +92,23 @@ public byte[] DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErro
9292
return retVal;
9393
}
9494

95-
#if NET10_0_OR_GREATER
96-
public int GetProtectedSize(ReadOnlySpan<byte> plainText)
95+
public bool TryGetProtectedSize(ReadOnlySpan<byte> plainText, out int cipherTextLength)
9796
{
97+
cipherTextLength = default;
98+
9899
// Get the current key ring to access the encryptor
99100
var currentKeyRing = _keyRingProvider.GetCurrentKeyRing();
100101
var defaultEncryptor = currentKeyRing.DefaultAuthenticatedEncryptor;
101-
if (defaultEncryptor is not IOptimizedAuthenticatedEncryptor optimizedAuthenticatedEncryptor)
102+
if (defaultEncryptor is not ISpanAuthenticatedEncryptor optimizedAuthenticatedEncryptor)
102103
{
103-
throw new NotSupportedException("The current default encryptor does not support optimized protection.");
104+
return false;
104105
}
105106
CryptoUtil.Assert(optimizedAuthenticatedEncryptor != null, "optimizedAuthenticatedEncryptor != null");
106107

107108
// We allocate a 20-byte pre-buffer so that we can inject the magic header and key id into the return value.
108109
// See Protect() / TryProtect() for details
109-
return _magicHeaderKeyIdSize + optimizedAuthenticatedEncryptor.GetEncryptedSize(plainText.Length);
110+
cipherTextLength = _magicHeaderKeyIdSize + optimizedAuthenticatedEncryptor.GetEncryptedSize(plainText.Length);
111+
return true;
110112
}
111113

112114
public bool TryProtect(ReadOnlySpan<byte> plaintext, Span<byte> destination, out int bytesWritten)
@@ -117,11 +119,11 @@ public bool TryProtect(ReadOnlySpan<byte> plaintext, Span<byte> destination, out
117119
var currentKeyRing = _keyRingProvider.GetCurrentKeyRing();
118120
var defaultKeyId = currentKeyRing.DefaultKeyId;
119121
var defaultEncryptor = currentKeyRing.DefaultAuthenticatedEncryptor;
120-
if (defaultEncryptor is not IOptimizedAuthenticatedEncryptor optimizedAuthenticatedEncryptor)
122+
if (defaultEncryptor is not ISpanAuthenticatedEncryptor spanEncryptor)
121123
{
122124
throw new NotSupportedException("The current default encryptor does not support optimized protection.");
123125
}
124-
CryptoUtil.Assert(optimizedAuthenticatedEncryptor != null, "optimizedAuthenticatedEncryptor != null");
126+
CryptoUtil.Assert(spanEncryptor != null, "optimizedAuthenticatedEncryptor != null");
125127

126128
if (_logger.IsDebugLevelEnabled())
127129
{
@@ -135,15 +137,24 @@ public bool TryProtect(ReadOnlySpan<byte> plaintext, Span<byte> destination, out
135137
var preBufferSize = _magicHeaderKeyIdSize;
136138
var postBufferSize = 0;
137139
var destinationBufferOffsets = destination.Slice(preBufferSize, destination.Length - (preBufferSize + postBufferSize));
138-
var success = optimizedAuthenticatedEncryptor.TryEncrypt(plaintext, aad, destinationBufferOffsets, out bytesWritten);
140+
var success = spanEncryptor.TryEncrypt(plaintext, aad, destinationBufferOffsets, out bytesWritten);
139141

140142
// At this point: destination := { 000..000 || encryptorSpecificProtectedPayload },
141143
// where 000..000 is a placeholder for our magic header and key id.
142144

143145
// Write out the magic header and key id
146+
#if NET10_0_OR_GREATER
144147
BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(0, sizeof(uint)), MAGIC_HEADER_V0);
145148
var writeKeyIdResult = defaultKeyId.TryWriteBytes(destination.Slice(sizeof(uint), sizeof(Guid)));
146149
Debug.Assert(writeKeyIdResult, "Failed to write Guid to destination.");
150+
#else
151+
fixed (byte* pbRetVal = destination)
152+
{
153+
WriteBigEndianInteger(pbRetVal, MAGIC_HEADER_V0);
154+
WriteGuid(&pbRetVal[sizeof(uint)], defaultKeyId);
155+
}
156+
#endif
157+
147158
bytesWritten += _magicHeaderKeyIdSize;
148159

149160
// At this point, destination := { magicHeader || keyId || encryptorSpecificProtectedPayload }
@@ -156,7 +167,6 @@ public bool TryProtect(ReadOnlySpan<byte> plaintext, Span<byte> destination, out
156167
throw Error.Common_EncryptionFailed(ex);
157168
}
158169
}
159-
#endif
160170

161171
public byte[] Protect(byte[] plaintext)
162172
{
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#nullable enable
2-
Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IOptimizedAuthenticatedEncryptor
3-
Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IOptimizedAuthenticatedEncryptor.GetEncryptedSize(int plainTextLength) -> int
4-
Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IOptimizedAuthenticatedEncryptor.TryEncrypt(System.ReadOnlySpan<byte> plaintext, System.ReadOnlySpan<byte> additionalAuthenticatedData, System.Span<byte> destination, out int bytesWritten) -> bool
2+
Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ISpanAuthenticatedEncryptor
3+
Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ISpanAuthenticatedEncryptor.GetEncryptedSize(int plainTextLength) -> int
4+
Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ISpanAuthenticatedEncryptor.TryEncrypt(System.ReadOnlySpan<byte> plaintext, System.ReadOnlySpan<byte> additionalAuthenticatedData, System.Span<byte> destination, out int bytesWritten) -> bool

src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Internal/RoundtripEncryptionHelpers.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Buffers;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Text;
89
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
910

@@ -28,20 +29,23 @@ public static void AssertTryEncryptTryDecryptParity(IAuthenticatedEncryptor encr
2829
/// </summary>
2930
public static void AssertTryEncryptTryDecryptParity(IAuthenticatedEncryptor encryptor, ArraySegment<byte> plaintext, ArraySegment<byte> aad)
3031
{
32+
var spanAuthenticatedEncryptor = encryptor as ISpanAuthenticatedEncryptor;
33+
Debug.Assert(spanAuthenticatedEncryptor != null, "ISpanDataProtector is not supported by the encryptor");
34+
3135
// assert "allocatey" Encrypt/Decrypt APIs roundtrip correctly
3236
byte[] ciphertext = encryptor.Encrypt(plaintext, aad);
3337
byte[] decipheredtext = encryptor.Decrypt(new ArraySegment<byte>(ciphertext), aad);
3438
Assert.Equal(plaintext.AsSpan(), decipheredtext.AsSpan());
3539

3640
// assert calculated size is correct
37-
var expectedSize = encryptor.GetEncryptedSize(plaintext.Count);
41+
var expectedSize = spanAuthenticatedEncryptor.GetEncryptedSize(plaintext.Count);
3842
Assert.Equal(expectedSize, ciphertext.Length);
3943

4044
// perform TryEncrypt and Decrypt roundtrip - ensures cross operation compatibility
4145
var cipherTextPooled = ArrayPool<byte>.Shared.Rent(expectedSize);
4246
try
4347
{
44-
var tryEncryptResult = encryptor.TryEncrypt(plaintext, aad, cipherTextPooled, out var bytesWritten);
48+
var tryEncryptResult = spanAuthenticatedEncryptor.TryEncrypt(plaintext, aad, cipherTextPooled, out var bytesWritten);
4549
Assert.Equal(expectedSize, bytesWritten);
4650
Assert.True(tryEncryptResult);
4751

src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/KeyRingBasedDataProtectorTests.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Buffers.Binary;
5+
using System.Diagnostics;
56
using System.Globalization;
67
using System.Net;
78
using System.Security.Cryptography;
@@ -656,7 +657,8 @@ public void GetProtectedSize_TryProtect_CorrectlyEstimatesDataLength_MultipleSce
656657
newPurpose: "purpose");
657658

658659
// Act - get estimated size
659-
int estimatedSize = protector.GetProtectedSize(plaintext);
660+
var protectionSizeResult = protector.TryGetProtectedSize(plaintext, out var estimatedSize);
661+
Assert.True(protectionSizeResult, "TryGetProtectedSize should succeed");
660662

661663
// verify simple protect works
662664
var protectedData = protector.Protect(plaintext);
@@ -716,8 +718,9 @@ public void GetProtectedSize_TryProtect_VariousPlaintextSizes(int plaintextSize)
716718
newPurpose: "purpose");
717719

718720
// Act - get estimated size
719-
int estimatedSize = protector.GetProtectedSize(plaintext);
720-
721+
var protectionSizeResult = protector.TryGetProtectedSize(plaintext, out var estimatedSize);
722+
Assert.True(protectionSizeResult, "TryGetProtectedSize should succeed");
723+
721724
// Act - allocate buffer and try protect
722725
byte[] destination = new byte[estimatedSize];
723726
bool success = protector.TryProtect(plaintext, destination, out int bytesWritten);

src/DataProtection/Extensions/src/DataProtectionAdvancedExtensions.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public static string Unprotect(this ITimeLimitedDataProtector protector, string
9898

9999
private sealed class TimeLimitedWrappingProtector : IDataProtector
100100
#if NET10_0_OR_GREATER
101-
, IOptimizedDataProtector
101+
, ISpanDataProtector
102102
#endif
103103
{
104104
public DateTimeOffset Expiration;
@@ -131,19 +131,20 @@ public byte[] Unprotect(byte[] protectedData)
131131
}
132132

133133
#if NET10_0_OR_GREATER
134-
public int GetProtectedSize(ReadOnlySpan<byte> plainText)
134+
public bool TryGetProtectedSize(ReadOnlySpan<byte> plainText, out int cipherTextLength)
135135
{
136-
if (_innerProtector is IOptimizedDataProtector optimizedDataProtector)
136+
if (_innerProtector is ISpanDataProtector optimizedDataProtector)
137137
{
138-
return optimizedDataProtector.GetProtectedSize(plainText);
138+
return optimizedDataProtector.TryGetProtectedSize(plainText, out cipherTextLength);
139139
}
140140

141-
throw new NotSupportedException("The inner protector does not support optimized data protection.");
141+
cipherTextLength = default;
142+
return false;
142143
}
143144

144145
public bool TryProtect(ReadOnlySpan<byte> plainText, Span<byte> destination, out int bytesWritten)
145146
{
146-
if (_innerProtector is IOptimizedDataProtector optimizedDataProtector)
147+
if (_innerProtector is ISpanDataProtector optimizedDataProtector)
147148
{
148149
return optimizedDataProtector.TryProtect(plainText, destination, out bytesWritten);
149150
}

src/DataProtection/Extensions/src/TimeLimitedDataProtector.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.DataProtection;
1818
/// </summary>
1919
internal sealed class TimeLimitedDataProtector : ITimeLimitedDataProtector
2020
#if NET10_0_OR_GREATER
21-
, IOptimizedDataProtector
21+
, ISpanDataProtector
2222
#endif
2323
{
2424
private const string MyPurposeString = "Microsoft.AspNetCore.DataProtection.TimeLimitedDataProtector.v1";
@@ -134,27 +134,29 @@ byte[] IDataProtector.Unprotect(byte[] protectedData)
134134
}
135135

136136
#if NET10_0_OR_GREATER
137-
public int GetProtectedSize(ReadOnlySpan<byte> plainText)
137+
public bool TryGetProtectedSize(ReadOnlySpan<byte> plainText, out int cipherTextLength)
138138
{
139139
var dataProtector = GetInnerProtectorWithTimeLimitedPurpose();
140-
if (dataProtector is IOptimizedDataProtector optimizedDataProtector)
140+
if (dataProtector is ISpanDataProtector optimizedDataProtector)
141141
{
142-
var size = optimizedDataProtector.GetProtectedSize(plainText);
142+
var result = optimizedDataProtector.TryGetProtectedSize(plainText, out cipherTextLength);
143143

144144
// prepended the expiration time as a 64-bit UTC tick count takes ExpirationTimeHeaderSize bytes;
145145
// see Protect(byte[] plaintext, DateTimeOffset expiration) for details
146-
return size + ExpirationTimeHeaderSize;
146+
cipherTextLength += ExpirationTimeHeaderSize;
147+
return result;
147148
}
148149

149-
throw new NotSupportedException("The inner protector does not support optimized data protection.");
150+
cipherTextLength = default;
151+
return false;
150152
}
151153

152154
public bool TryProtect(ReadOnlySpan<byte> plaintext, Span<byte> destination, out int bytesWritten)
153155
=> TryProtect(plaintext, destination, DateTimeOffset.MaxValue, out bytesWritten);
154156

155157
public bool TryProtect(ReadOnlySpan<byte> plaintext, Span<byte> destination, DateTimeOffset expiration, out int bytesWritten)
156158
{
157-
if (_innerProtector is not IOptimizedDataProtector optimizedDataProtector)
159+
if (_innerProtector is not ISpanDataProtector optimizedDataProtector)
158160
{
159161
throw new NotSupportedException("The inner protector does not support optimized data protection.");
160162
}

0 commit comments

Comments
 (0)