File: Passkeys\BufferSourceJsonConverter.cs
Web Access
Project: src\src\Identity\Core\src\Microsoft.AspNetCore.Identity.csproj (Microsoft.AspNetCore.Identity)
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
 
namespace Microsoft.AspNetCore.Identity;
 
internal sealed class BufferSourceJsonConverter : JsonConverter<BufferSource>
{
    private const int StackallocByteThreshold = 256;
 
    public override BufferSource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.ValueIsEscaped)
        {
            // We currently don't handle escaped base64url values, as we don't expect
            // to encounter them when reading payloads produced by WebAuthn clients.
            // See: https://www.w3.org/TR/webauthn-3/#base64url-encoding
            throw new JsonException("Unexpected escaped value in base64url string.");
        }
 
        var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
        if (!TryDecodeBase64Url(span, out var bytes))
        {
            throw new JsonException("Expected a valid base64url string.");
        }
 
        return BufferSource.FromBytes(bytes);
    }
 
    public override void Write(Utf8JsonWriter writer, BufferSource value, JsonSerializerOptions options)
    {
        var bytes = value.AsSpan();
        WriteBase64UrlStringValue(writer, bytes);
    }
 
    // Based on https://github.com/dotnet/runtime/blob/624737eb3796e1a760465912b27ac349965d8ba5/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs#L218
    private static bool TryDecodeBase64Url(ReadOnlySpan<byte> utf8Unescaped, [NotNullWhen(true)] out byte[]? bytes)
    {
        byte[]? pooledArray = null;
 
        Span<byte> byteSpan = utf8Unescaped.Length <= StackallocByteThreshold ?
            stackalloc byte[StackallocByteThreshold] :
            (pooledArray = ArrayPool<byte>.Shared.Rent(utf8Unescaped.Length));
 
        var status = Base64Url.DecodeFromUtf8(utf8Unescaped, byteSpan, out var bytesConsumed, out var bytesWritten);
        if (status != OperationStatus.Done)
        {
            bytes = null;
 
            if (pooledArray != null)
            {
                ArrayPool<byte>.Shared.Return(pooledArray);
            }
 
            return false;
        }
        Debug.Assert(bytesConsumed == utf8Unescaped.Length);
 
        bytes = byteSpan[..bytesWritten].ToArray();
 
        if (pooledArray != null)
        {
            ArrayPool<byte>.Shared.Return(pooledArray);
        }
 
        return true;
    }
 
    private static void WriteBase64UrlStringValue(Utf8JsonWriter writer, ReadOnlySpan<byte> bytes)
    {
        byte[]? pooledArray = null;
 
        var encodedLength = Base64Url.GetEncodedLength(bytes.Length);
        var byteSpan = encodedLength <= StackallocByteThreshold ?
            stackalloc byte[encodedLength] :
            (pooledArray = ArrayPool<byte>.Shared.Rent(encodedLength));
 
        var status = Base64Url.EncodeToUtf8(bytes, byteSpan, out var bytesConsumed, out var bytesWritten);
        Debug.Assert(status == OperationStatus.Done);
        Debug.Assert(bytesConsumed == bytes.Length);
 
        var base64UrlUtf8 = byteSpan[..bytesWritten];
        writer.WriteStringValue(base64UrlUtf8);
 
        if (pooledArray != null)
        {
            ArrayPool<byte>.Shared.Return(pooledArray);
        }
    }
}