Skip to content

Commit 5682ced

Browse files
Merge pull request #5084 from BitGo/WP-2959/do-retrofit-during-recovery
feat: retrofit during GG18 user and backup recovery
2 parents e4dcbe1 + fe036bd commit 5682ced

File tree

26 files changed

+489
-976
lines changed

26 files changed

+489
-976
lines changed

modules/abstract-cosmos/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
},
4040
"dependencies": {
4141
"@bitgo/sdk-core": "^28.12.0",
42-
"@bitgo/sdk-lib-mpc": "^10.1.0",
4342
"@bitgo/statics": "^50.5.0",
4443
"@bitgo/utxo-lib": "^11.0.1",
4544
"@cosmjs/amino": "^0.29.5",

modules/abstract-cosmos/src/cosmosCoin.ts

Lines changed: 25 additions & 238 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ import {
22
BaseCoin,
33
BaseTransaction,
44
BitGoBase,
5-
ECDSA,
65
Ecdsa,
7-
ECDSAMethodTypes,
86
ECDSAUtils,
97
ExplanationResult,
10-
hexToBigInt,
118
InvalidAddressError,
129
InvalidMemoIdError,
1310
KeyPair,
@@ -22,7 +19,6 @@ import {
2219
VerifyAddressOptions,
2320
VerifyTransactionOptions,
2421
} from '@bitgo/sdk-core';
25-
import { EcdsaPaillierProof, EcdsaRangeProof, EcdsaTypes } from '@bitgo/sdk-lib-mpc';
2622
import { BaseCoin as StaticsBaseCoin, CoinFamily } from '@bitgo/statics';
2723
import { bip32 } from '@bitgo/utxo-lib';
2824
import { Coin } from '@cosmjs/stargate';
@@ -136,11 +132,8 @@ export class CosmosCoin extends BaseCoin {
136132
* @returns {CosmosLikeCoinRecoveryOutput} the serialized transaction hex string and index
137133
* of the address being swept
138134
*/
139-
async recover(params: RecoveryOptions, openSSLBytes: Uint8Array): Promise<CosmosLikeCoinRecoveryOutput> {
135+
async recover(params: RecoveryOptions): Promise<CosmosLikeCoinRecoveryOutput> {
140136
// Step 1: Check if params contains the required parameters
141-
if (!params.bitgoKey) {
142-
throw new Error('missing bitgoKey');
143-
}
144137

145138
if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) {
146139
throw new Error('invalid recoveryDestination');
@@ -157,19 +150,20 @@ export class CosmosCoin extends BaseCoin {
157150
if (!params.walletPassphrase) {
158151
throw new Error('missing wallet passphrase');
159152
}
160-
if (!openSSLBytes) {
161-
throw new Error('missing openSSLBytes');
162-
}
163153

164154
// Step 2: Fetch the bitgo key from params
165155
const userKey = params.userKey.replace(/\s/g, '');
166156
const backupKey = params.backupKey.replace(/\s/g, '');
167-
const bitgoKey = params.bitgoKey.replace(/\s/g, '');
168157

158+
const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares(
159+
userKey,
160+
backupKey,
161+
params.walletPassphrase
162+
); // baseAddress is not extracted
169163
// Step 3: Instantiate the ECDSA signer and fetch the address details
170164
const MPC = new Ecdsa();
171165
const chainId = await this.getChainId();
172-
const publicKey = MPC.deriveUnhardened(bitgoKey, ROOT_PATH).slice(0, 66);
166+
const publicKey = MPC.deriveUnhardened(commonKeyChain, ROOT_PATH).slice(0, 66);
173167
const senderAddress = this.getAddressFromPublicKey(publicKey);
174168

175169
// Step 4: Fetch account details such as accountNo, balance and check for sufficient funds once gasAmount has been deducted
@@ -214,47 +208,11 @@ export class CosmosCoin extends BaseCoin {
214208
let serializedTx = unsignedTransaction.toBroadcastFormat();
215209
const signableHex = unsignedTransaction.signablePayload.toString('hex');
216210

217-
const isGG18SigningMaterial = ECDSAUtils.isGG18SigningMaterial(userKey, params.walletPassphrase);
218-
let signature: ECDSA.Signature;
219-
220-
if (isGG18SigningMaterial) {
221-
// GG18
222-
const [userKeyCombined, backupKeyCombined] = ((): [
223-
ECDSAMethodTypes.KeyCombined | undefined,
224-
ECDSAMethodTypes.KeyCombined | undefined
225-
] => {
226-
const [userKeyCombined, backupKeyCombined] = this.getKeyCombinedFromTssKeyShares(
227-
userKey,
228-
backupKey,
229-
params.walletPassphrase
230-
);
231-
return [userKeyCombined, backupKeyCombined];
232-
})();
233-
234-
if (!userKeyCombined || !backupKeyCombined) {
235-
throw new Error('Missing combined key shares for user or backup');
236-
}
237-
238-
// Step 7: Sign the tx
239-
signature = await this.signRecoveryTSS(userKeyCombined, backupKeyCombined, signableHex, openSSLBytes);
240-
} else {
241-
// DKLS
242-
const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares(
243-
userKey,
244-
backupKey,
245-
params.walletPassphrase
246-
); // baseAddress is not extracted
247-
248-
if (!userKeyShare || !backupKeyShare || !commonKeyChain) {
249-
throw new Error('Missing combined key shares for user or backup or common');
250-
}
251-
252-
// Step 7: Sign the tx
253-
const message = unsignedTransaction.signablePayload;
254-
const messageHash = (utils.getHashFunction() || createHash('sha256')).update(message).digest();
211+
// Step 7: Sign the tx
212+
const message = unsignedTransaction.signablePayload;
213+
const messageHash = (utils.getHashFunction() || createHash('sha256')).update(message).digest();
255214

256-
signature = await ECDSAUtils.signRecoveryMpcV2(messageHash, userKeyShare, backupKeyShare, commonKeyChain);
257-
}
215+
const signature = await ECDSAUtils.signRecoveryMpcV2(messageHash, userKeyShare, backupKeyShare, commonKeyChain);
258216

259217
const signableBuffer = Buffer.from(signableHex, 'hex');
260218
MPC.verify(signableBuffer, signature, this.getHashFunction());
@@ -278,13 +236,8 @@ export class CosmosCoin extends BaseCoin {
278236
validatorSrcAddress: string;
279237
validatorDstAddress: string;
280238
amountToRedelegate: string;
281-
},
282-
openSSLBytes: Uint8Array
283-
): Promise<CosmosLikeCoinRecoveryOutput> {
284-
if (!params.bitgoKey) {
285-
throw new Error('missing bitgoKey');
286239
}
287-
240+
): Promise<CosmosLikeCoinRecoveryOutput> {
288241
if (!params.validatorSrcAddress || !this.isValidAddress(params.validatorSrcAddress)) {
289242
throw new Error('invalid validatorSrcAddress');
290243
}
@@ -308,14 +261,19 @@ export class CosmosCoin extends BaseCoin {
308261
if (!params.amountToRedelegate) {
309262
throw new Error('missing amountToRedelegate');
310263
}
311-
if (!openSSLBytes) {
312-
throw new Error('missing openSSLBytes');
313-
}
314-
const bitgoKey = params.bitgoKey.replace(/\s/g, '');
264+
265+
const userKey = params.userKey.replace(/\s/g, '');
266+
const backupKey = params.backupKey.replace(/\s/g, '');
267+
268+
const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares(
269+
userKey,
270+
backupKey,
271+
params.walletPassphrase
272+
); // baseAddress is not extracted
315273

316274
const MPC = new Ecdsa();
317275
const chainId = await this.getChainId();
318-
const publicKey = MPC.deriveUnhardened(bitgoKey, ROOT_PATH).slice(0, 66);
276+
const publicKey = MPC.deriveUnhardened(commonKeyChain, ROOT_PATH).slice(0, 66);
319277
const senderAddress = this.getAddressFromPublicKey(publicKey);
320278

321279
const [accountNumber, sequenceNo] = await this.getAccountDetails(senderAddress);
@@ -350,25 +308,9 @@ export class CosmosCoin extends BaseCoin {
350308
const unsignedTransaction = (await txnBuilder.build()) as CosmosTransaction;
351309
let serializedTx = unsignedTransaction.toBroadcastFormat();
352310
const signableHex = unsignedTransaction.signablePayload.toString('hex');
353-
const userKey = params.userKey.replace(/\s/g, '');
354-
const backupKey = params.backupKey.replace(/\s/g, '');
355-
const [userKeyCombined, backupKeyCombined] = ((): [
356-
ECDSAMethodTypes.KeyCombined | undefined,
357-
ECDSAMethodTypes.KeyCombined | undefined
358-
] => {
359-
const [userKeyCombined, backupKeyCombined] = this.getKeyCombinedFromTssKeyShares(
360-
userKey,
361-
backupKey,
362-
params.walletPassphrase
363-
);
364-
return [userKeyCombined, backupKeyCombined];
365-
})();
366-
367-
if (!userKeyCombined || !backupKeyCombined) {
368-
throw new Error('Missing combined key shares for user or backup');
369-
}
370-
371-
const signature = await this.signRecoveryTSS(userKeyCombined, backupKeyCombined, signableHex, openSSLBytes);
311+
const message = unsignedTransaction.signablePayload;
312+
const messageHash = (utils.getHashFunction() || createHash('sha256')).update(message).digest();
313+
const signature = await ECDSAUtils.signRecoveryMpcV2(messageHash, userKeyShare, backupKeyShare, commonKeyChain);
372314
const signableBuffer = Buffer.from(signableHex, 'hex');
373315
MPC.verify(signableBuffer, signature, this.getHashFunction());
374316
const cosmosKeyPair = this.getKeyPair(publicKey);
@@ -379,161 +321,6 @@ export class CosmosCoin extends BaseCoin {
379321
return { serializedTx: serializedTx };
380322
}
381323

382-
private getKeyCombinedFromTssKeyShares(
383-
userPublicOrPrivateKeyShare: string,
384-
backupPrivateOrPublicKeyShare: string,
385-
walletPassphrase?: string
386-
): [ECDSAMethodTypes.KeyCombined, ECDSAMethodTypes.KeyCombined] {
387-
let backupPrv;
388-
let userPrv;
389-
try {
390-
backupPrv = this.bitgo.decrypt({
391-
input: backupPrivateOrPublicKeyShare,
392-
password: walletPassphrase,
393-
});
394-
userPrv = this.bitgo.decrypt({
395-
input: userPublicOrPrivateKeyShare,
396-
password: walletPassphrase,
397-
});
398-
} catch (e) {
399-
throw new Error(`Error decrypting backup keychain: ${e.message}`);
400-
}
401-
402-
const userSigningMaterial = JSON.parse(userPrv) as ECDSAMethodTypes.SigningMaterial;
403-
const backupSigningMaterial = JSON.parse(backupPrv) as ECDSAMethodTypes.SigningMaterial;
404-
405-
if (!userSigningMaterial.backupNShare) {
406-
throw new Error('Invalid user key - missing backupNShare');
407-
}
408-
409-
if (!backupSigningMaterial.userNShare) {
410-
throw new Error('Invalid backup key - missing userNShare');
411-
}
412-
413-
const MPC = new Ecdsa();
414-
415-
const userKeyCombined = MPC.keyCombine(userSigningMaterial.pShare, [
416-
userSigningMaterial.bitgoNShare,
417-
userSigningMaterial.backupNShare,
418-
]);
419-
420-
const userSigningKeyDerived = MPC.keyDerive(
421-
userSigningMaterial.pShare,
422-
[userSigningMaterial.bitgoNShare, userSigningMaterial.backupNShare],
423-
'm/0'
424-
);
425-
426-
const userKeyDerivedCombined = {
427-
xShare: userSigningKeyDerived.xShare,
428-
yShares: userKeyCombined.yShares,
429-
};
430-
431-
const backupKeyCombined = MPC.keyCombine(backupSigningMaterial.pShare, [
432-
userSigningKeyDerived.nShares[2],
433-
backupSigningMaterial.bitgoNShare,
434-
]);
435-
436-
if (
437-
userKeyDerivedCombined.xShare.y !== backupKeyCombined.xShare.y ||
438-
userKeyDerivedCombined.xShare.chaincode !== backupKeyCombined.xShare.chaincode
439-
) {
440-
throw new Error('Common keychains do not match');
441-
}
442-
443-
return [userKeyDerivedCombined, backupKeyCombined];
444-
}
445-
446-
// TODO(BG-78714): Reduce code duplication between this and eth.ts
447-
private async signRecoveryTSS(
448-
userKeyCombined: ECDSA.KeyCombined,
449-
backupKeyCombined: ECDSA.KeyCombined,
450-
txHex: string,
451-
openSSLBytes: Uint8Array,
452-
{
453-
rangeProofChallenge,
454-
}: {
455-
rangeProofChallenge?: EcdsaTypes.SerializedNtilde;
456-
} = {}
457-
): Promise<ECDSAMethodTypes.Signature> {
458-
const MPC = new Ecdsa();
459-
const signerOneIndex = userKeyCombined.xShare.i;
460-
const signerTwoIndex = backupKeyCombined.xShare.i;
461-
462-
// Since this is a user <> backup signing, we will reuse the same range proof challenge
463-
rangeProofChallenge =
464-
rangeProofChallenge ?? EcdsaTypes.serializeNtildeWithProofs(await EcdsaRangeProof.generateNtilde(openSSLBytes));
465-
466-
const userToBackupPaillierChallenge = await EcdsaPaillierProof.generateP(
467-
hexToBigInt(userKeyCombined.yShares[signerTwoIndex].n)
468-
);
469-
const backupToUserPaillierChallenge = await EcdsaPaillierProof.generateP(
470-
hexToBigInt(backupKeyCombined.yShares[signerOneIndex].n)
471-
);
472-
473-
const userXShare = MPC.appendChallenge(
474-
userKeyCombined.xShare,
475-
rangeProofChallenge,
476-
EcdsaTypes.serializePaillierChallenge({ p: userToBackupPaillierChallenge })
477-
);
478-
const userYShare = MPC.appendChallenge(
479-
userKeyCombined.yShares[signerTwoIndex],
480-
rangeProofChallenge,
481-
EcdsaTypes.serializePaillierChallenge({ p: backupToUserPaillierChallenge })
482-
);
483-
const backupXShare = MPC.appendChallenge(
484-
backupKeyCombined.xShare,
485-
rangeProofChallenge,
486-
EcdsaTypes.serializePaillierChallenge({ p: backupToUserPaillierChallenge })
487-
);
488-
const backupYShare = MPC.appendChallenge(
489-
backupKeyCombined.yShares[signerOneIndex],
490-
rangeProofChallenge,
491-
EcdsaTypes.serializePaillierChallenge({ p: userToBackupPaillierChallenge })
492-
);
493-
494-
const signShares: ECDSA.SignShareRT = await MPC.signShare(userXShare, userYShare);
495-
496-
const signConvertS21 = await MPC.signConvertStep1({
497-
xShare: backupXShare,
498-
yShare: backupYShare, // YShare corresponding to the other participant signerOne
499-
kShare: signShares.kShare,
500-
});
501-
const signConvertS12 = await MPC.signConvertStep2({
502-
aShare: signConvertS21.aShare,
503-
wShare: signShares.wShare,
504-
});
505-
const signConvertS21_2 = await MPC.signConvertStep3({
506-
muShare: signConvertS12.muShare,
507-
bShare: signConvertS21.bShare,
508-
});
509-
510-
const [signCombineOne, signCombineTwo] = [
511-
MPC.signCombine({
512-
gShare: signConvertS12.gShare,
513-
signIndex: {
514-
i: signConvertS12.muShare.i,
515-
j: signConvertS12.muShare.j,
516-
},
517-
}),
518-
MPC.signCombine({
519-
gShare: signConvertS21_2.gShare,
520-
signIndex: {
521-
i: signConvertS21_2.signIndex.i,
522-
j: signConvertS21_2.signIndex.j,
523-
},
524-
}),
525-
];
526-
527-
const MESSAGE = Buffer.from(txHex, 'hex');
528-
529-
const [signA, signB] = [
530-
MPC.sign(MESSAGE, signCombineOne.oShare, signCombineTwo.dShare, this.getHashFunction()),
531-
MPC.sign(MESSAGE, signCombineTwo.oShare, signCombineOne.dShare, this.getHashFunction()),
532-
];
533-
534-
return MPC.constructSignature([signA, signB]);
535-
}
536-
537324
/** @inheritDoc **/
538325
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
539326
let totalAmount = new BigNumber(0);

modules/abstract-cosmos/src/lib/iface.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export interface SendMessage {
3030
export interface RecoveryOptions {
3131
userKey?: string; // Box A
3232
backupKey?: string; // Box B
33-
bitgoKey: string; // Box C
3433
recoveryDestination: string;
3534
krsProvider?: string;
3635
walletPassphrase?: string;

modules/abstract-eth/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
},
4242
"dependencies": {
4343
"@bitgo/sdk-core": "^28.12.0",
44-
"@bitgo/sdk-lib-mpc": "^10.1.0",
4544
"@bitgo/statics": "^50.5.0",
4645
"@bitgo/utxo-lib": "^11.0.1",
4746
"@ethereumjs/common": "^2.6.5",

0 commit comments

Comments
 (0)