/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Shared/WebEncoders/WebEncoders.cs
514 строк
19 KB
Korolev Dmitry
Antiforgery perf improvements (includes new DataProtection API usage) (#64751)
29 янв 2026, 23:38
Не верифицирован
29 янв 2026, 23:38
f87c18f
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enable using System; #if NETCOREAPP using System.Buffers; using System.Buffers.Text; #endif using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Shared; using Microsoft.Extensions.WebEncoders.Sources; #if WebEncoders_In_WebUtilities namespace Microsoft.AspNetCore.WebUtilities; #else namespace Microsoft.Extensions.Internal; #endif /// <summary> /// Contains utility APIs to assist with common encoding and decoding operations. /// </summary> #if WebEncoders_In_WebUtilities public #else internal #endif static class WebEncoders { #if NET9_0_OR_GREATER /// <summary>SearchValues for the two Base64 and two Base64Url chars that differ from each other.</summary> private static readonly SearchValues<char> s_base64vsBase64UrlDifferentiators = SearchValues.Create("+/-_"); #endif /// <summary> /// Decodes a base64url-encoded string. /// </summary> /// <param name="input">The base64url-encoded input to decode.</param> /// <returns>The base64url-decoded form of the input.</returns> /// <remarks> /// The input must not contain any whitespace or padding characters. /// Throws <see cref="FormatException"/> if the input is malformed. /// </remarks> public static byte[] Base64UrlDecode(string input) { ArgumentNullThrowHelper.ThrowIfNull(input); return Base64UrlDecode(input, offset: 0, count: input.Length); } /// <summary> /// Decodes a base64url-encoded substring of a given string. /// </summary> /// <param name="input">A string containing the base64url-encoded input to decode.</param> /// <param name="offset">The position in <paramref name="input"/> at which decoding should begin.</param> /// <param name="count">The number of characters in <paramref name="input"/> to decode.</param> /// <returns>The base64url-decoded form of the input.</returns> /// <remarks> /// The input must not contain any whitespace or padding characters. /// Throws <see cref="FormatException"/> if the input is malformed. /// </remarks> public static byte[] Base64UrlDecode(string input, int offset, int count) { ArgumentNullThrowHelper.ThrowIfNull(input); ValidateParameters(input.Length, nameof(input), offset, count); // Special-case empty input if (count == 0) { return Array.Empty<byte>(); } #if NET9_0_OR_GREATER // Legacy behavior of Base64UrlDecode supports either Base64 or Base64Url input. // If it has a - or _, or if it doesn't have + or /, it can be treated as Base64Url. // Searching for any of them allows us to stop the search as early as we know whether Base64Url should be used. ReadOnlySpan<char> inputSpan = input.AsSpan(offset, count); int indexOfFirstDifferentiator = inputSpan.IndexOfAny(s_base64vsBase64UrlDifferentiators); if (indexOfFirstDifferentiator < 0 || inputSpan[indexOfFirstDifferentiator] is '-' or '_') { return Base64Url.DecodeFromChars(inputSpan); } // Otherwise, maintain the legacy behavior of accepting Base64 input. Input that // contained both +/ and -_ is neither Base64 nor Base64Url and is considered invalid. if (offset == 0 && count == input.Length) { return Convert.FromBase64String(input); } #endif // Create array large enough for the Base64 characters, not just shorter Base64-URL-encoded form. var buffer = new char[GetArraySizeRequiredToDecode(count)]; return Base64UrlDecode(input, offset, buffer, bufferOffset: 0, count: count); } /// <summary> /// Decodes a base64url-encoded <paramref name="input"/> into a <c>byte[]</c>. /// </summary> /// <param name="input">A string containing the base64url-encoded input to decode.</param> /// <param name="offset">The position in <paramref name="input"/> at which decoding should begin.</param> /// <param name="buffer"> /// Scratch buffer to hold the <see cref="char"/>s to decode. Array must be large enough to hold /// <paramref name="bufferOffset"/> and <paramref name="count"/> characters as well as Base64 padding /// characters. Content is not preserved. /// </param> /// <param name="bufferOffset"> /// The offset into <paramref name="buffer"/> at which to begin writing the <see cref="char"/>s to decode. /// </param> /// <param name="count">The number of characters in <paramref name="input"/> to decode.</param> /// <returns>The base64url-decoded form of the <paramref name="input"/>.</returns> /// <remarks> /// The input must not contain any whitespace or padding characters. /// Throws <see cref="FormatException"/> if the input is malformed. /// </remarks> public static byte[] Base64UrlDecode(string input, int offset, char[] buffer, int bufferOffset, int count) { ArgumentNullThrowHelper.ThrowIfNull(input); ArgumentNullThrowHelper.ThrowIfNull(buffer); ValidateParameters(input.Length, nameof(input), offset, count); ArgumentOutOfRangeThrowHelper.ThrowIfNegative(bufferOffset); if (count == 0) { return Array.Empty<byte>(); } #if NET9_0_OR_GREATER // Legacy behavior of Base64UrlDecode supports either Base64 or Base64Url input. // If it has a - or _, or if it doesn't have + or /, it can be treated as Base64Url. // Searching for any of them allows us to stop the search as early as we know Base64Url should be used. ReadOnlySpan<char> inputSpan = input.AsSpan(offset, count); int indexOfFirstDifferentiator = inputSpan.IndexOfAny(s_base64vsBase64UrlDifferentiators); if (indexOfFirstDifferentiator < 0 || inputSpan[indexOfFirstDifferentiator] is '-' or '_') { return Base64Url.DecodeFromChars(inputSpan); } // Otherwise, maintain the legacy behavior of accepting Base64 input. Input that // contained both +/ and -_ is neither Base64 nor Base64Url and is considered invalid. if (offset == 0 && count == input.Length) { return Convert.FromBase64String(input); } #endif // Assumption: input is base64url encoded without padding and contains no whitespace. var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); var arraySizeRequired = checked(count + paddingCharsToAdd); Debug.Assert(arraySizeRequired % 4 == 0, "Invariant: Array length must be a multiple of 4."); if (buffer.Length - bufferOffset < arraySizeRequired) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, EncoderResources.WebEncoders_InvalidCountOffsetOrLength, nameof(count), nameof(bufferOffset), nameof(input)), nameof(count)); } // Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'. var i = bufferOffset; #if NET8_0_OR_GREATER Span<char> bufferSpan = buffer.AsSpan(i, count); inputSpan.CopyTo(bufferSpan); bufferSpan.Replace('-', '+'); bufferSpan.Replace('_', '/'); i += count; #else for (var j = offset; i - bufferOffset < count; i++, j++) { var ch = input[j]; if (ch == '-') { buffer[i] = '+'; } else if (ch == '_') { buffer[i] = '/'; } else { buffer[i] = ch; } } #endif // Add the padding characters back. for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--) { buffer[i] = '='; } // Decode. // If the caller provided invalid base64 chars, they'll be caught here. return Convert.FromBase64CharArray(buffer, bufferOffset, arraySizeRequired); } #if NET9_0_OR_GREATER /// <summary> /// Decodes a base64url-encoded <paramref name="input"/> into <paramref name="output"/>. /// </summary> /// <param name="input">The base64url-encoded input to decode.</param> /// <param name="output">The buffer to receive the decoded bytes.</param> /// <returns>The number of bytes written to <paramref name="output"/>.</returns> /// <remarks> /// The input must not contain any whitespace or padding characters. /// Throws <see cref="FormatException"/> if the input is malformed. /// </remarks> internal static int Base64UrlDecode(string input, Span<byte> output) { if (string.IsNullOrEmpty(input)) { return 0; } var status = Base64Url.DecodeFromChars(input, output, out _, out int bytesWritten); if (status != OperationStatus.Done) { throw new FormatException( string.Format( CultureInfo.CurrentCulture, EncoderResources.WebEncoders_MalformedInput, input.Length)); } return bytesWritten; } #endif /// <summary> /// Gets the minimum <c>char[]</c> size required for decoding of <paramref name="count"/> characters /// with the <see cref="Base64UrlDecode(string, int, char[], int, int)"/> method. /// </summary> /// <param name="count">The number of characters to decode.</param> /// <returns> /// The minimum <c>char[]</c> size required for decoding of <paramref name="count"/> characters. /// </returns> public static int GetArraySizeRequiredToDecode(int count) { ArgumentOutOfRangeThrowHelper.ThrowIfNegative(count); if (count == 0) { return 0; } var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); return checked(count + numPaddingCharsToAdd); } /// <summary> /// Encodes <paramref name="input"/> using base64url encoding. /// </summary> /// <param name="input">The binary input to encode.</param> /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns> public static string Base64UrlEncode(byte[] input) { ArgumentNullThrowHelper.ThrowIfNull(input); return Base64UrlEncode(input, offset: 0, count: input.Length); } /// <summary> /// Encodes <paramref name="input"/> using base64url encoding. /// </summary> /// <param name="input">The binary input to encode.</param> /// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param> /// <param name="count">The number of bytes from <paramref name="input"/> to encode.</param> /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns> public static string Base64UrlEncode(byte[] input, int offset, int count) { ArgumentNullThrowHelper.ThrowIfNull(input); ValidateParameters(input.Length, nameof(input), offset, count); #if NETCOREAPP return Base64UrlEncode(input.AsSpan(offset, count)); #else // Special-case empty input if (count == 0) { return string.Empty; } var buffer = new char[GetArraySizeRequiredToEncode(count)]; var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count); return new string(buffer, startIndex: 0, length: numBase64Chars); #endif } /// <summary> /// Encodes <paramref name="input"/> using base64url encoding. /// </summary> /// <param name="input">The binary input to encode.</param> /// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param> /// <param name="output"> /// Buffer to receive the base64url-encoded form of <paramref name="input"/>. Array must be large enough to /// hold <paramref name="outputOffset"/> characters and the full base64-encoded form of /// <paramref name="input"/>, including padding characters. /// </param> /// <param name="outputOffset"> /// The offset into <paramref name="output"/> at which to begin writing the base64url-encoded form of /// <paramref name="input"/>. /// </param> /// <param name="count">The number of <c>byte</c>s from <paramref name="input"/> to encode.</param> /// <returns> /// The number of characters written to <paramref name="output"/>, less any padding characters. /// </returns> public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count) { ArgumentNullThrowHelper.ThrowIfNull(input); ArgumentNullThrowHelper.ThrowIfNull(output); ValidateParameters(input.Length, nameof(input), offset, count); ArgumentOutOfRangeThrowHelper.ThrowIfNegative(outputOffset); var arraySizeRequired = GetArraySizeRequiredToEncode(count); if (output.Length - outputOffset < arraySizeRequired) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, EncoderResources.WebEncoders_InvalidCountOffsetOrLength, nameof(count), nameof(outputOffset), nameof(output)), nameof(count)); } #if NETCOREAPP return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset)); #else // Special-case empty input. if (count == 0) { return 0; } // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. // Start with default Base64 encoding. var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset); // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. for (var i = outputOffset; i - outputOffset < numBase64Chars; i++) { var ch = output[i]; if (ch == '+') { output[i] = '-'; } else if (ch == '/') { output[i] = '_'; } else if (ch == '=') { // We've reached a padding character; truncate the remainder. return i - outputOffset; } } return numBase64Chars; #endif } /// <summary> /// Get the minimum output <c>char[]</c> size required for encoding <paramref name="count"/> /// <see cref="byte"/>s with the <see cref="Base64UrlEncode(byte[], int, char[], int, int)"/> method. /// </summary> /// <param name="count">The number of characters to encode.</param> /// <returns> /// The minimum output <c>char[]</c> size required for encoding <paramref name="count"/> <see cref="byte"/>s. /// </returns> public static int GetArraySizeRequiredToEncode(int count) { var numWholeOrPartialInputBlocks = checked(count + 2) / 3; return checked(numWholeOrPartialInputBlocks * 4); } #if NETCOREAPP /// <summary> /// Encodes <paramref name="input"/> using base64url encoding. /// </summary> /// <param name="input">The binary input to encode.</param> /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns> [SkipLocalsInit] public static string Base64UrlEncode(ReadOnlySpan<byte> input) { #if NET9_0_OR_GREATER return Base64Url.EncodeToString(input); #else const int StackAllocThreshold = 128; if (input.IsEmpty) { return string.Empty; } int bufferSize = GetArraySizeRequiredToEncode(input.Length); char[]? bufferToReturnToPool = null; Span<char> buffer = bufferSize <= StackAllocThreshold ? stackalloc char[StackAllocThreshold] : bufferToReturnToPool = ArrayPool<char>.Shared.Rent(bufferSize); var numBase64Chars = Base64UrlEncode(input, buffer); var base64Url = new string(buffer.Slice(0, numBase64Chars)); if (bufferToReturnToPool != null) { ArrayPool<char>.Shared.Return(bufferToReturnToPool); } return base64Url; #endif } #if NET9_0_OR_GREATER /// <summary> /// Encodes <paramref name="input"/> using base64url encoding. /// </summary> /// <param name="input">The binary input to encode.</param> /// <param name="output">The buffer to place the result in.</param> /// <returns></returns> public static int Base64UrlEncode(ReadOnlySpan<byte> input, Span<char> output) { return Base64Url.EncodeToChars(input, output); } #else private static int Base64UrlEncode(ReadOnlySpan<byte> input, Span<char> output) { Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length)); if (input.IsEmpty) { return 0; } // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. Convert.TryToBase64Chars(input, output, out int charsWritten); // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. for (var i = 0; i < charsWritten; i++) { var ch = output[i]; if (ch == '+') { output[i] = '-'; } else if (ch == '/') { output[i] = '_'; } else if (ch == '=') { // We've reached a padding character; truncate the remainder. return i; } } return charsWritten; } #endif #endif private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength) { switch (inputLength % 4) { case 0: return 0; case 2: return 2; case 3: return 1; default: throw new FormatException( string.Format( CultureInfo.CurrentCulture, EncoderResources.WebEncoders_MalformedInput, inputLength)); } } private static void ValidateParameters(int bufferLength, string inputName, int offset, int count) { ArgumentOutOfRangeThrowHelper.ThrowIfNegative(offset); ArgumentOutOfRangeThrowHelper.ThrowIfNegative(count); if (bufferLength - offset < count) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, EncoderResources.WebEncoders_InvalidCountOffsetOrLength, nameof(count), nameof(offset), inputName), nameof(count)); } } }