1- import type { BitGoBase , IWallet } from '@bitgo/sdk-core' ;
1+ import type { BitGoBase , IWallet , KeychainWebauthnDevice , KeychainWithEncryptedPrv } from '@bitgo/sdk-core' ;
22import { buildEvalByCredential , matchDeviceByCredentialId } from './prfHelpers' ;
33import { derivePassword } from './derivePassword' ;
44import type { WebAuthnProvider } from './webAuthnTypes' ;
55
6- interface AuthChallengeResponse {
7- challenge : string ;
8- allowCredentials ?: Array < { id : string ; type : string ; transports ?: string [ ] } > ;
9- origin ?: string ;
6+ /** API payloads may use either spelling for the webauthn device list. */
7+ type UserKeychainResponse = KeychainWithEncryptedPrv & {
8+ webAuthnDevices ?: KeychainWebauthnDevice [ ] ;
9+ } ;
10+
11+ function webauthnDevicesFromKeychain ( keychain : UserKeychainResponse ) : KeychainWebauthnDevice [ ] | undefined {
12+ const lower = keychain . webauthnDevices ;
13+ if ( lower !== undefined && lower . length > 0 ) {
14+ return lower ;
15+ }
16+ const upper = keychain . webAuthnDevices ;
17+ if ( upper !== undefined && upper . length > 0 ) {
18+ return upper ;
19+ }
20+ return undefined ;
21+ }
22+
23+ function challengeFromAuthResponse ( body : unknown ) : string {
24+ if ( typeof body !== 'object' || body === null ) {
25+ throw new Error ( 'Invalid assertion challenge response' ) ;
26+ }
27+ const rec = body as Record < string , unknown > ;
28+ if ( typeof rec . challenge !== 'string' ) {
29+ throw new Error ( 'Invalid assertion challenge response' ) ;
30+ }
31+ return rec . challenge ;
1032}
1133
1234/**
@@ -23,45 +45,42 @@ export async function derivePasskeyPrfKey(params: {
2345} ) : Promise < string > {
2446 const { bitgo, wallet, provider } = params ;
2547
26- // Fetch the wallet's user keychain to get webauthnDevices
27- const keychain = await wallet . getEncryptedUserKeychain ( ) ;
28- const devices = ( keychain as any ) . webauthnDevices ?? ( keychain as any ) . webAuthnDevices ;
48+ const keychain : UserKeychainResponse = await wallet . getEncryptedUserKeychain ( ) ;
49+ const devices = webauthnDevicesFromKeychain ( keychain ) ;
2950
3051 if ( ! devices || devices . length === 0 ) {
3152 throw new Error ( 'No passkey devices available' ) ;
3253 }
3354
34- // Build PRF eval map from devices
35- const { evalByCredential } = buildEvalByCredential ( devices as Parameters < typeof buildEvalByCredential > [ 0 ] ) ;
55+ const { evalByCredential } = buildEvalByCredential ( devices ) ;
3656
3757 if ( Object . keys ( evalByCredential ) . length === 0 ) {
3858 throw new Error ( 'No passkey devices available with a valid PRF salt' ) ;
3959 }
4060
41- // Fetch a server-issued assertion challenge via the auth endpoint
42- const { challenge } = ( await bitgo . get ( bitgo . url ( '/user/otp/webauthn/auth' , 2 ) ) . result ( ) ) as AuthChallengeResponse ;
61+ const challenge = challengeFromAuthResponse ( await bitgo . get ( bitgo . url ( '/user/otp/webauthn/auth' , 2 ) ) . result ( ) ) ;
62+
63+ const allowCredentials = Object . keys ( evalByCredential ) . map ( ( credId ) => {
64+ const nodeBuf = Buffer . from ( credId . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) , 'base64' ) ;
65+ const id = nodeBuf . buffer . slice ( nodeBuf . byteOffset , nodeBuf . byteOffset + nodeBuf . byteLength ) ;
66+ return {
67+ type : 'public-key' as const ,
68+ id,
69+ } ;
70+ } ) ;
4371
44- // Build allowCredentials so the browser knows which credentials to use.
45- // Pass the Buffer (Uint8Array) directly — not .buffer — so the provider
46- // layer can correctly slice it via ArrayBuffer.isView.
47- const allowCredentials = Object . keys ( evalByCredential ) . map ( ( credId ) => ( {
48- type : 'public-key' as const ,
49- id : Buffer . from ( credId . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) , 'base64' ) as unknown as ArrayBuffer ,
50- } ) ) ;
72+ const publicKey : PublicKeyCredentialRequestOptions = {
73+ challenge : new Uint8Array ( Buffer . from ( challenge , 'base64' ) ) ,
74+ allowCredentials,
75+ } ;
5176
52- // Trigger WebAuthn assertion with PRF evaluation via the provider (navigator layer)
5377 const result = await provider . get ( {
54- publicKey : {
55- challenge : Buffer . from ( challenge , 'base64' ) ,
56- allowCredentials,
57- } as PublicKeyCredentialRequestOptions ,
78+ publicKey,
5879 evalByCredential,
5980 } ) ;
6081
61- // Verify the credential matches a known device
62- matchDeviceByCredentialId ( devices as Parameters < typeof matchDeviceByCredentialId > [ 0 ] , result . credentialId ) ;
82+ matchDeviceByCredentialId ( devices , result . credentialId ) ;
6383
64- // Derive and return hex-encoded wallet passphrase
6584 if ( ! result . prfResult ) {
6685 throw new Error ( 'PRF output was not returned by the authenticator' ) ;
6786 }
0 commit comments