CloudBuilder/CloudBuilder.Security.Service/WebAuthnAssertionPatcher.cs
owenchen 873f65dbbe ow
2026-08-06 13:15:56 +08:00

93 lines
4.1 KiB
C#

using Fido2NetLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Security.Service
{
/// <summary>
/// Re-encodes the base64url string fields on a freshly-deserialized
/// <see cref="AuthenticatorAssertionRawResponse"/> as <c>byte[]</c>.
///
/// Fido2NetLib 1.0.0-alpha has these properties typed as <c>string</c>
/// (no [JsonConverter] — that was added in v1.1.0+), but its
/// <c>MakeAssertionAsync</c> internally does
/// <c>(byte[])raw.Id</c> and
/// <c>(byte[])raw.Response.ClientDataJson</c> to look up the credential
/// and verify the signature. The hard cast throws
/// "Unable to cast object of type 'System.String' to type 'System.Byte[]'".
///
/// This helper was originally a private static method on
/// <c>UserProfileProcess</c>; it was extracted so
/// <c>FinishEpayslipBiometricAssertion</c> (the new epayslip-unlock
/// flow) can share the same Fido2NetLib 1.0.0-alpha workaround without
/// duplicating the reflection-patching logic.
///
/// Behaviour:
/// 1. Checks the runtime property type.
/// 2. If it's already <c>byte[]</c> (v1.1.0+), base64url-decodes the
/// incoming string and stores the bytes.
/// 3. If it's <c>string</c> (v1.0.0-alpha), we can't store <c>byte[]</c>
/// in a <c>string</c> field — CLR enforces type safety. Fido2NetLib's
/// internal (byte[]) cast will then throw on the next call with a
/// clear InvalidCastException we can diagnose from the stack trace.
/// </summary>
public static class WebAuthnAssertionPatcher
{
public static void PatchResponseToByteArrays(AuthenticatorAssertionRawResponse raw)
{
if (raw == null) return;
PatchStringPropertyToByteArray(raw, "Id");
PatchStringPropertyToByteArray(raw, "RawId");
if (raw.Response != null)
{
PatchStringPropertyToByteArray(raw.Response, "Id");
PatchStringPropertyToByteArray(raw.Response, "AuthenticatorData");
PatchStringPropertyToByteArray(raw.Response, "Signature");
PatchStringPropertyToByteArray(raw.Response, "ClientDataJson");
PatchStringPropertyToByteArray(raw.Response, "UserHandle");
}
}
private static void PatchStringPropertyToByteArray(object target, string propertyName)
{
if (target == null) return;
var prop = target.GetType().GetProperty(propertyName);
if (prop == null) return;
if (!prop.CanWrite) return;
var current = prop.GetValue(target);
// If Fido2NetLib 1.1.0+ shape — property is already byte[].
if (prop.PropertyType == typeof(byte[]))
{
if (current is byte[]) return; // already decoded by the converter
if (current is string s && !string.IsNullOrEmpty(s))
{
try { prop.SetValue(target, Fido2NetLib.Base64Url.Decode(s)); }
catch { /* leave original value; Fido2NetLib will surface a clearer error */ }
}
return;
}
// If the property is `object` (1.0.0-alpha may store the raw
// deserialized value as object), we can safely put a byte[] in it.
if (prop.PropertyType == typeof(object))
{
if (current is string s && !string.IsNullOrEmpty(s))
{
try { prop.SetValue(target, Fido2NetLib.Base64Url.Decode(s)); } catch { }
}
return;
}
// If the property is `string` (1.0.0-alpha default), we cannot
// store byte[] in a string-typed field — CLR enforces type safety
// even via reflection. Fido2NetLib's internal (byte[]) cast will
// throw on the next call. The caller will see a clear
// InvalidCastException that we can diagnose from the stack trace.
}
}
}