Skip to content

Commit

Permalink
fix(WebAuthnP256): support Firefox 1Password Add-on (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmm authored Nov 13, 2024
1 parent 60d55f7 commit 5da9efb
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/thick-jobs-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ox": patch
---

Shimmed `WebAuthnP256.createCredential` for 1Password Firefox Add-on.
1 change: 0 additions & 1 deletion examples/webauthn-p256/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "webauthn-p256",
"private": true,
"version": "0.0.6",
"type": "module",
"scripts": {
"dev": "vite"
Expand Down
8 changes: 5 additions & 3 deletions src/core/WebAuthnP256.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -99,6 +100,7 @@ export declare namespace createCredential {

type ErrorType =
| getCredentialCreationOptions.ErrorType
| internal.parseCredentialPublicKey.ErrorType
| Errors.GlobalErrorType
}

Expand Down
75 changes: 58 additions & 17 deletions src/core/internal/webauthn.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down Expand Up @@ -178,21 +179,61 @@ export function parseAsn1Signature(bytes: Uint8Array) {
* @internal
*/
export async function parseCredentialPublicKey(
cPublicKey: Bytes.Bytes,
response: AuthenticatorAttestationResponse,
): Promise<PublicKey.PublicKey> {
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
}

0 comments on commit 5da9efb

Please sign in to comment.