|
1 | 1 | import { BitGoBase, Keychain } from '@bitgo/sdk-core'; |
| 2 | +import { base64UrlToBuffer } from './base64url'; |
2 | 3 | import { deriveEnterpriseSalt } from './deriveEnterpriseSalt'; |
3 | 4 | import { derivePassword } from './derivePassword'; |
4 | 5 | import { WebAuthnOtpDevice, WebAuthnProvider } from './webAuthnTypes'; |
@@ -37,47 +38,41 @@ export async function attachPasskeyToWallet(params: { |
37 | 38 | const keychain = await wallet.getEncryptedUserKeychain(); |
38 | 39 | const keychainId = keychain.id; |
39 | 40 |
|
40 | | - // Derive enterprise-scoped salt |
41 | | - const enterpriseSalt = deriveEnterpriseSalt(device.prfSalt, enterpriseId); |
| 41 | + // Derive enterprise-scoped salt (base64url; same encoding is used as the |
| 42 | + // PRF eval input and as the server-stored prfSalt so the bytes fed to the |
| 43 | + // authenticator match between attach and derive). |
| 44 | + const prfSalt = deriveEnterpriseSalt(device.prfSalt, enterpriseId); |
42 | 45 |
|
43 | 46 | // Decrypt private key with existing passphrase |
44 | 47 | const privateKey = bitgo.decrypt({ password: existingPassphrase, input: keychain.encryptedPrv }); |
45 | 48 |
|
46 | 49 | // Decode credentialId from base64url to ArrayBuffer for allowCredentials. |
47 | 50 | // The WebAuthn spec requires allowCredentials to be non-empty when using evalByCredential, |
48 | 51 | // and each entry must correspond to a key in the evalByCredential map. |
49 | | - const credentialIdBuffer = Buffer.from(device.credentialId.replace(/-/g, '+').replace(/_/g, '/'), 'base64').buffer; |
| 52 | + const credentialIdBuffer = base64UrlToBuffer(device.credentialId).buffer; |
50 | 53 |
|
51 | | - // PRF assertion — evalByCredential maps this device's credentialId to its enterprise salt |
| 54 | + // PRF assertion — evalByCredential maps this device's credentialId to the |
| 55 | + // base64url enterprise salt. The provider layer is responsible for decoding |
| 56 | + // base64url to raw bytes before handing it to the WebAuthn PRF extension. |
52 | 57 | const authResult = await provider.get({ |
53 | 58 | publicKey: { |
54 | 59 | allowCredentials: [{ type: 'public-key', id: credentialIdBuffer }], |
55 | 60 | } as PublicKeyCredentialRequestOptions, |
56 | | - evalByCredential: { [device.credentialId]: enterpriseSalt }, |
| 61 | + evalByCredential: { [device.credentialId]: prfSalt }, |
57 | 62 | }); |
58 | 63 |
|
59 | 64 | if (!authResult.prfResult) { |
60 | 65 | throw new Error('PRF assertion did not return a result.'); |
61 | 66 | } |
62 | 67 |
|
63 | | - // Derive password from PRF output and re-encrypt |
64 | 68 | const prfPassword = derivePassword(authResult.prfResult); |
65 | 69 | const encryptedPrv = bitgo.encrypt({ password: prfPassword, input: privateKey }); |
66 | 70 |
|
67 | | - // Convert enterpriseSalt from hex to base64url (URL-safe, no padding) |
68 | | - // as required by the server's prfSalt validation. |
69 | | - const prfSaltBase64url = Buffer.from(enterpriseSalt, 'hex') |
70 | | - .toString('base64') |
71 | | - .replace(/\+/g, '-') |
72 | | - .replace(/\//g, '_') |
73 | | - .replace(/=+$/, ''); |
74 | | - |
75 | | - // PUT webauthnInfo to keychain endpoint |
76 | 71 | const updatedKeychain = await bitgo |
77 | 72 | .put(bitgo.url(`/${coin}/key/${keychainId}`, 2)) |
78 | 73 | .send({ |
79 | 74 | webauthnInfo: { |
80 | | - prfSalt: prfSaltBase64url, |
| 75 | + prfSalt, |
81 | 76 | otpDeviceId: device.id, |
82 | 77 | encryptedPrv, |
83 | 78 | }, |
|
0 commit comments