Skip to content

Commit

Permalink
Use BCL Base64Url implementation (#575)
Browse files Browse the repository at this point in the history
* Use BCL Base64Url implementation

* [Testing] React to Base64Url changes (encoder is now strict, and no longer supports Base64)

* Apply formatting

* Update deps

* Eliminate allocation in Base64UrlConverter and improve error message when encountering invalid data

* Add Base64UrlConverter.EnableRelaxedDecoding feature

* Format Base64UrlConverter

* Fix EnableRelaxedDecoding implementation and add test coverage
  • Loading branch information
iamcarbon authored Nov 18, 2024
1 parent 8f50921 commit 3c6808e
Show file tree
Hide file tree
Showing 28 changed files with 271 additions and 339 deletions.
6 changes: 4 additions & 2 deletions Demo/TestController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Text;
using System.Buffers.Text;
using System.Text;
using System.Text.Json;

using Fido2NetLib;
using Fido2NetLib.Development;
using Fido2NetLib.Objects;
Expand Down Expand Up @@ -42,7 +44,7 @@ public OkObjectResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialPa

try
{
username = Base64Url.Decode(opts.Username);
username = Base64Url.DecodeFromChars(opts.Username);
}
catch (FormatException)
{
Expand Down
158 changes: 0 additions & 158 deletions Src/Fido2.Models/Base64Url.cs

This file was deleted.

81 changes: 76 additions & 5 deletions Src/Fido2.Models/Converters/Base64UrlConverter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System.Text.Json;
#nullable enable

using System.Buffers;
using System.Buffers.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Fido2NetLib;
Expand All @@ -8,20 +12,87 @@ namespace Fido2NetLib;
/// </summary>
public sealed class Base64UrlConverter : JsonConverter<byte[]>
{
public static bool EnableRelaxedDecoding { get; set; }

public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!reader.HasValueSequence)
byte[]? rentedBuffer = null;

scoped ReadOnlySpan<byte> source;

if (!reader.HasValueSequence && !reader.ValueIsEscaped)
{
return Base64Url.DecodeUtf8(reader.ValueSpan);
source = reader.ValueSpan;
}
else
{
return Base64Url.Decode(reader.GetString());
int valueLength = reader.HasValueSequence ? checked((int)reader.ValueSequence.Length) : reader.ValueSpan.Length;

Span<byte> buffer = valueLength <= 32 ? stackalloc byte[32] : (rentedBuffer = ArrayPool<byte>.Shared.Rent(valueLength));
int bytesRead = reader.CopyString(buffer);
source = buffer[..bytesRead];
}

try
{
return Base64Url.DecodeFromUtf8(source);
}
catch (Exception ex)
{
if (Base64.IsValid(source))
{
static byte[] DecodeBase64FromUtf8(scoped ReadOnlySpan<byte> source)
{
var rentedBuffer = ArrayPool<byte>.Shared.Rent(Base64.GetMaxDecodedFromUtf8Length(source.Length));

try
{
_ = Base64.DecodeFromUtf8(source, rentedBuffer, out _, out int written);

return rentedBuffer[0..written];
}
finally
{
ArrayPool<byte>.Shared.Return(rentedBuffer);
}
}

if (EnableRelaxedDecoding)
{
return DecodeBase64FromUtf8(source);
}
else
{
throw new JsonException("Expected data to be in Base64Url format, but received Base64 encoding instead.");
}
}
else
{
throw new JsonException(ex.Message, ex);
}
}
finally
{
if (rentedBuffer != null)
{
ArrayPool<byte>.Shared.Return(rentedBuffer);
}
}
}

public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(Base64Url.Encode(value));
var rentedBuffer = ArrayPool<byte>.Shared.Rent(Base64Url.GetEncodedLength(value.Length));

try
{
Base64Url.EncodeToUtf8(value, rentedBuffer, out _, out int written);

writer.WriteStringValue(rentedBuffer.AsSpan(0..written));
}
finally
{
ArrayPool<byte>.Shared.Return(rentedBuffer);
}
}
}
6 changes: 5 additions & 1 deletion Src/Fido2.Models/Fido2.Models.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(SupportedTargetFrameworks)</TargetFrameworks>
Expand All @@ -9,4 +9,8 @@
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.Memory" Version="9.0.0" />
</ItemGroup>

</Project>
3 changes: 1 addition & 2 deletions Src/Fido2.Models/Metadata/BiometricStatusReport.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;

