Skip to content

Commit b52c7ae

Browse files
feat(sdk-core): add webauthnInfo support to createMpc [WAL-761]
What changed: - Thread webauthnInfo through createMpc/createKeychains so hardware authenticator (PRF-derived) encryption can be stored alongside the standard encryptedPrv on user keychains - Use encryptAsync instead of encrypt for webauthnDevices entries so v2 encryption is applied consistently - Replace ad-hoc literal 1 with ShareKeyPosition.USER in ecdsa.ts webauthn guard - Remove AddKeychainOptions annotation that was inadvertently excluding prv from the type - Rename WebAuthn passphrase param type to WebauthnKeyEncryptionInfo, move canonical definition to iWallets.ts, consolidate duplicate AcceptShareWebauthnInfo, and re-export from iKeychains.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9370461 commit b52c7ae

12 files changed

Lines changed: 158 additions & 34 deletions

File tree

modules/bitgo/test/v2/unit/keychains.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,51 @@ describe('V2 Keychains', function () {
434434
keychains.should.deepEqual(stubbedKeychainsTriplet);
435435
});
436436
});
437+
438+
['tsol'].forEach((coin) => {
439+
it('should pass webauthnInfo to createKeychains for EDDSA TSS', async function () {
440+
const webauthnInfo = {
441+
otpDeviceId: 'device-1',
442+
prfSalt: 'salt-1',
443+
passphrase: 'prf-derived-passphrase',
444+
};
445+
const createKeychains = sandbox
446+
.stub(EDDSAUtils.default.prototype, 'createKeychains')
447+
.resolves(stubbedKeychainsTriplet);
448+
await bitgo.coin(coin).keychains().createMpc({
449+
multisigType: 'tss',
450+
passphrase: 'password',
451+
enterprise: 'enterprise',
452+
webauthnInfo,
453+
});
454+
createKeychains.calledOnce.should.be.true();
455+
createKeychains.firstCall.args[0].should.have.property('webauthnInfo', webauthnInfo);
456+
});
457+
});
458+
459+
['tbsc'].forEach((coin) => {
460+
it('should pass webauthnInfo to createKeychains for ECDSA TSS', async function () {
461+
nock(bgUrl).get('/api/v2/tss/settings').reply(200, {
462+
coinSettings: {},
463+
});
464+
const webauthnInfo = {
465+
otpDeviceId: 'device-1',
466+
prfSalt: 'salt-1',
467+
passphrase: 'prf-derived-passphrase',
468+
};
469+
const createKeychains = sandbox
470+
.stub(ECDSAUtils.EcdsaUtils.prototype, 'createKeychains')
471+
.resolves(stubbedKeychainsTriplet);
472+
await bitgo.coin(coin).keychains().createMpc({
473+
multisigType: 'tss',
474+
passphrase: 'password',
475+
enterprise: 'enterprise',
476+
webauthnInfo,
477+
});
478+
createKeychains.calledOnce.should.be.true();
479+
createKeychains.firstCall.args[0].should.have.property('webauthnInfo', webauthnInfo);
480+
});
481+
});
437482
});
438483

439484
describe('Recreate Keychains from MPCV1 to MPCV2', async function () {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export interface WebauthnInfo {
1212
encryptedPrv: string;
1313
}
1414

15+
import type { WebauthnKeyEncryptionInfo } from '../wallet/iWallets';
16+
export type { WebauthnKeyEncryptionInfo };
17+
1518
export type SourceType = 'bitgo' | 'backup' | 'user' | 'cold';
1619

1720
export type WebauthnFmt = 'none' | 'packed' | 'fido-u2f';
@@ -189,6 +192,7 @@ export interface CreateMpcOptions {
189192
originalPasscodeEncryptionCode?: string;
190193
enterprise?: string;
191194
retrofit?: DecryptedRetrofitPayload;
195+
webauthnInfo?: WebauthnKeyEncryptionInfo;
192196
encryptionVersion?: EncryptionVersion;
193197
}
194198

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ export class Keychains implements IKeychains {
332332
enterprise: params.enterprise,
333333
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
334334
retrofit: params.retrofit,
335+
webauthnInfo: params.webauthnInfo,
335336
encryptionVersion: params.encryptionVersion,
336337
});
337338
}

