Skip to content

Commit cdf9957

Browse files
committed
-10% so far
1 parent 0230498 commit cdf9957

File tree

9 files changed

+338
-36
lines changed

9 files changed

+338
-36
lines changed

src/Antiforgery/src/Internal/DefaultAntiforgeryTokenSerializer.cs

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
5+
using System.Diagnostics;
46
using Microsoft.AspNetCore.DataProtection;
57
using Microsoft.AspNetCore.WebUtilities;
68
using Microsoft.Extensions.ObjectPool;
@@ -30,26 +32,30 @@ public AntiforgeryToken Deserialize(string serializedToken)
3032
{
3133
var serializationContext = _pool.Get();
3234

35+
byte[]? rented = null;
3336
Exception? innerException = null;
3437
try
3538
{
36-
var count = serializedToken.Length;
37-
var charsRequired = WebEncoders.GetArraySizeRequiredToDecode(count);
38-
var chars = serializationContext.GetChars(charsRequired);
39-
var tokenBytes = WebEncoders.Base64UrlDecode(
39+
var tokenLength = serializedToken.Length;
40+
var charsRequired = WebEncoders.GetArraySizeRequiredToDecode(tokenLength);
41+
42+
var chars = charsRequired < 128 ? stackalloc byte[128] : (rented = ArrayPool<byte>.Shared.Rent(charsRequired));
43+
chars = chars[..charsRequired];
44+
45+
var decodedResult = WebEncoders.TryBase64UrlDecode(
4046
serializedToken,
4147
offset: 0,
42-
buffer: chars,
43-
bufferOffset: 0,
44-
count: count);
45-
48+
count: tokenLength,
49+
destination: chars,
50+
out var bytesWritten);
51+
Debug.Assert(decodedResult is true);
52+
53+
// when DataProtection obtains Span<>'ish APIs,
54+
// we will just pass in chars directly. For now we need to allocate.
55+
var tokenBytes = chars.Slice(0, bytesWritten).ToArray();
4656
var unprotectedBytes = _cryptoSystem.Unprotect(tokenBytes);
47-
var stream = serializationContext.Stream;
48-
stream.Write(unprotectedBytes, offset: 0, count: unprotectedBytes.Length);
49-
stream.Position = 0L;
5057

51-
var reader = serializationContext.Reader;
52-
var token = Deserialize(reader);
58+
var token = Deserialize(unprotectedBytes);
5359
if (token != null)
5460
{
5561
return token;
@@ -63,6 +69,11 @@ public AntiforgeryToken Deserialize(string serializedToken)
6369
finally
6470
{
6571
_pool.Return(serializationContext);
72+
73+
if (rented is not null)
74+
{
75+
ArrayPool<byte>.Shared.Return(rented);
76+
}
6677
}
6778

6879
// if we reached this point, something went wrong deserializing
@@ -81,39 +92,49 @@ public AntiforgeryToken Deserialize(string serializedToken)
8192
* | `- Username: UTF-8 string with 7-bit integer length prefix
8293
* `- AdditionalData: UTF-8 string with 7-bit integer length prefix
8394
*/
84-
private static AntiforgeryToken? Deserialize(BinaryReader reader)
95+
private static AntiforgeryToken? Deserialize(ReadOnlySpan<byte> unprotectedBytes)
8596
{
97+
// pointer to current byte at unprotectedBytes
98+
var pointer = 0;
99+
86100
// we can only consume tokens of the same serialized version that we generate
87-
var embeddedVersion = reader.ReadByte();
101+
var embeddedVersion = unprotectedBytes[pointer++];
88102
if (embeddedVersion != TokenVersion)
89103
{
90104
return null;
91105
}
92106

93107
var deserializedToken = new AntiforgeryToken();
94-
var securityTokenBytes = reader.ReadBytes(AntiforgeryToken.SecurityTokenBitLength / 8);
95-
deserializedToken.SecurityToken =
96-
new BinaryBlob(AntiforgeryToken.SecurityTokenBitLength, securityTokenBytes);
97-
deserializedToken.IsCookieToken = reader.ReadBoolean();
98108

109+
var securityTokenBytesLength = AntiforgeryToken.SecurityTokenBitLength / 8;
110+
var securityTokenBytes = unprotectedBytes.Slice(pointer, securityTokenBytesLength).ToArray();
111+
pointer += securityTokenBytesLength;
112+
deserializedToken.SecurityToken = new BinaryBlob(AntiforgeryToken.SecurityTokenBitLength, securityTokenBytes);
113+
114+
deserializedToken.IsCookieToken = unprotectedBytes.BinaryReadBoolean(ref pointer);
99115
if (!deserializedToken.IsCookieToken)
100116
{
101-
var isClaimsBased = reader.ReadBoolean();
117+
var isClaimsBased = unprotectedBytes.BinaryReadBoolean(ref pointer);
102118
if (isClaimsBased)
103119
{
104-
var claimUidBytes = reader.ReadBytes(AntiforgeryToken.ClaimUidBitLength / 8);
120+
var claimUidBytesLength = AntiforgeryToken.ClaimUidBitLength / 8;
121+
var claimUidBytes = unprotectedBytes.Slice(pointer, claimUidBytesLength).ToArray();
122+
pointer += claimUidBytesLength;
105123
deserializedToken.ClaimUid = new BinaryBlob(AntiforgeryToken.ClaimUidBitLength, claimUidBytes);
106124
}
107125
else
108126
{
109-
deserializedToken.Username = reader.ReadString();
127+
deserializedToken.Username = unprotectedBytes.Slice(pointer).BinaryReadString(out var usernameBytesRead);
128+
pointer += usernameBytesRead;
110129
}
111130

112-
deserializedToken.AdditionalData = reader.ReadString();
131+
deserializedToken.AdditionalData = unprotectedBytes.Slice(pointer).BinaryReadString(out var addDataBytesRead);
132+
pointer += addDataBytesRead;
133+
113134
}
114135

115136
// if there's still unconsumed data in the stream, fail
116-
if (reader.BaseStream.ReadByte() != -1)
137+
if (pointer < unprotectedBytes.Length && unprotectedBytes[pointer] != 0x00)
117138
{
118139
return null;
119140
}

src/Antiforgery/src/Internal/DefaultClaimUidExtractor.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
5+
using System.Buffers.Binary;
46
using System.Diagnostics;
57
using System.Security.Claims;
68
using System.Security.Cryptography;
9+
using System.Text;
710
using Microsoft.Extensions.ObjectPool;
811

912
namespace Microsoft.AspNetCore.Antiforgery;
@@ -32,8 +35,11 @@ public DefaultClaimUidExtractor(ObjectPool<AntiforgerySerializationContext> pool
3235
return null;
3336
}
3437

38+
// todo skip allocations here as well
3539
var claimUidBytes = ComputeSha256(uniqueIdentifierParameters);
36-
return Convert.ToBase64String(claimUidBytes);
40+
41+
Convert.TryToBase64Chars(claimUidBytes, out var str, out int charsWritten);
42+
return str.ToString;
3743
}
3844

3945
public static IList<string>? GetUniqueIdentifierParameters(IEnumerable<ClaimsIdentity> claimsIdentities)
@@ -119,7 +125,53 @@ public DefaultClaimUidExtractor(ObjectPool<AntiforgerySerializationContext> pool
119125
return identifierParameters;
120126
}
121127

122-
private byte[] ComputeSha256(IEnumerable<string> parameters)
128+
private void ComputeSha256(IEnumerable<string> parameters, Span<byte> output)
129+
{
130+
// compute total size
131+
int totalSize = 0;
132+
foreach (var param in parameters)
133+
{
134+
int byteCount = Encoding.UTF8.GetByteCount(param);
135+
totalSize += 4 + byteCount; // 4 bytes for length prefix
136+
}
137+
138+
byte[]? rented = null;
139+
var buffer = totalSize <= 256
140+
? stackalloc byte[256]
141+
: (rented = ArrayPool<byte>.Shared.Rent(totalSize));
142+
buffer = buffer[..totalSize];
143+
144+
try
145+
{
146+
int offset = 0;
147+
148+
foreach (var param in parameters)
149+
{
150+
int byteCount = Encoding.UTF8.GetByteCount(param);
151+
152+
// Write 4-byte length prefix
153+
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(offset, 4), byteCount);
154+
offset += 4;
155+
156+
// Write UTF-8 bytes
157+
Encoding.UTF8.GetBytes(param, buffer.Slice(offset, byteCount));
158+
offset += byteCount;
159+
}
160+
161+
SHA256.TryHashData(buffer.Slice(0, totalSize), output, out int bytesWritten);
162+
Debug.Assert(bytesWritten == 32);
163+
}
164+
finally
165+
{
166+
buffer.Clear(); // security ?
167+
if (rented is not null)
168+
{
169+
ArrayPool<byte>.Shared.Return(rented);
170+
}
171+
}
172+
}
173+
174+
private byte[] ComputeSha256StreamWriter(IEnumerable<string> parameters)
123175
{
124176
var serializationContext = _pool.Get();
125177

src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<Description>An antiforgery system for ASP.NET Core designed to generate and validate tokens to prevent Cross-Site Request Forgery attacks.</Description>
@@ -26,5 +26,7 @@
2626

2727
<ItemGroup>
2828
<Compile Include="$(SharedSourceRoot)HttpExtensions.cs" LinkBase="Shared"/>
29+
<Compile Include="$(SharedSourceRoot)ReadOnlySpanExtensions.cs" LinkBase="Shared"/>
30+
<Compile Include="$(SharedSourceRoot)Encoding\Int7BitEncodingUtils.cs" LinkBase="Shared"/>
2931
</ItemGroup>
3032
</Project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
static Microsoft.AspNetCore.WebUtilities.WebEncoders.TryBase64UrlDecode(string! input, int offset, int count, System.Span<byte> destination, out int bytesWritten) -> bool

src/Shared/Encoding/Int7BitEncodingUtils.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,37 @@ public static int Measure7BitEncodedUIntLength(this uint value)
2828
#endif
2929
}
3030

31+
public static int Read7BitEncodedInt(this ReadOnlySpan<byte> span, out int bytesRead)
32+
{
33+
int result = 0;
34+
int shift = 0;
35+
bytesRead = 0;
36+
37+
while (true)
38+
{
39+
if (bytesRead >= span.Length)
40+
{
41+
throw new InvalidOperationException("Invalid 7-bit encoded int.");
42+
}
43+
44+
byte b = span[bytesRead++];
45+
result |= (b & 0x7F) << shift;
46+
47+
if ((b & 0x80) == 0)
48+
{
49+
break;
50+
}
51+
52+
shift += 7;
53+
if (shift > 35)
54+
{
55+
throw new FormatException("7-bit encoded int too large.");
56+
}
57+
}
58+
59+
return result;
60+
}
61+
3162
public static int Write7BitEncodedInt(this Span<byte> target, int value)
3263
=> Write7BitEncodedInt(target, (uint)value);
3364

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
using System.Reflection;
6+
using System.Text;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Http.Features;
9+
using Microsoft.AspNetCore.Shared;
10+
11+
internal static class ReadOnlySpanExtensions
12+
{
13+
public static bool BinaryReadBoolean(this ReadOnlySpan<byte> span, ref int offset)
14+
{
15+
return span[offset++] != 0;
16+
}
17+
18+
public static string BinaryReadString(this ReadOnlySpan<byte> span, out int totalBytesRead)
19+
{
20+
int length = span.Read7BitEncodedInt(out int prefixLength);
21+
int stringEnd = prefixLength + length;
22+
23+
if (span.Length < stringEnd)
24+
{
25+
throw new InvalidOperationException("Not enough data to read the string.");
26+
}
27+
28+
var strBytes = span.Slice(prefixLength, length);
29+
totalBytesRead = stringEnd;
30+
31+
return Encoding.UTF8.GetString(strBytes);
32+
}
33+
}

src/Shared/WebEncoders/Properties/EncoderResources.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ namespace Microsoft.Extensions.WebEncoders.Sources;
99
// in the contentFiles section. Revisit once we convert repos to MSBuild
1010
internal static class EncoderResources
1111
{
12+
/// <summary>
13+
/// Invalid {0}, {1} or {2} length.
14+
/// </summary>
15+
internal const string WebEncoders_InvalidCountOrLength = "Invalid {0} or {1} length.";
16+
1217
/// <summary>
1318
/// Invalid {0}, {1} or {2} length.
1419
/// </summary>

0 commit comments

Comments
 (0)