namespace Fido2NetLib;

Expand Down
3 changes: 2 additions & 1 deletion Src/Fido2/AttestationFormat/AndroidSafetyNet.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
Expand Down Expand Up @@ -46,7 +47,7 @@ public override async ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttes

try
{
jwtHeaderBytes = Base64Url.Decode(jwtComponents[0]);
jwtHeaderBytes = Base64Url.DecodeFromChars(jwtComponents[0]);
}
catch (FormatException)
{
Expand Down
1 change: 0 additions & 1 deletion Src/Fido2/AuthenticatorAssertionResponse.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
Expand Down
4 changes: 2 additions & 2 deletions Src/Fido2/Fido2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

<ItemGroup>
<ProjectReference Include="..\Fido2.Models\Fido2.Models.csproj" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="NSec.Cryptography" Version="22.4.0" />
<PackageReference Include="System.Formats.Cbor" Version="8.0.0" />
<PackageReference Include="System.Formats.Cbor" Version="9.0.0" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.0" />
</ItemGroup>

Expand Down
4 changes: 1 addition & 3 deletions Src/Fido2/MakeAssertionParams.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel;

namespace Fido2NetLib;

Expand Down
5 changes: 3 additions & 2 deletions Src/Fido2/Metadata/ConformanceMetadataRepository.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -132,7 +133,7 @@ public async Task<MetadataBLOBPayload> DeserializeAndValidateBlobAsync(string ra
throw new Fido2MetadataException("The JWT does not have the 3 expected components");

var blobHeader = jwtParts[0];
using var jsonDoc = JsonDocument.Parse(Base64Url.Decode(blobHeader));
using var jsonDoc = JsonDocument.Parse(Base64Url.DecodeFromChars(blobHeader));
var tokenHeader = jsonDoc.RootElement;

var blobAlg = tokenHeader.TryGetProperty("alg", out var algEl)
Expand Down Expand Up @@ -235,7 +236,7 @@ public async Task<MetadataBLOBPayload> DeserializeAndValidateBlobAsync(string ra

var blobPayload = ((JsonWebToken)validateTokenResult.SecurityToken).EncodedPayload;

MetadataBLOBPayload blob = JsonSerializer.Deserialize(Base64Url.Decode(blobPayload), FidoModelSerializerContext.Default.MetadataBLOBPayload)!;
MetadataBLOBPayload blob = JsonSerializer.Deserialize(Base64Url.DecodeFromChars(blobPayload), FidoModelSerializerContext.Default.MetadataBLOBPayload)!;
blob.JwtAlg = blobAlg;
return blob;
}
Expand Down
5 changes: 3 additions & 2 deletions Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers.Text;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
Expand Down Expand Up @@ -67,7 +68,7 @@ private async Task<MetadataBLOBPayload> DeserializeAndValidateBlobAsync(string r
throw new ArgumentException("The JWT does not have the 3 expected components");

var blobHeaderString = jwtParts[0];
using var blobHeaderDoc = JsonDocument.Parse(Base64Url.Decode(blobHeaderString));
using var blobHeaderDoc = JsonDocument.Parse(Base64Url.DecodeFromChars(blobHeaderString));
var blobHeader = blobHeaderDoc.RootElement;

string blobAlg = blobHeader.TryGetProperty("alg", out var algEl)
Expand Down Expand Up @@ -186,7 +187,7 @@ private async Task<MetadataBLOBPayload> DeserializeAndValidateBlobAsync(string r

var blobPayload = ((JsonWebToken)validateTokenResult.SecurityToken).EncodedPayload;

MetadataBLOBPayload blob = JsonSerializer.Deserialize(Base64Url.Decode(blobPayload), FidoModelSerializerContext.Default.MetadataBLOBPayload)!;
MetadataBLOBPayload blob = JsonSerializer.Deserialize(Base64Url.DecodeFromChars(blobPayload), FidoModelSerializerContext.Default.MetadataBLOBPayload)!;
blob.JwtAlg = blobAlg;
return blob;
}
Expand Down
2 changes: 2 additions & 0 deletions Src/Fido2/Objects/CredentialPublicKey.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

using Fido2NetLib.Cbor;

using NSec.Cryptography;

namespace Fido2NetLib.Objects;
Expand Down
Loading

0 comments on commit 3c6808e

Please sign in to comment.