Skip to content

Commit fa25394

Browse files
committed
feat(passkey-crypto): extend package with PRF helpers and sdk-core passkey module
- add buildEvalByCredential and matchDeviceByCredentialId to @bitgo/passkey-crypto - use WebauthnDevice from @bitgo/public-types to avoid circular deps - create sdk-core/passkey/ with WebAuthnProvider, PasskeyAuthResult, PasskeyGetOptions - re-export WebAuthnOtpDevice from @bitgo/public-types in sdk-core - alias KeychainWebauthnDevice to WebauthnDevice from @bitgo/public-types - bump @bitgo/public-types to 6.1.0 and add @bitgo/passkey-crypto to sdk-core TICKET: WCN-187
1 parent 41a9575 commit fa25394

12 files changed

Lines changed: 164 additions & 120 deletions

File tree

Dockerfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ COPY --from=builder /tmp/bitgo/modules/express /var/bitgo-express/
4343
#COPY_START
4444
COPY --from=builder /tmp/bitgo/modules/abstract-lightning /var/modules/abstract-lightning/
4545
COPY --from=builder /tmp/bitgo/modules/sdk-core /var/modules/sdk-core/
46+
COPY --from=builder /tmp/bitgo/modules/passkey-crypto /var/modules/passkey-crypto/
47+
COPY --from=builder /tmp/bitgo/modules/sjcl /var/modules/sjcl/
4648
COPY --from=builder /tmp/bitgo/modules/sdk-lib-mpc /var/modules/sdk-lib-mpc/
4749
COPY --from=builder /tmp/bitgo/modules/sdk-opensslbytes /var/modules/sdk-opensslbytes/
48-
COPY --from=builder /tmp/bitgo/modules/sjcl /var/modules/sjcl/
4950
COPY --from=builder /tmp/bitgo/modules/secp256k1 /var/modules/secp256k1/
5051
COPY --from=builder /tmp/bitgo/modules/statics /var/modules/statics/
5152
COPY --from=builder /tmp/bitgo/modules/utxo-lib /var/modules/utxo-lib/
@@ -145,9 +146,10 @@ COPY --from=builder /tmp/bitgo/modules/sdk-coin-zec /var/modules/sdk-coin-zec/
145146

146147
RUN cd /var/modules/abstract-lightning && yarn link && \
147148
cd /var/modules/sdk-core && yarn link && \
149+
cd /var/modules/passkey-crypto && yarn link && \
150+
cd /var/modules/sjcl && yarn link && \
148151
cd /var/modules/sdk-lib-mpc && yarn link && \
149152
cd /var/modules/sdk-opensslbytes && yarn link && \
150-
cd /var/modules/sjcl && yarn link && \
151153
cd /var/modules/secp256k1 && yarn link && \
152154
cd /var/modules/statics && yarn link && \
153155
cd /var/modules/utxo-lib && yarn link && \
@@ -250,9 +252,10 @@ cd /var/modules/sdk-coin-zec && yarn link
250252
RUN cd /var/bitgo-express && \
251253
yarn link @bitgo/abstract-lightning && \
252254
yarn link @bitgo/sdk-core && \
255+
yarn link @bitgo/passkey-crypto && \
256+
yarn link @bitgo/sjcl && \
253257
yarn link @bitgo/sdk-lib-mpc && \
254258
yarn link @bitgo/sdk-opensslbytes && \
255-
yarn link @bitgo/sjcl && \
256259
yarn link @bitgo/secp256k1 && \
257260
yarn link @bitgo/statics && \
258261
yarn link @bitgo/utxo-lib && \

