Skip to content

Commit b892927

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

10 files changed

Lines changed: 161 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { derivePassword } from './derivePassword';
22
export { deriveEnterpriseSalt } from './deriveEnterpriseSalt';
3+
export { buildEvalByCredential, matchDeviceByCredentialId } from './prfHelpers';
4+
export type { WebAuthnOtpDevice, PasskeyAuthResult, PasskeyGetOptions, WebAuthnProvider } from './webAuthnTypes';
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export type { WebAuthnOtpDevice } from '@bitgo/public-types';
2+
3+
/** Result of a WebAuthn assertion with the PRF extension. */
4+
export interface PasskeyAuthResult {
5+
prfResult: ArrayBuffer | undefined;
6+
credentialId: string;
7+
otpCode: string;
8+
}
9+
10+
/** Options for WebAuthnProvider.get(). */
11+
export interface PasskeyGetOptions {
12+
publicKey: PublicKeyCredentialRequestOptions;
13+
evalByCredential?: Record<string, string>;
14+
}
15+
16+
/** Abstraction over the WebAuthn credential API. */
17+
export interface WebAuthnProvider {
18+
create(options: PublicKeyCredentialCreationOptions): Promise<PublicKeyCredential>;
19+
get(options: PasskeyGetOptions): Promise<PasskeyAuthResult>;
20+
}
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", "DOM"],
45
"outDir": "./dist",
56
"rootDir": "./",
67
"strictPropertyInitialization": false,

modules/sdk-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
]
4141
},
4242
"dependencies": {
43-
"@bitgo/public-types": "5.96.2",
43+
"@bitgo/public-types": "6.1.0",
4444
"@bitgo/sdk-lib-mpc": "^10.11.1",
4545
"@bitgo/secp256k1": "^1.11.0",
4646
"@bitgo/sjcl": "^1.1.0",

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;

0 commit comments

Comments
 (0)