From 5da9efbfebfa738ee0f78927e90b3fab61cbb2e8 Mon Sep 17 00:00:00 2001 From: tmm Date: Tue, 12 Nov 2024 19:35:15 -0500 Subject: [PATCH] fix(WebAuthnP256): support Firefox 1Password Add-on (#25) --- .changeset/thick-jobs-greet.md | 5 ++ examples/webauthn-p256/package.json | 1 - src/core/WebAuthnP256.ts | 8 +-- src/core/internal/webauthn.ts | 75 ++++++++++++++++++++++------- 4 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 .changeset/thick-jobs-greet.md diff --git a/.changeset/thick-jobs-greet.md b/.changeset/thick-jobs-greet.md new file mode 100644 index 00000000..2b02823b --- /dev/null +++ b/.changeset/thick-jobs-greet.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Shimmed `WebAuthnP256.createCredential` for 1Password Firefox Add-on. diff --git a/examples/webauthn-p256/package.json b/examples/webauthn-p256/package.json index 23654871..56fe92a3 100644 --- a/examples/webauthn-p256/package.json +++ b/examples/webauthn-p256/package.json @@ -1,7 +1,6 @@ { "name": "webauthn-p256", "private": true, - "version": "0.0.6", "type": "module", "scripts": { "dev": "vite" diff --git a/src/core/WebAuthnP256.ts b/src/core/WebAuthnP256.ts index 10ac9bae..a5e2ea4b 100644 --- a/src/core/WebAuthnP256.ts +++ b/src/core/WebAuthnP256.ts @@ -67,9 +67,10 @@ export async function createCredential( creationOptions, )) as internal.PublicKeyCredential if (!credential) throw new CredentialCreationFailedError() - const publicKey = await internal.parseCredentialPublicKey( - new Uint8Array((credential.response as any).getPublicKey()), - ) + + const response = credential.response as AuthenticatorAttestationResponse + const publicKey = await internal.parseCredentialPublicKey(response) + return { id: credential.id, publicKey, @@ -99,6 +100,7 @@ export declare namespace createCredential { type ErrorType = | getCredentialCreationOptions.ErrorType + | internal.parseCredentialPublicKey.ErrorType | Errors.GlobalErrorType } diff --git a/src/core/internal/webauthn.ts b/src/core/internal/webauthn.ts index e873ba3a..96c12d97 100644 --- a/src/core/internal/webauthn.ts +++ b/src/core/internal/webauthn.ts @@ -1,7 +1,8 @@ import { p256 } from '@noble/curves/p256' -import type * as Bytes from '../Bytes.js' +import type * as Errors from '../Errors.js' import * as Hex from '../Hex.js' import * as PublicKey from '../PublicKey.js' +import { CredentialCreationFailedError } from '../WebAuthnP256.js' /** @internal */ export type AttestationConveyancePreference = @@ -178,21 +179,61 @@ export function parseAsn1Signature(bytes: Uint8Array) { * @internal */ export async function parseCredentialPublicKey( - cPublicKey: Bytes.Bytes, + response: AuthenticatorAttestationResponse, ): Promise { - const cryptoKey = await crypto.subtle.importKey( - 'spki', - new Uint8Array(cPublicKey), - { - name: 'ECDSA', - namedCurve: 'P-256', - hash: 'SHA-256', - }, - true, - ['verify'], - ) - const publicKey = new Uint8Array( - await crypto.subtle.exportKey('raw', cryptoKey), - ) - return PublicKey.from(publicKey) + const publicKeyBuffer = response.getPublicKey() + if (!publicKeyBuffer) throw new CredentialCreationFailedError() + + try { + // Converting `publicKeyBuffer` throws when credential is created by 1Password Firefox Add-on + const publicKeyBytes = new Uint8Array(publicKeyBuffer) + const cryptoKey = await crypto.subtle.importKey( + 'spki', + new Uint8Array(publicKeyBytes), + { + name: 'ECDSA', + namedCurve: 'P-256', + hash: 'SHA-256', + }, + true, + ['verify'], + ) + const publicKey = new Uint8Array( + await crypto.subtle.exportKey('raw', cryptoKey), + ) + return PublicKey.from(publicKey) + } catch (error) { + // Fallback for 1Password Firefox Add-on restricts access to certain credential properties + // so we need to use `attestationObject` to extract the public key. + // https://github.com/passwordless-id/webauthn/issues/50#issuecomment-2072902094 + if ((error as Error).message !== 'Permission denied to access object') + throw error + + const data = new Uint8Array(response.attestationObject) + const coordinateLength = 0x20 + const cborPrefix = 0x58 + + const findStart = (key: number) => { + const coordinate = new Uint8Array([key, cborPrefix, coordinateLength]) + for (let i = 0; i < data.length - coordinate.length; i++) + if (coordinate.every((byte, j) => data[i + j] === byte)) + return i + coordinate.length + throw new CredentialCreationFailedError() + } + + const xStart = findStart(0x21) + const yStart = findStart(0x22) + + return PublicKey.from( + new Uint8Array([ + 0x04, + ...data.slice(xStart, xStart + coordinateLength), + ...data.slice(yStart, yStart + coordinateLength), + ]), + ) + } +} + +export declare namespace parseCredentialPublicKey { + type ErrorType = CredentialCreationFailedError | Errors.GlobalErrorType }