diff --git a/src/utils/account-passkeys.ts b/src/utils/account-passkeys.ts index e5211a0..dd92832 100644 --- a/src/utils/account-passkeys.ts +++ b/src/utils/account-passkeys.ts @@ -32,6 +32,44 @@ function textBytes(value: string): Uint8Array { return new TextEncoder().encode(value); } +function hexByte(value: number): string { + return value.toString(16).padStart(2, '0'); +} + +function dotNetGuidBytesToUuid(bytes: Uint8Array): string | null { + if (bytes.length !== 16) return null; + return [ + [bytes[3], bytes[2], bytes[1], bytes[0]].map(hexByte).join(''), + [bytes[5], bytes[4]].map(hexByte).join(''), + [bytes[7], bytes[6]].map(hexByte).join(''), + [bytes[8], bytes[9]].map(hexByte).join(''), + Array.from(bytes.slice(10, 16)).map(hexByte).join(''), + ].join('-'); +} + +function uuidToDotNetGuidBytes(value: string): Uint8Array | null { + const match = String(value || '').trim().match( + /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i + ); + if (!match) return null; + const hex = match.slice(1).join(''); + const bytes = new Uint8Array(16); + for (let i = 0; i < 16; i += 1) { + bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return new Uint8Array([ + bytes[3], bytes[2], bytes[1], bytes[0], + bytes[5], bytes[4], + bytes[7], bytes[6], + bytes[8], bytes[9], + bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], + ]); +} + +function normalizeWebAuthnBase64(value: unknown): string { + return String(value || '').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); +} + async function importHmacKey(secret: string): Promise { return crypto.subtle.importKey('raw', textBytes(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']); } @@ -141,13 +179,16 @@ export function getAccountPasskeyRpConfig(request: Request, env: Env): { rpId: s } export function userIdToWebAuthnUserId(userId: string): Uint8Array { - return textBytes(userId); + return uuidToDotNetGuidBytes(userId) || textBytes(userId); } export function userHandleToUserId(userHandle: string | undefined): string | null { if (!userHandle) return null; try { - const decoded = new TextDecoder().decode(base64UrlToBytes(userHandle)); + const bytes = base64UrlToBytes(userHandle); + const officialGuid = dotNetGuidBytesToUuid(bytes); + if (officialGuid) return officialGuid; + const decoded = new TextDecoder().decode(bytes); return decoded.trim() || null; } catch { return null; @@ -209,17 +250,17 @@ export function normalizeRegistrationResponse(raw: unknown): RegistrationRespons const clientDataJSON = response.clientDataJSON || response.clientDataJson; if (!input.id || !input.rawId || !clientDataJSON || !response.attestationObject) return null; return { - id: String(input.id), - rawId: String(input.rawId), + id: normalizeWebAuthnBase64(input.id), + rawId: normalizeWebAuthnBase64(input.rawId), type: 'public-key', authenticatorAttachment: input.authenticatorAttachment, clientExtensionResults: input.clientExtensionResults || input.extensions || {}, response: { - attestationObject: String(response.attestationObject), - clientDataJSON: String(clientDataJSON), - authenticatorData: response.authenticatorData ? String(response.authenticatorData) : undefined, + attestationObject: normalizeWebAuthnBase64(response.attestationObject), + clientDataJSON: normalizeWebAuthnBase64(clientDataJSON), + authenticatorData: response.authenticatorData ? normalizeWebAuthnBase64(response.authenticatorData) : undefined, transports: Array.isArray(response.transports) ? response.transports.map(String) as AuthenticatorTransportFuture[] : undefined, - publicKey: response.publicKey ? String(response.publicKey) : undefined, + publicKey: response.publicKey ? normalizeWebAuthnBase64(response.publicKey) : undefined, publicKeyAlgorithm: typeof response.publicKeyAlgorithm === 'number' ? response.publicKeyAlgorithm : undefined, }, }; @@ -232,16 +273,16 @@ export function normalizeAuthenticationResponse(raw: unknown): AuthenticationRes const clientDataJSON = response.clientDataJSON || response.clientDataJson; if (!input.id || !input.rawId || !clientDataJSON || !response.authenticatorData || !response.signature) return null; return { - id: String(input.id), - rawId: String(input.rawId), + id: normalizeWebAuthnBase64(input.id), + rawId: normalizeWebAuthnBase64(input.rawId), type: 'public-key', authenticatorAttachment: input.authenticatorAttachment, clientExtensionResults: input.clientExtensionResults || input.extensions || {}, response: { - authenticatorData: String(response.authenticatorData), - clientDataJSON: String(clientDataJSON), - signature: String(response.signature), - userHandle: response.userHandle ? String(response.userHandle) : undefined, + authenticatorData: normalizeWebAuthnBase64(response.authenticatorData), + clientDataJSON: normalizeWebAuthnBase64(clientDataJSON), + signature: normalizeWebAuthnBase64(response.signature), + userHandle: response.userHandle ? normalizeWebAuthnBase64(response.userHandle) : undefined, }, }; }