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, 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. } } }