modules/sdk-core/src/bitgo/utils/mpcUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import assert from 'assert';
55
import { decrypt, readMessage, readPrivateKey, SerializedKeyPair } from 'openpgp';
66
import { IBaseCoin, KeychainsTriplet } from '../baseCoin';
77
import { BitGoBase } from '../bitgoBase';
8-
import { AddKeychainOptions, Keychain, KeyType } from '../keychain';
8+
import { AddKeychainOptions, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../keychain';
99
import { encryptText, getBitgoGpgPubKey } from './opengpgUtils';
1010
import {
1111
IntentRecipient,
@@ -105,6 +105,7 @@ export abstract class MpcUtils {
105105
passphrase: string;
106106
enterprise?: string;
107107
originalPasscodeEncryptionCode?: string;
108+
webauthnInfo?: WebauthnKeyEncryptionInfo;
108109
}): Promise<KeychainsTriplet>;
109110

110111
/**

modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as openpgp from 'openpgp';
33
import { Key, readKey, SerializedKeyPair } from 'openpgp';
44
import { IBaseCoin, KeychainsTriplet } from '../../baseCoin';
55
import { BitGoBase } from '../../bitgoBase';
6-
import { Keychain, KeyIndices } from '../../keychain';
6+
import { Keychain, KeyIndices, WebauthnKeyEncryptionInfo } from '../../keychain';
77
import { getTxRequest } from '../../tss';
88
import { IWallet } from '../../wallet';
99
import { MpcUtils } from '../mpcUtils';
@@ -216,6 +216,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
216216
enterprise?: string | undefined;
217217
originalPasscodeEncryptionCode?: string | undefined;
218218
isThirdPartyBackup?: boolean;
219+
webauthnInfo?: WebauthnKeyEncryptionInfo;
219220
encryptionVersion?: EncryptionVersion;
220221
}): Promise<KeychainsTriplet> {
221222
throw new Error('Method not implemented.');

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Key, SerializedKeyPair } from 'openpgp';
22
import { EncryptionVersion, IEncryptionSession, IRequestTracer } from '../../../api';
33
import { type ITransactionRecipient, KeychainsTriplet, ParsedTransaction, TransactionParams } from '../../baseCoin';
4-
import { ApiKeyShare, Keychain } from '../../keychain';
4+
import { ApiKeyShare, Keychain, WebauthnKeyEncryptionInfo } from '../../keychain';
55
import { ApiVersion, Memo, WalletType } from '../../wallet';
66
import { EDDSA, GShare, Signature, SignShare } from '../../../account-lib/mpc/tss';
77
import { Signature as EcdsaSignature } from '../../../account-lib/mpc/tss/ecdsa/types';
@@ -482,6 +482,7 @@ export type CreateKeychainParamsBase = {
482482
passphrase?: string;
483483
enterprise?: string;
484484
originalPasscodeEncryptionCode?: string;
485+
webauthnInfo?: WebauthnKeyEncryptionInfo;
485486
encryptionVersion?: EncryptionVersion;
486487
encryptionSession?: IEncryptionSession;
487488
};

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { EcdsaPaillierProof, EcdsaRangeProof, EcdsaTypes, hexToBigInt, minModulu
77
import { bip32 } from '@bitgo/utxo-lib';
88

99
import { ECDSA, Ecdsa } from '../../../../account-lib/mpc/tss';
10-
import { AddKeychainOptions, Keychain, KeyType } from '../../../keychain';
10+
import { AddKeychainOptions, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../../../keychain';
1111
import ECDSAMethods, { ECDSAMethodTypes } from '../../../tss/ecdsa';
1212
import { KeychainsTriplet } from '../../../baseCoin';
1313
import {
@@ -109,6 +109,7 @@ export class EcdsaUtils extends BaseEcdsaUtils {
109109
passphrase: string;
110110
enterprise?: string | undefined;
111111
originalPasscodeEncryptionCode?: string | undefined;
112+
webauthnInfo?: WebauthnKeyEncryptionInfo;
112113
}): Promise<KeychainsTriplet> {
113114
const MPC = new Ecdsa();
114115
const m = 2;
@@ -141,6 +142,7 @@ export class EcdsaUtils extends BaseEcdsaUtils {
141142
bitgoKeychain,
142143
passphrase: params.passphrase,
143144
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
145+
webauthnInfo: params.webauthnInfo,
144146
});
145147
const backupKeychainPromise = this.createBackupKeychain({
146148
userGpgKey,
@@ -180,6 +182,7 @@ export class EcdsaUtils extends BaseEcdsaUtils {
180182
bitgoKeychain,
181183
passphrase,
182184
originalPasscodeEncryptionCode,
185+
webauthnInfo,
183186
}: CreateEcdsaKeychainParams): Promise<Keychain> {
184187
if (!passphrase) {
185188
throw new Error('Please provide a wallet passphrase');
@@ -194,7 +197,8 @@ export class EcdsaUtils extends BaseEcdsaUtils {
194197
backupKeyShare.userHeldKeyShare,
195198
bitgoKeychain,
196199
passphrase,
197-
originalPasscodeEncryptionCode
200+
originalPasscodeEncryptionCode,
201+
webauthnInfo
198202
);
199203
}
200204

@@ -307,7 +311,8 @@ export class EcdsaUtils extends BaseEcdsaUtils {
307311
backupKeyShare: KeyShare,
308312
bitgoKeychain: Keychain,
309313
passphrase: string,
310-
originalPasscodeEncryptionCode?: string
314+
originalPasscodeEncryptionCode?: string,
315+
webauthnInfo?: WebauthnKeyEncryptionInfo
311316
): Promise<Keychain> {
312317
const bitgoKeyShares = bitgoKeychain.keyShares;
313318
if (!bitgoKeyShares) {
@@ -402,6 +407,19 @@ export class EcdsaUtils extends BaseEcdsaUtils {
402407
password: passphrase,
403408
}),
404409
originalPasscodeEncryptionCode,
410+
webauthnDevices:
411+
webauthnInfo && recipientIndex === ShareKeyPosition.USER
412+
? [
413+
{
414+
otpDeviceId: webauthnInfo.otpDeviceId,
415+
prfSalt: webauthnInfo.prfSalt,
416+
encryptedPrv: await this.bitgo.encryptAsync({
417+
input: prv,
418+
password: webauthnInfo.passphrase,
419+
}),
420+
},
421+
]
422+
: undefined,
405423
};
406424

407425
const keychains = this.baseCoin.keychains();

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from '@bitgo/public-types';
2222

2323
import { Ecdsa } from '../../../../account-lib';
24-
import { AddKeychainOptions, Keychain, KeyType } from '../../../keychain';
24+
import { AddKeychainOptions, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../../../keychain';
2525
import { DecryptedRetrofitPayload } from '../../../keychain/iKeychains';
2626
import { ECDSAMethodTypes, getTxRequest } from '../../../tss';
2727
import { sendSignatureShareV2, sendTxRequest } from '../../../tss/common';
@@ -66,6 +66,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
6666
enterprise: string;
6767
originalPasscodeEncryptionCode?: string;
6868
retrofit?: DecryptedRetrofitPayload;
69+
webauthnInfo?: WebauthnKeyEncryptionInfo;
6970
encryptionVersion?: EncryptionVersion;
7071
}): Promise<KeychainsTriplet> {
7172
const { userSession, backupSession } = this.getUserAndBackupSession(2, 3, params.retrofit);
@@ -332,6 +333,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
332333
userReducedPrivateMaterial,
333334
params.passphrase,
334335
params.originalPasscodeEncryptionCode,
336+
params.webauthnInfo,
335337
encryptionSession
336338
);
337339
const backupKeychainPromise = this.addBackupKeychain(
@@ -369,6 +371,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
369371
reducedPrivateMaterial?: Buffer,
370372
passphrase?: string,
371373
originalPasscodeEncryptionCode?: string,
374+
webauthnInfo?: WebauthnKeyEncryptionInfo,
372375
encryptionSession?: {
373376
encrypt(plaintext: string): Promise<string>;
374377
decrypt(ciphertext: string): Promise<string>;
@@ -378,21 +381,23 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
378381
let source: string;
379382
let encryptedPrv: string | undefined = undefined;
380383
let reducedEncryptedPrv: string | undefined = undefined;
384+
let privateMaterialBase64: string | undefined = undefined;
381385
switch (participantIndex) {
382386
case MPCv2PartiesEnum.USER:
383387
case MPCv2PartiesEnum.BACKUP:
384388
source = participantIndex === MPCv2PartiesEnum.USER ? 'user' : 'backup';
385389
assert(privateMaterial, `Private material is required for ${source} keychain`);
386390
assert(reducedPrivateMaterial, `Reduced private material is required for ${source} keychain`);
387391
assert(passphrase, `Passphrase is required for ${source} keychain`);
392+
privateMaterialBase64 = privateMaterial.toString('base64');
388393
if (encryptionSession) {
389-
encryptedPrv = await encryptionSession.encrypt(privateMaterial.toString('base64'));
394+
encryptedPrv = await encryptionSession.encrypt(privateMaterialBase64);
390395
reducedEncryptedPrv = await encryptionSession.encrypt(
391396
btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(reducedPrivateMaterial))))
392397
);
393398
} else {
394399
encryptedPrv = this.bitgo.encrypt({
395-
input: privateMaterial.toString('base64'),
400+
input: privateMaterialBase64,
396401
password: passphrase,
397402
});
398403
// Encrypts the CBOR-encoded ReducedKeyShare (which contains the party's private
@@ -423,6 +428,19 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
423428
isMPCv2: true,
424429
};
425430

431+
if (webauthnInfo && participantIndex === MPCv2PartiesEnum.USER && privateMaterialBase64) {
432+
recipientKeychainParams.webauthnDevices = [
433+
{
434+
otpDeviceId: webauthnInfo.otpDeviceId,
435+
prfSalt: webauthnInfo.prfSalt,
436+
encryptedPrv: await this.bitgo.encryptAsync({
437+
input: privateMaterialBase64,
438+
password: webauthnInfo.passphrase,
439+
}),
440+
},
441+
];
442+
}
443+
426444
const keychains = this.baseCoin.keychains();
427445
return { ...(await keychains.add(recipientKeychainParams)), reducedEncryptedPrv: reducedEncryptedPrv };
428446
}
@@ -543,6 +561,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
543561
reducedPrivateMaterial: Buffer,
544562
passphrase: string,
545563
originalPasscodeEncryptionCode?: string,
564+
webauthnInfo?: WebauthnKeyEncryptionInfo,
546565
encryptionSession?: {
547566
encrypt(plaintext: string): Promise<string>;
548567
decrypt(ciphertext: string): Promise<string>;
@@ -556,6 +575,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
556575
reducedPrivateMaterial,
557576
passphrase,
558577
originalPasscodeEncryptionCode,
578+
webauthnInfo,
559579
encryptionSession
560580
);
561581
}
@@ -579,6 +599,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
579599
reducedPrivateMaterial,
580600
passphrase,
581601
originalPasscodeEncryptionCode,
602+
undefined,
582603
encryptionSession
583604
);
584605
}

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import assert from 'assert';
55
import * as openpgp from 'openpgp';
66
import Eddsa, { GShare, SignShare } from '../../../../account-lib/mpc/tss';
7-
import { AddKeychainOptions, CreateBackupOptions, Keychain } from '../../../keychain';
7+
import { AddKeychainOptions, CreateBackupOptions, Keychain, WebauthnKeyEncryptionInfo } from '../../../keychain';
88
import { verifyWalletSignature } from '../../../tss/eddsa/eddsa';
99
import { createShareProof, encryptText, generateGPGKeyPair, getBitgoGpgPubKey } from '../../opengpgUtils';
1010
import {
@@ -132,6 +132,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
132132
bitgoKeychain,
133133
passphrase,
134134
originalPasscodeEncryptionCode,
135+
webauthnInfo,
135136
encryptionSession,
136137
}: CreateEddsaKeychainParams): Promise<Keychain> {
137138
const MPC = await Eddsa.initialize();
@@ -198,6 +199,18 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
198199
});
199200
}
200201
}
202+
if (webauthnInfo && userKeychainParams.encryptedPrv) {
203+
userKeychainParams.webauthnDevices = [
204+
{
205+
otpDeviceId: webauthnInfo.otpDeviceId,
206+
prfSalt: webauthnInfo.prfSalt,
207+
encryptedPrv: await this.bitgo.encryptAsync({
208+
input: JSON.stringify(userSigningMaterial),
209+
password: webauthnInfo.passphrase,
210+
}),
211+
},
212+
];
213+
}
201214

202215
return await this.baseCoin.keychains().add(userKeychainParams);
203216
}
@@ -358,6 +371,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
358371
passphrase?: string;
359372
enterprise?: string;
360373
originalPasscodeEncryptionCode?: string;
374+
webauthnInfo?: WebauthnKeyEncryptionInfo;
361375
encryptionVersion?: EncryptionVersion;
362376
}): Promise<KeychainsTriplet> {
363377
const MPC = await Eddsa.initialize();
@@ -391,6 +405,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
391405
bitgoKeychain,
392406
passphrase: params.passphrase,
393407
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
408+
webauthnInfo: params.webauthnInfo,
394409
encryptionSession,
395410
});
396411
const backupKeychainPromise = this.createBackupKeychain({

0 commit comments

Comments
 (0)