123 lines
4.9 KiB
C#
123 lines
4.9 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, 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.
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 跨平台 Base64Url,行为等价旧Fido2NetLib.Base64Url,Windows+Linux完全兼容
|
||
/// </summary>
|
||
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('=');
|
||
}
|
||
}
|
||
}
|