Skip to content

Commit 46dfd3a

Browse files
committed
feat(sdk-core): add attachPasskeyToWallet function
- fetch wallet to infer coin, keychainId, enterpriseId - verify encryptedPrv exists before PRF assertion - derive enterprise salt and re-encrypt prv with PRF-derived password - PUT webauthnInfo to keychain endpoint Ticket: WCN-411
1 parent 94da3fc commit 46dfd3a

6 files changed

Lines changed: 371 additions & 3 deletions

File tree

Dockerfile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ COPY --from=builder /tmp/bitgo/modules/sdk-coin-icp /var/modules/sdk-coin-icp/
9696
COPY --from=builder /tmp/bitgo/modules/sdk-coin-initia /var/modules/sdk-coin-initia/
9797
COPY --from=builder /tmp/bitgo/modules/sdk-coin-injective /var/modules/sdk-coin-injective/
9898
COPY --from=builder /tmp/bitgo/modules/sdk-coin-islm /var/modules/sdk-coin-islm/
99-
COPY --from=builder /tmp/bitgo/modules/sdk-coin-kaspa /var/modules/sdk-coin-kaspa/
10099
COPY --from=builder /tmp/bitgo/modules/sdk-coin-mon /var/modules/sdk-coin-mon/
101100
COPY --from=builder /tmp/bitgo/modules/sdk-coin-near /var/modules/sdk-coin-near/
102101
COPY --from=builder /tmp/bitgo/modules/sdk-coin-oas /var/modules/sdk-coin-oas/
@@ -198,7 +197,6 @@ cd /var/modules/sdk-coin-icp && yarn link && \
198197
cd /var/modules/sdk-coin-initia && yarn link && \
199198
cd /var/modules/sdk-coin-injective && yarn link && \
200199
cd /var/modules/sdk-coin-islm && yarn link && \
201-
cd /var/modules/sdk-coin-kaspa && yarn link && \
202200
cd /var/modules/sdk-coin-mon && yarn link && \
203201
cd /var/modules/sdk-coin-near && yarn link && \
204202
cd /var/modules/sdk-coin-oas && yarn link && \
@@ -303,7 +301,6 @@ RUN cd /var/bitgo-express && \
303301
yarn link @bitgo/sdk-coin-initia && \
304302
yarn link @bitgo/sdk-coin-injective && \
305303
yarn link @bitgo/sdk-coin-islm && \
306-
yarn link @bitgo/sdk-coin-kaspa && \
307304
yarn link @bitgo/sdk-coin-mon && \
308305
yarn link @bitgo/sdk-coin-near && \
309306
yarn link @bitgo/sdk-coin-oas && \

modules/sdk-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
]
4141
},
4242
"dependencies": {
43+
"@bitgo/passkey-crypto": "^0.1.0",
4344
"@bitgo/public-types": "5.97.0",
4445
"@bitgo/sdk-lib-mpc": "^10.11.1",
4546
"@bitgo/secp256k1": "^1.11.0",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { BitGoBase } from '../bitgoBase';
2+
import { Keychain } from '../keychain';
3+
import { deriveEnterpriseSalt, derivePassword } from '@bitgo/passkey-crypto';
4+
import { WebAuthnOtpDevice, WebAuthnProvider } from './types';
5+
6+
export async function attachPasskeyToWallet(params: {
7+
bitgo: BitGoBase;
8+
walletId: string;
9+
device: WebAuthnOtpDevice;
10+
existingPassphrase: string;
11+
provider: WebAuthnProvider;
12+
}): Promise<Keychain> {
13+
const { bitgo, walletId, device, existingPassphrase, provider } = params;
14+
15+
// Throw early if PRF extension is not supported
16+
if (!device.prfSalt) {
17+
throw new Error('PRF extension not supported by this device. Please use a different passkey.');
18+
}
19+
20+
// Fetch wallet to infer coin, keychainId, enterpriseId
21+
const walletData = await bitgo.get(bitgo.url(`/wallet/${walletId}`, 2)).result();
22+
23+
const coin = walletData.coin;
24+
if (!coin || typeof coin !== 'string') {
25+
throw new Error(`Wallet ${walletId} has no coin type.`);
26+
}
27+
28+
const keys = walletData.keys as string[] | undefined;
29+
if (!keys || keys.length === 0) {
30+
throw new Error(`Wallet ${walletId} has no keys.`);
31+
}
32+
const keychainId = keys[0];
33+
34+
const enterpriseId = walletData.enterprise as string | undefined;
35+
if (!enterpriseId) {
36+
throw new Error(`Wallet ${walletId} has no enterprise.`);
37+
}
38+
39+
// Fetch user keychain
40+
const keychain = await bitgo.get(bitgo.url(`/${coin}/key/${keychainId}`, 2)).result();
41+
42+
if (!keychain.encryptedPrv) {
43+
throw new Error(
44+
`Keychain ${keychainId} has no encryptedPrv. Cannot attach passkey without an existing encrypted private key.`
45+
);
46+
}
47+
48+
// Derive enterprise-scoped salt
49+
const enterpriseSalt = deriveEnterpriseSalt(device.prfSalt, enterpriseId);
50+
51+
// Decrypt private key with existing passphrase
52+
const privateKey = bitgo.decrypt({ password: existingPassphrase, input: keychain.encryptedPrv });
53+
54+
// PRF assertion — evalByCredential maps this device's credentialId to its enterprise salt
55+
const authResult = await provider.get({
56+
publicKey: {} as PublicKeyCredentialRequestOptions,
57+
evalByCredential: { [device.credentialId]: enterpriseSalt },
58+
});
59+
60+
if (!authResult.prfResult) {
61+
throw new Error('PRF assertion did not return a result.');
62+
}
63+
64+
// Derive password from PRF output and re-encrypt
65+
const prfPassword = derivePassword(authResult.prfResult);
66+
const encryptedPrv = bitgo.encrypt({ password: prfPassword, input: privateKey });
67+
68+
// PUT webauthnInfo to keychain endpoint
69+
const updatedKeychain = await bitgo
70+
.put(bitgo.url(`/${coin}/key/${keychainId}`, 2))
71+
.send({
72+
webauthnInfo: {
73+
prfSalt: enterpriseSalt,
74+
otpDeviceId: device.id,
75+
encryptedPrv,
76+
},
77+
})
78+
.result();
79+
80+
return updatedKeychain as Keychain;
81+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './attachPasskeyToWallet';
2+
export * from './types';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type { WebAuthnOtpDevice, WebAuthnProvider, PasskeyAuthResult, PasskeyGetOptions } from '@bitgo/passkey-crypto';

0 commit comments

Comments
 (0)