93 lines
4.1 KiB
C#
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.
|
|
}
|
|
}
|
|
}
|