| File: Internal\DefaultAntiforgeryTokenSerializer.cs | Web Access |
| Project: src\aspnetcore\src\Antiforgery\src\Microsoft.AspNetCore.Antiforgery.csproj (Microsoft.AspNetCore.Antiforgery) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Buffers.Text; using System.Text; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Shared; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Antiforgery; internal sealed class DefaultAntiforgeryTokenSerializer : IAntiforgeryTokenSerializer { private const string Purpose = "Microsoft.AspNetCore.Antiforgery.AntiforgeryToken.v1"; private const byte TokenVersion = 0x01; private readonly IDataProtector _defaultCryptoSystem; private readonly ISpanDataProtector? _perfCryptoSystem; public DefaultAntiforgeryTokenSerializer(IDataProtectionProvider provider) { ArgumentNullException.ThrowIfNull(provider); _defaultCryptoSystem = provider.CreateProtector(Purpose); _perfCryptoSystem = _defaultCryptoSystem as ISpanDataProtector; } public AntiforgeryToken Deserialize(string serializedToken) { byte[]? tokenBytesRent = null; Exception? innerException = null; try { var maxTokenDecodedSize = Base64Url.GetMaxDecodedLength(serializedToken.Length); var rent = maxTokenDecodedSize <= 256 ? stackalloc byte[256] : (tokenBytesRent = ArrayPool<byte>.Shared.Rent(maxTokenDecodedSize)); var tokenBytes = rent[..maxTokenDecodedSize]; var bytesWritten = WebEncoders.Base64UrlDecode(serializedToken, tokenBytes); var tokenBytesDecoded = tokenBytes[..bytesWritten]; if (_perfCryptoSystem is not null) { var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[256]); try { _perfCryptoSystem.Unprotect(tokenBytesDecoded, ref protectBuffer); var token = Deserialize(protectBuffer.WrittenSpan); if (token is not null) { return token; } } finally { protectBuffer.Dispose(); } } else { var unprotectedBytes = _defaultCryptoSystem.Unprotect(tokenBytesDecoded.ToArray()); var token = Deserialize(unprotectedBytes); if (token is not null) { return token; } } } catch (Exception ex) { // swallow all exceptions - homogenize error if something went wrong innerException = ex; } finally { if (tokenBytesRent is not null) { ArrayPool<byte>.Shared.Return(tokenBytesRent); } } // if we reached this point, something went wrong deserializing throw new AntiforgeryValidationException(Resources.AntiforgeryToken_DeserializationFailed, innerException); } /* The serialized format of the anti-XSRF token is as follows: * Version: 1 byte integer * SecurityToken: 16 byte binary blob * IsCookieToken: 1 byte Boolean * [if IsCookieToken != true] * +- IsClaimsBased: 1 byte Boolean * | [if IsClaimsBased = true] * | `- ClaimUid: 32 byte binary blob * | [if IsClaimsBased = false] * | `- Username: UTF-8 string with 7-bit integer length prefix * `- AdditionalData: UTF-8 string with 7-bit integer length prefix */ private static AntiforgeryToken? Deserialize(ReadOnlySpan<byte> tokenBytes) { // Minimum lengths: // - Cookie token: 1 (version) + 16 (securityToken) + 1 (isCookieToken) = 18 bytes // - Request token (username): 18 + 1 (isClaimsBased) + 1 (username prefix) + 1 (additionalData prefix) = 21 bytes // - Request token (claims): 18 + 1 (isClaimsBased) + 32 (claimUid) + 1 (additionalData prefix) = 52 bytes const int minCookieTokenLength = 1 + (AntiforgeryToken.SecurityTokenBitLength / 8) + 1; // 18 bytes const int minRequestTokenPayloadLength = 1 + 1 + 1; // 3 bytes: isClaimsBased + username prefix + additionalData prefix if (tokenBytes.Length < minCookieTokenLength) { return null; } var embeddedVersion = tokenBytes[0]; tokenBytes = tokenBytes[1..]; if (embeddedVersion != TokenVersion) { return null; } var deserializedToken = new AntiforgeryToken(); // Read SecurityToken (16 bytes) const int securityTokenByteLength = AntiforgeryToken.SecurityTokenBitLength / 8; deserializedToken.SecurityToken = new BinaryBlob( AntiforgeryToken.SecurityTokenBitLength, tokenBytes[..securityTokenByteLength].ToArray()); tokenBytes = tokenBytes[securityTokenByteLength..]; // Read IsCookieToken (1 byte) deserializedToken.IsCookieToken = tokenBytes[0] != 0; tokenBytes = tokenBytes[1..]; if (!deserializedToken.IsCookieToken) { // Validate minimum length for request token payload (after header has been consumed) if (tokenBytes.Length < minRequestTokenPayloadLength) { return null; } // Read IsClaimsBased (1 byte) var isClaimsBased = tokenBytes[0] != 0; tokenBytes = tokenBytes[1..]; if (isClaimsBased) { // Read ClaimUid (32 bytes) const int claimUidByteLength = AntiforgeryToken.ClaimUidBitLength / 8; if (tokenBytes.Length < claimUidByteLength + 1) // +1 for additionalData prefix { return null; } deserializedToken.ClaimUid = new BinaryBlob( AntiforgeryToken.ClaimUidBitLength, tokenBytes[..claimUidByteLength].ToArray()); tokenBytes = tokenBytes[claimUidByteLength..]; } else { // Read Username (7-bit encoded length prefix + UTF-8 string) var usernameLength = tokenBytes.Read7BitEncodedString(out var username); tokenBytes = tokenBytes[usernameLength..]; deserializedToken.Username = username; } var additionalDataLength = tokenBytes.Read7BitEncodedString(out var additionalData); tokenBytes = tokenBytes[additionalDataLength..]; deserializedToken.AdditionalData = additionalData; } // if there's still unconsumed data in the span, fail if (tokenBytes.Length != 0) { return null; } // success return deserializedToken; } public string Serialize(AntiforgeryToken token) { ArgumentNullException.ThrowIfNull(token); var securityTokenBytes = token.SecurityToken.GetData(); var claimUidBytes = token.ClaimUid?.GetData(); var totalSize = 1 // TokenVersion + securityTokenBytes.Length + // SecurityToken + 1; // IsCookieToken if (!token.IsCookieToken) { totalSize += 1; // isClaimsBased if (token.ClaimUid is not null) { totalSize += claimUidBytes!.Length; } else { var usernameByteCount = Encoding.UTF8.GetByteCount(token.Username!); totalSize += usernameByteCount.Measure7BitEncodedUIntLength() + usernameByteCount; } var additionalDataByteCount = Encoding.UTF8.GetByteCount(token.AdditionalData); totalSize += additionalDataByteCount.Measure7BitEncodedUIntLength() + additionalDataByteCount; } byte[]? tokenBytesRent = null; var rent = totalSize < 256 ? stackalloc byte[256] : (tokenBytesRent = ArrayPool<byte>.Shared.Rent(totalSize)); var tokenBytes = rent[..totalSize]; try { var offset = 0; tokenBytes[offset++] = TokenVersion; securityTokenBytes.CopyTo(tokenBytes.Slice(offset, securityTokenBytes.Length)); offset += securityTokenBytes.Length; tokenBytes[offset++] = token.IsCookieToken ? (byte)1 : (byte)0; if (!token.IsCookieToken) { if (token.ClaimUid != null) { tokenBytes[offset++] = 1; // isClaimsBased claimUidBytes!.CopyTo(tokenBytes.Slice(offset, claimUidBytes!.Length)); offset += claimUidBytes.Length; } else { tokenBytes[offset++] = 0; // isClaimsBased offset += tokenBytes[offset..].Write7BitEncodedString(token.Username!); } offset += tokenBytes[offset..].Write7BitEncodedString(token.AdditionalData); } if (_perfCryptoSystem is not null) { var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[256]); try { _perfCryptoSystem.Protect(tokenBytes, ref protectBuffer); return Base64Url.EncodeToString(protectBuffer.WrittenSpan); } finally { protectBuffer.Dispose(); } } else { var protectedBytes = _defaultCryptoSystem.Protect(tokenBytes.ToArray()); return Base64Url.EncodeToString(protectedBytes); } } finally { if (tokenBytesRent is not null) { ArrayPool<byte>.Shared.Return(tokenBytesRent); } } } }