using Fido2NetLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Security.Service
{
///
/// Re-encodes the base64url string fields on a freshly-deserialized
/// as byte[].
///
/// Fido2NetLib 1.0.0-alpha has these properties typed as string
/// (no [JsonConverter] — that was added in v1.1.0+), but its
/// MakeAssertionAsync internally does
/// (byte[])raw.Id and
/// (byte[])raw.Response.ClientDataJson 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
/// UserProfileProcess; it was extracted so
/// FinishEpayslipBiometricAssertion (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 byte[] (v1.1.0+), base64url-decodes the
/// incoming string and stores the bytes.
/// 3. If it's string (v1.0.0-alpha), we can't store byte[]
/// in a string 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.
///
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, MyBase64Url.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, MyBase64Url.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.
}
}
///
/// 跨平台 Base64Url,行为等价旧Fido2NetLib.Base64Url,Windows+Linux完全兼容
///
public static class MyBase64Url
{
public static byte[] Decode(string input)
{
if (string.IsNullOrEmpty(input))
return null;
// base64url → 标准base64
input = input.Replace('-', '+').Replace('_', '/');
switch (input.Length % 4)
{
case 2: input += "=="; break;
case 3: input += "="; break;
}
return Convert.FromBase64String(input);
}
public static string Encode(byte[] data)
{
if (data == null) return null;
return Convert.ToBase64String(data)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
}
}
}