modules/passkey-crypto/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"access": "public"
3535
},
3636
"dependencies": {
37+
"@bitgo/public-types": "6.1.0",
3738
"@bitgo/sjcl": "^1.1.0"
3839
},
3940
"devDependencies": {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { derivePassword } from './derivePassword';
22
export { deriveEnterpriseSalt } from './deriveEnterpriseSalt';
3+
export { buildEvalByCredential, matchDeviceByCredentialId } from './prfHelpers';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { WebauthnDevice } from '@bitgo/public-types';
2+
3+
/**
4+
* Builds the PRF eval map and credential-to-device lookup from a wallet
5+
* keychain's webauthn devices. Devices without a prfSalt are skipped.
6+
*/
7+
export function buildEvalByCredential(devices: WebauthnDevice[]): {
8+
evalByCredential: Record<string, string>;
9+
credIdToDevice: Map<string, WebauthnDevice>;
10+
} {
11+
const evalByCredential: Record<string, string> = {};
12+
const credIdToDevice = new Map<string, WebauthnDevice>();
13+
14+
for (const device of devices) {
15+
if (!device.prfSalt) continue;
16+
const { credID } = device.authenticatorInfo;
17+
evalByCredential[credID] = device.prfSalt;
18+
credIdToDevice.set(credID, device);
19+
}
20+
21+
return { evalByCredential, credIdToDevice };
22+
}
23+
24+
/**
25+
* Returns the WebauthnDevice matching the given credential ID.
26+
* @throws if no matching device is found
27+
*/
28+
export function matchDeviceByCredentialId(devices: WebauthnDevice[], credentialId: string): WebauthnDevice {
29+
const device = devices.find((d) => d.authenticatorInfo.credID === credentialId);
30+
if (!device) {
31+
throw new Error('Could not identify which passkey device was used');
32+
}
33+
return device;
34+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as assert from 'assert';
2+
import { buildEvalByCredential, matchDeviceByCredentialId } from '../../src';
3+
import { WebauthnDevice } from '@bitgo/public-types';
4+
5+
const device1: WebauthnDevice = {
6+
otpDeviceId: 'oid-1',
7+
authenticatorInfo: { credID: 'cred-aaa', fmt: 'none', publicKey: 'pk-1' },
8+
prfSalt: 'salt-aaa',
9+
encryptedPrv: 'enc-prv-1',
10+
};
11+
12+
const device2: WebauthnDevice = {
13+
otpDeviceId: 'oid-2',
14+
authenticatorInfo: { credID: 'cred-bbb', fmt: 'none', publicKey: 'pk-2' },
15+
prfSalt: 'salt-bbb',
16+
encryptedPrv: 'enc-prv-2',
17+
};
18+
19+
describe('buildEvalByCredential', function () {
20+
it('maps each device credID to its prfSalt in evalByCredential', function () {
21+
const { evalByCredential } = buildEvalByCredential([device1, device2]);
22+
assert.deepStrictEqual(evalByCredential, { 'cred-aaa': 'salt-aaa', 'cred-bbb': 'salt-bbb' });
23+
});
24+
25+
it('populates credIdToDevice with both devices', function () {
26+
const { credIdToDevice } = buildEvalByCredential([device1, device2]);
27+
assert.strictEqual(credIdToDevice.get('cred-aaa'), device1);
28+
assert.strictEqual(credIdToDevice.get('cred-bbb'), device2);
29+
});
30+
31+
it('returns empty maps for an empty device list', function () {
32+
const { evalByCredential, credIdToDevice } = buildEvalByCredential([]);
33+
assert.deepStrictEqual(evalByCredential, {});
34+
assert.strictEqual(credIdToDevice.size, 0);
35+
});
36+
37+
it('skips devices with empty prfSalt', function () {
38+
const deviceNoPrf = { ...device1, prfSalt: '' };
39+
const { evalByCredential, credIdToDevice } = buildEvalByCredential([deviceNoPrf, device2]);
40+
assert.deepStrictEqual(evalByCredential, { 'cred-bbb': 'salt-bbb' });
41+
assert.strictEqual(credIdToDevice.has('cred-aaa'), false);
42+
});
43+
44+
it('skips devices with undefined prfSalt', function () {
45+
const deviceNoPrf = { ...device1, prfSalt: undefined as unknown as string };
46+
const { evalByCredential, credIdToDevice } = buildEvalByCredential([deviceNoPrf, device2]);
47+
assert.deepStrictEqual(evalByCredential, { 'cred-bbb': 'salt-bbb' });
48+
assert.strictEqual(credIdToDevice.has('cred-aaa'), false);
49+
});
50+
});
51+
52+
describe('matchDeviceByCredentialId', function () {
53+
it('returns the matching device', function () {
54+
assert.strictEqual(matchDeviceByCredentialId([device1, device2], 'cred-bbb'), device2);
55+
});
56+
57+
it('returns the first device when it matches', function () {
58+
assert.strictEqual(matchDeviceByCredentialId([device1, device2], 'cred-aaa'), device1);
59+
});
60+
61+
it('returns a device even when it has no prfSalt', function () {
62+
const deviceNoPrf = { ...device1, prfSalt: '' };
63+
assert.strictEqual(matchDeviceByCredentialId([deviceNoPrf, device2], 'cred-aaa'), deviceNoPrf);
64+
});
65+
66+
it('throws the expected error message when no device matches', function () {
67+
assert.throws(
68+
() => matchDeviceByCredentialId([device1, device2], 'cred-unknown'),
69+
(err: Error) => {
70+
assert.strictEqual(err.message, 'Could not identify which passkey device was used');
71+
return true;
72+
}
73+
);
74+
});
75+
76+
it('throws when the device list is empty', function () {
77+
assert.throws(() => matchDeviceByCredentialId([], 'cred-aaa'), Error);
78+
});
79+
});

modules/passkey-crypto/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"extends": "../../tsconfig.json",
33
"compilerOptions": {
4+
"lib": ["ES2020"],
45
"outDir": "./dist",
56
"rootDir": "./",
67
"strictPropertyInitialization": false,

modules/sdk-core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
]
4141
},
4242
"dependencies": {
43-
"@bitgo/public-types": "5.96.2",
43+
"@bitgo/passkey-crypto": "^0.1.0",
44+
"@bitgo/public-types": "6.1.0",
4445
"@bitgo/sdk-lib-mpc": "^10.11.1",
4546
"@bitgo/secp256k1": "^1.11.0",
4647
"@bitgo/sjcl": "^1.1.0",

modules/sdk-core/src/bitgo/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './internal';
1717
export * from './keychain';
1818
export * as bitcoin from './legacyBitcoin';
1919
export * from './market';
20+
export * from './passkey';
2021
export * from './pendingApproval';
2122
export { WalletProofs } from './proofs';
2223
export * from './recovery';

modules/sdk-core/src/bitgo/keychain/iKeychains.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { KeychainsTriplet, KeyPair } from '../baseCoin';
33
import { BitgoPubKeyType } from '../utils/tss/baseTypes';
44
import { IWallet } from '../wallet';
55
import { BitGoKeyFromOvcShares, OvcToBitGoJSON } from './ovcJsonCodec';
6+
import type { WebauthnDevice } from '@bitgo/public-types';
7+
8+
export type KeychainWebauthnDevice = WebauthnDevice;
69

710
export type KeyType = 'tss' | 'independent' | 'blsdkg';
811

@@ -14,23 +17,6 @@ export interface WebauthnInfo {
1417

1518
export type SourceType = 'bitgo' | 'backup' | 'user' | 'cold';
1619

17-
export type WebauthnFmt = 'none' | 'packed' | 'fido-u2f';
18-
19-
export interface WebauthnAuthenticatorInfo {
20-
credID: string;
21-
fmt: WebauthnFmt;
22-
publicKey: string;
23-
}
24-
25-
export interface KeychainWebauthnDevice {
26-
otpDeviceId: string;
27-
authenticatorInfo: WebauthnAuthenticatorInfo;
28-
// salt for the webauthn prf extension
29-
prfSalt: string;
30-
// Wallet private key encrypted to webauthn derived password
31-
encryptedPrv: string;
32-
}
33-
3420
export interface Keychain {
3521
id: string;
3622
pub?: string;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type { WebAuthnOtpDevice, PasskeyAuthResult, PasskeyGetOptions, WebAuthnProvider } from './types';
2+
export { buildEvalByCredential, matchDeviceByCredentialId } from '@bitgo/passkey-crypto';

0 commit comments

Comments
 (0)