From 195ecdf2ac6fde20bfb18e49ba49f41c391dfba9 Mon Sep 17 00:00:00 2001 From: Bahaa Desoky Date: Mon, 1 Jun 2026 16:02:26 -0400 Subject: [PATCH] feat: use decryptAsync/encryptAsync for remaining WRW flows Ticket: WCN-172 --- modules/abstract-cosmos/src/cosmosCoin.ts | 5 +- .../src/abstractEthLikeNewCoins.ts | 3 +- .../src/recovery/backupKeyRecovery.ts | 8 +- .../src/recovery/crossChainRecovery.ts | 4 +- modules/sdk-api/src/v1/wallet.ts | 27 ++++-- modules/sdk-coin-eos/src/eos.ts | 4 +- modules/sdk-coin-icp/src/icp.ts | 3 +- modules/sdk-coin-rune/src/rune.ts | 3 +- modules/sdk-coin-stx/src/stx.ts | 4 +- modules/sdk-coin-tempo/src/tempo.ts | 3 +- modules/sdk-coin-trx/src/trx.ts | 6 +- modules/sdk-coin-vet/src/vet.ts | 6 +- modules/sdk-coin-xrp/src/xrp.ts | 4 +- modules/sdk-coin-xrp/test/resources/xrp.ts | 14 +-- .../src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts | 87 +++++++++++++++---- .../test/unit/bitgo/utils/tss/baseTSSUtils.ts | 3 + .../unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts | 16 +++- 17 files changed, 140 insertions(+), 60 deletions(-) diff --git a/modules/abstract-cosmos/src/cosmosCoin.ts b/modules/abstract-cosmos/src/cosmosCoin.ts index 7d6ac6541e..cd7ef8de6c 100644 --- a/modules/abstract-cosmos/src/cosmosCoin.ts +++ b/modules/abstract-cosmos/src/cosmosCoin.ts @@ -243,7 +243,7 @@ export class CosmosCoin extends BaseCoin { throw new Error('Invalid key format'); } - return await ECDSAUtils.getMpcV2RecoveryKeyShares(userKey, backupKey, walletPassphrase); + return await ECDSAUtils.getMpcV2RecoveryKeyShares(userKey, backupKey, walletPassphrase, this.bitgo); } /** @@ -491,7 +491,8 @@ export class CosmosCoin extends BaseCoin { const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( userKey, backupKey, - params.walletPassphrase + params.walletPassphrase, + this.bitgo ); // baseAddress is not extracted const MPC = new Ecdsa(); diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index 4cb72147f8..77e7e12124 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -2250,7 +2250,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( userPublicOrPrivateKeyShare, backupPrivateOrPublicKeyShare, - params.walletPassphrase + params.walletPassphrase, + this.bitgo ); const { gasLimit, gasPrice } = await this.getGasValues(params); diff --git a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts index 8c902d1401..3cf4c50052 100644 --- a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts @@ -3,7 +3,7 @@ import { BitGoBase, ErrorNoInputToRecover, getKrsProvider, - getBip32Keys as getBip32KeysFromSdkCore, + getBip32KeysAsync as getBip32KeysFromSdkCore, isTriple, krsProviders, Triple, @@ -410,8 +410,8 @@ export function formatBackupKeyRecoveryResult( return txInfo; } -function getBip32Keys(bitgo: BitGoBase, params: RecoverParams): Triple { - const keys = getBip32KeysFromSdkCore(bitgo, params, { requireBitGoXpub: true }); +async function getBip32Keys(bitgo: BitGoBase, params: RecoverParams): Promise> { + const keys = await getBip32KeysFromSdkCore(bitgo, params, { requireBitGoXpub: true }); if (!isTriple(keys)) { throw new Error(`expected key triple`); } @@ -469,7 +469,7 @@ export async function backupKeyRecovery( } // check whether key material and password authenticate the users and return parent keys of all three keys of the wallet - const keys = getBip32Keys(bitgo, params); + const keys = await getBip32Keys(bitgo, params); const walletKeys = fixedScriptWallet.RootWalletKeys.from({ triple: keys, derivationPrefixes: [params.userKeyPath || 'm/0/0', 'm/0/0', 'm/0/0'], diff --git a/modules/abstract-utxo/src/recovery/crossChainRecovery.ts b/modules/abstract-utxo/src/recovery/crossChainRecovery.ts index eb27a2423d..dc6c5b3a3f 100644 --- a/modules/abstract-utxo/src/recovery/crossChainRecovery.ts +++ b/modules/abstract-utxo/src/recovery/crossChainRecovery.ts @@ -1,6 +1,6 @@ import { BIP32, CoinName, fixedScriptWallet, address as wasmAddress } from '@bitgo/wasm-utxo'; +import { decryptAsync } from '@bitgo/sdk-api'; import { BitGoBase, IWallet, Keychain, Triple, Wallet } from '@bitgo/sdk-core'; -import { decrypt } from '@bitgo/sdk-api'; import { AbstractUtxoCoin, TransactionInfo } from '../abstractUtxoCoin'; import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction'; @@ -313,7 +313,7 @@ async function getPrv(xprv?: string, passphrase?: string, wallet?: IWallet | Wal encryptedPrv = (await (wallet as WalletV1).getEncryptedUserKeychain()).encryptedXprv; } - return getPrv(decrypt(passphrase, encryptedPrv)); + return getPrv(await decryptAsync(passphrase, encryptedPrv)); } /** diff --git a/modules/sdk-api/src/v1/wallet.ts b/modules/sdk-api/src/v1/wallet.ts index b06f431377..6c98a22fc8 100644 --- a/modules/sdk-api/src/v1/wallet.ts +++ b/modules/sdk-api/src/v1/wallet.ts @@ -1751,7 +1751,10 @@ Wallet.prototype.createAndSignTransaction = function (params, callback) { const safeUserKey = _.get(this.wallet, 'private.userPrivKey'); if (_.isString(safeUserKey) && _.isString(params.walletPassphrase)) { // @ts-expect-error - no implicit this - transaction.signingKey = this.bitgo.decrypt({ password: params.walletPassphrase, input: safeUserKey }); + transaction.signingKey = await this.bitgo.decryptAsync({ + password: params.walletPassphrase, + input: safeUserKey, + }); } else { throw e; } @@ -1809,10 +1812,13 @@ Wallet.prototype.getAndPrepareSigningKeychain = function (params, callback) { // Caller provided a wallet passphrase if (params.walletPassphrase) { - return self.getEncryptedUserKeychain().then(function (keychain) { + return self.getEncryptedUserKeychain().then(async function (keychain) { // Decrypt the user key with a passphrase try { - keychain.xprv = self.bitgo.decrypt({ password: params.walletPassphrase, input: keychain.encryptedXprv }); + keychain.xprv = await self.bitgo.decryptAsync({ + password: params.walletPassphrase, + input: keychain.encryptedXprv, + }); } catch (e) { throw new Error('Unable to decrypt user keychain'); } @@ -2295,21 +2301,24 @@ Wallet.prototype.shareWallet = function (params, callback) { sharing = result; if (needsKeychain) { - return self.getEncryptedUserKeychain({}).then(function (keychain) { + return self.getEncryptedUserKeychain({}).then(async function (keychain) { // Decrypt the user key with a passphrase if (keychain.encryptedXprv) { if (!params.walletPassphrase) { throw new Error('Missing walletPassphrase argument'); } try { - keychain.xprv = self.bitgo.decrypt({ password: params.walletPassphrase, input: keychain.encryptedXprv }); + keychain.xprv = await self.bitgo.decryptAsync({ + password: params.walletPassphrase, + input: keychain.encryptedXprv, + }); } catch (e) { throw new Error('Unable to decrypt user keychain'); } const eckey = makeRandomKey(); const secret = getSharedSecret(eckey, Buffer.from(sharing.pubkey, 'hex')).toString('hex'); - const newEncryptedXprv = self.bitgo.encrypt({ password: secret, input: keychain.xprv }); + const newEncryptedXprv = await self.bitgo.encryptAsync({ password: secret, input: keychain.xprv }); sharedKeychain = { xpub: keychain.xpub, @@ -2599,10 +2608,10 @@ Wallet.prototype.recover = async function (params) { assert(parsedUnsignedTx.outs.length === 1); assert(_.sumBy(params.unspents, 'value') - _.sumBy(parsedUnsignedTx.outs, 'value') === Number(approximateTxFee)); - const plainUserKey = this.bitgo.decrypt({ password: params.walletPassphrase, input: params.userKey }); + const plainUserKey = await this.bitgo.decryptAsync({ password: params.walletPassphrase, input: params.userKey }); const halfSignedTx = await this.signTransaction({ ...unsignedTx, signingKey: plainUserKey }); - const plainBackupKey = this.bitgo.decrypt({ password: params.walletPassphrase, input: params.backupKey }); + const plainBackupKey = await this.bitgo.decryptAsync({ password: params.walletPassphrase, input: params.backupKey }); const fullSignedTx = await this.signTransaction({ ...unsignedTx, transactionHex: halfSignedTx.tx, @@ -2659,7 +2668,7 @@ Wallet.prototype.sweep = async function (params) { assert(parsedUnsignedTx.outs.length === 1); assert(_.sumBy(params.unspents, 'value') - _.sumBy(parsedUnsignedTx.outs, 'value') === Number(approximateTxFee)); - const plainUserKey = this.bitgo.decrypt({ password: params.walletPassphrase, input: params.userKey }); + const plainUserKey = await this.bitgo.decryptAsync({ password: params.walletPassphrase, input: params.userKey }); const halfSignedTx = await this.signTransaction({ ...unsignedTx, signingKey: plainUserKey }); return await this.sendTransaction({ diff --git a/modules/sdk-coin-eos/src/eos.ts b/modules/sdk-coin-eos/src/eos.ts index d364b727ef..33d8a459c6 100644 --- a/modules/sdk-coin-eos/src/eos.ts +++ b/modules/sdk-coin-eos/src/eos.ts @@ -20,7 +20,7 @@ import { BitGoBase, checkKrsProvider, Environments, - getBip32Keys, + getBip32KeysAsync, getIsKrsRecovery, getIsUnsignedSweep, HalfSignedAccountTransaction as BaseHalfSignedTransaction, @@ -905,7 +905,7 @@ export class Eos extends BaseCoin { throw new Error('Invalid destination address!'); } - const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false }); + const keys = await getBip32KeysAsync(this.bitgo, params, { requireBitGoXpub: false }); const rootAddressDetails = this.getAddressDetails(params.rootAddress); const account = await this.getAccountFromNode({ address: rootAddressDetails.address }); diff --git a/modules/sdk-coin-icp/src/icp.ts b/modules/sdk-coin-icp/src/icp.ts index bbf403d0cf..75b8303d62 100644 --- a/modules/sdk-coin-icp/src/icp.ts +++ b/modules/sdk-coin-icp/src/icp.ts @@ -409,7 +409,8 @@ export class Icp extends BaseCoin { ({ userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( userKey, backupKey, - params.walletPassphrase + params.walletPassphrase, + this.bitgo )); publicKey = MPC.deriveUnhardened(commonKeyChain, ROOT_PATH).slice(0, 66); } else { diff --git a/modules/sdk-coin-rune/src/rune.ts b/modules/sdk-coin-rune/src/rune.ts index 682d32b84d..fcd2fb0aa0 100644 --- a/modules/sdk-coin-rune/src/rune.ts +++ b/modules/sdk-coin-rune/src/rune.ts @@ -167,7 +167,8 @@ export class Rune extends CosmosCoin { const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( userKey, backupKey, - params.walletPassphrase + params.walletPassphrase, + this.bitgo ); // baseAddress is not extracted // Step 3: Instantiate the ECDSA signer and fetch the address details const MPC = new Ecdsa(); diff --git a/modules/sdk-coin-stx/src/stx.ts b/modules/sdk-coin-stx/src/stx.ts index b9e1c537b0..622050447f 100644 --- a/modules/sdk-coin-stx/src/stx.ts +++ b/modules/sdk-coin-stx/src/stx.ts @@ -4,7 +4,7 @@ import { BaseTransaction, BitGoBase, Environments, - getBip32Keys, + getBip32KeysAsync, getIsUnsignedSweep, KeyPair, MethodNotImplementedError, @@ -701,7 +701,7 @@ export class Stx extends BaseCoin { } } const isUnsignedSweep = getIsUnsignedSweep(params); - const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: true }); + const keys = await getBip32KeysAsync(this.bitgo, params, { requireBitGoXpub: true }); const rootAddressDetails = getAddressDetails(params.rootAddress); const [accountBalanceData, accountNonceData] = await Promise.all([ this.getNativeStxBalanceFromNode({ address: rootAddressDetails.address }), diff --git a/modules/sdk-coin-tempo/src/tempo.ts b/modules/sdk-coin-tempo/src/tempo.ts index 685163a93b..f7fddb897a 100644 --- a/modules/sdk-coin-tempo/src/tempo.ts +++ b/modules/sdk-coin-tempo/src/tempo.ts @@ -492,7 +492,8 @@ export class Tempo extends AbstractEthLikeNewCoins { const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( userKey, backupKey, - params.walletPassphrase + params.walletPassphrase, + this.bitgo ); const MPC = new Ecdsa(); diff --git a/modules/sdk-coin-trx/src/trx.ts b/modules/sdk-coin-trx/src/trx.ts index e68383c1a7..e3bc919b3a 100644 --- a/modules/sdk-coin-trx/src/trx.ts +++ b/modules/sdk-coin-trx/src/trx.ts @@ -10,7 +10,7 @@ import { BaseCoin, BitGoBase, common, - getBip32Keys, + getBip32KeysAsync, getIsKrsRecovery, getIsUnsignedSweep, KeyPair, @@ -848,7 +848,7 @@ export class Trx extends BaseCoin { } // get our user, backup keys - const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false }); + const keys = await getBip32KeysAsync(this.bitgo, params, { requireBitGoXpub: false }); // we need to decode our bitgoKey to a base58 address const bitgoHexAddr = this.pubToHexAddress(this.xpubToUncompressedPub(params.bitgoKey)); @@ -1032,7 +1032,7 @@ export class Trx extends BaseCoin { ); } - const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false }); + const keys = await getBip32KeysAsync(this.bitgo, params, { requireBitGoXpub: false }); const baseAddrHex = this.pubToHexAddress(this.xpubToUncompressedPub(params.bitgoKey)); const txnsBatch: RecoveryTransaction[] = []; diff --git a/modules/sdk-coin-vet/src/vet.ts b/modules/sdk-coin-vet/src/vet.ts index 245b12e967..a7c1a71312 100644 --- a/modules/sdk-coin-vet/src/vet.ts +++ b/modules/sdk-coin-vet/src/vet.ts @@ -415,7 +415,8 @@ export class Vet extends BaseCoin { ({ userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( userKey, backupKey, - params.walletPassphrase + params.walletPassphrase, + this.bitgo )); publicKey = MPC.deriveUnhardened(commonKeyChain, 'm/0').slice(0, 66); } @@ -763,7 +764,8 @@ export class Vet extends BaseCoin { ({ userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( userKey, backupKey, - params.walletPassphrase + params.walletPassphrase, + this.bitgo )); publicKey = MPC.deriveUnhardened(commonKeyChain, 'm/0').slice(0, 66); } diff --git a/modules/sdk-coin-xrp/src/xrp.ts b/modules/sdk-coin-xrp/src/xrp.ts index 5eab9194a4..e88cf802d5 100644 --- a/modules/sdk-coin-xrp/src/xrp.ts +++ b/modules/sdk-coin-xrp/src/xrp.ts @@ -11,7 +11,7 @@ import { BaseCoin, BitGoBase, checkKrsProvider, - getBip32Keys, + getBip32KeysAsync, InvalidAddressError, KeyPair, MethodNotImplementedError, @@ -552,7 +552,7 @@ export class Xrp extends BaseCoin { throw new Error('Invalid destination address!'); } - const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false }); + const keys = await getBip32KeysAsync(this.bitgo, params, { requireBitGoXpub: false }); const { addressDetails, feeDetails, serverDetails, accountLines } = await promiseProps({ addressDetails: this.bitgo.post(rippledUrl).send(accountInfoParams), diff --git a/modules/sdk-coin-xrp/test/resources/xrp.ts b/modules/sdk-coin-xrp/test/resources/xrp.ts index 821bb766a8..bbe1545457 100644 --- a/modules/sdk-coin-xrp/test/resources/xrp.ts +++ b/modules/sdk-coin-xrp/test/resources/xrp.ts @@ -81,17 +81,17 @@ export const TEST_TOKEN_TRANSFER_TX = { export const keys = { userKey: - '{"iv":"ZN/gBap8QYIpjbbkZCDY8g==","v":1,"iter":10000,"ks":256,"ts":64,"mode"\n' + - ':"ccm","adata":"","cipher":"aes","salt":"Egt/IC14ugw=","ct":"eRiONKtGrlEX8e\n' + - '5s5EAon7MadWZxyQWJMFJp16rimEd/2LWyGObo/d6hdJWUSZE1lDzpYV9x/Qg3vKz8Wy4ee8R0h\n' + + '{"iv":"ZN/gBap8QYIpjbbkZCDY8g==","v":1,"iter":10000,"ks":256,"ts":64,"mode"' + + ':"ccm","adata":"","cipher":"aes","salt":"Egt/IC14ugw=","ct":"eRiONKtGrlEX8e' + + '5s5EAon7MadWZxyQWJMFJp16rimEd/2LWyGObo/d6hdJWUSZE1lDzpYV9x/Qg3vKz8Wy4ee8R0h' + '8J+Ddo/Q8dR/yDNImcNGBclBMrh9c8cowuzRMnbMlbrLc949tN3d3A1jXOu3Rr5Wt4h1ag="}', backupKey: - '{"iv":"D5SCw343R+l9qbP3TrXzlg==","v":1,"iter":10000,"ks":256,"ts":64,"mode"\n' + - ':"ccm","adata":"","cipher":"aes","salt":"yug6WjWDjCA=","ct":"++m1LyBWw9emM2\n' + - 'J1P85+T2VJEFPXFjshWssVBaHuccsiD0MsYsFX5d+hVfDrWV2aDOJAuOdtoCo+R3LrG2JST80ru\n' + + '{"iv":"D5SCw343R+l9qbP3TrXzlg==","v":1,"iter":10000,"ks":256,"ts":64,"mode"' + + ':"ccm","adata":"","cipher":"aes","salt":"yug6WjWDjCA=","ct":"++m1LyBWw9emM2' + + 'J1P85+T2VJEFPXFjshWssVBaHuccsiD0MsYsFX5d+hVfDrWV2aDOJAuOdtoCo+R3LrG2JST80ru' + '37Y383IvRlB3A85MSo/poMtN1JyzorwF6Cfiz26bY3OKxywaeWJvr9SEDJxTDTx8HH9GsE="}', bitgoKey: - 'xpub661MyMwAqRbcGBXTTnaLrqur67ZHc9BA9X3GdAx6Kj8HVyg32TvktXv8DPN13QvnWSnrfC8\n' + + 'xpub661MyMwAqRbcGBXTTnaLrqur67ZHc9BA9X3GdAx6Kj8HVyg32TvktXv8DPN13QvnWSnrfC8' + 'KFWvaUfR4kfwyikf6TuyJ3Ei8HGs7vxfdyia', rootAddress: 'rNTfZB1h4TDdF9QXw37nbWk9euZmRby4qn', }; diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index 006cb8f775..a10e2b0aac 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -54,6 +54,7 @@ import { BaseEcdsaUtils } from './base'; import { EcdsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './ecdsaMPCv2KeyGenSender'; import { envRequiresBitgoPubGpgKeyConfig, isBitgoMpcPubKey } from '../../../tss/bitgoPubKeys'; import { InvalidTransactionError } from '../../../errors'; +import { BitGoBase } from '../../../bitgoBase'; export class EcdsaMPCv2Utils extends BaseEcdsaUtils { private static readonly DKLS23_SIGNING_USER_GPG_KEY = 'DKLS23_SIGNING_USER_GPG_KEY'; @@ -396,7 +397,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(reducedPrivateMaterial)))) ); } else { - encryptedPrv = this.bitgo.encrypt({ + encryptedPrv = await this.bitgo.encryptAsync({ input: privateMaterialBase64, password: passphrase, }); @@ -404,7 +405,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { // scalar s_i) with the wallet passphrase. The result is stored as reducedEncryptedPrv // on the key card QR code and represents a second copy of private key material // beyond the server-stored encryptedPrv. - reducedEncryptedPrv = this.bitgo.encrypt({ + reducedEncryptedPrv = await this.bitgo.encryptAsync({ // Buffer.toString('base64') can not be used here as it does not work on the browser. // The browser deals with a Buffer as Uint8Array, therefore in the browser .toString('base64') just creates a comma seperated string of the array values. input: btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(reducedPrivateMaterial)))), @@ -1209,12 +1210,12 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { } } - const encryptedRound1Session = this.bitgo.encrypt({ + const encryptedRound1Session = await this.bitgo.encryptAsync({ input: sessionData, password: walletPassphrase, adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_ROUND1_STATE}:${adata}`, }); - const encryptedUserGpgPrvKey = this.bitgo.encrypt({ + const encryptedUserGpgPrvKey = await this.bitgo.encryptAsync({ input: userGpgKey.privateKey, password: walletPassphrase, adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_USER_GPG_KEY}:${adata}`, @@ -1310,7 +1311,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { } } - const encryptedRound2Session = this.bitgo.encrypt({ + const encryptedRound2Session = await this.bitgo.encryptAsync({ input: sessionData, password: walletPassphrase, adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_ROUND2_STATE}:${adata}`, @@ -1397,6 +1398,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { } /** + * TODO: deprecate this function in favor of isGG18SigningMaterialAsync once v2 encryption is default * Checks if the given key share, when decrypted, contains valid GG18 signing material. * * @param {string} keyShare - The encrypted key share string. @@ -1417,22 +1419,51 @@ export function isGG18SigningMaterial(keyShare: string, walletPassphrase: string } } +/** + * Async version of {@link isGG18SigningMaterial} with v1/v2 auto-detect decrypt via `bitgo.decryptAsync`. + */ +export async function isGG18SigningMaterialAsync( + keyShare: string, + walletPassphrase: string | undefined, + bitgo: BitGoBase +): Promise { + try { + const prv = await bitgo.decryptAsync({ password: walletPassphrase, input: keyShare }); + const signingMaterial = JSON.parse(prv); + return ( + signingMaterial.pShare && + signingMaterial.bitgoNShare && + (signingMaterial.userNShare || signingMaterial.backupNShare) + ); + } catch (error) { + return false; + } +} + /** * Get the MPC v2 recovery key shares from the provided user and backup key shares. * @param encryptedUserKey encrypted gg18 or MPCv2 user key * @param encryptedBackupKey encrypted gg18 or MPCv2 backup key * @param walletPassphrase password for user and backup key + * @param bitgo BitGo instance for v1/v2 auto-detect decrypt * @returns MPC v2 recovery key shares */ export async function getMpcV2RecoveryKeyShares( encryptedUserKey: string, encryptedBackupKey: string, - walletPassphrase?: string + walletPassphrase?: string, + bitgo?: BitGoBase ): Promise<{ userKeyShare: Buffer; backupKeyShare: Buffer; commonKeyChain: string; }> { + if (bitgo) { + if (await isGG18SigningMaterialAsync(encryptedUserKey, walletPassphrase, bitgo)) { + return getMpcV2RecoveryKeySharesFromGG18(encryptedUserKey, encryptedBackupKey, walletPassphrase, bitgo); + } + return getMpcV2RecoveryKeySharesFromReducedKey(encryptedUserKey, encryptedBackupKey, walletPassphrase, bitgo); + } if (isGG18SigningMaterial(encryptedUserKey, walletPassphrase)) { return getMpcV2RecoveryKeySharesFromGG18(encryptedUserKey, encryptedBackupKey, walletPassphrase); } @@ -1494,16 +1525,18 @@ export async function signRecoveryMpcV2( async function getMpcV2RecoveryKeySharesFromGG18( encryptedGG18UserKey: string, encryptedGG18BackupKey: string, - walletPassphrase?: string + walletPassphrase?: string, + bitgo?: BitGoBase ): Promise<{ userKeyShare: Buffer; backupKeyShare: Buffer; commonKeyChain: string; }> { - const [userKeyCombined, backupKeyCombined] = getKeyCombinedFromTssKeyShares( + const [userKeyCombined, backupKeyCombined] = await getKeyCombinedFromTssKeyShares( encryptedGG18UserKey, encryptedGG18BackupKey, - walletPassphrase + walletPassphrase, + bitgo ); const retrofitDataA: DklsTypes.RetrofitData = { xShare: userKeyCombined.xShare, @@ -1535,14 +1568,28 @@ async function getMpcV2RecoveryKeySharesFromGG18( async function getMpcV2RecoveryKeySharesFromReducedKey( encryptedMPCv2UserKey: string, encryptedMPCv2BackupKey: string, - walletPassphrase?: string + walletPassphrase?: string, + bitgo?: BitGoBase ): Promise<{ userKeyShare: Buffer; backupKeyShare: Buffer; commonKeyChain: string; }> { - const userCompressedPrv = Buffer.from(sjcl.decrypt(walletPassphrase, encryptedMPCv2UserKey), 'base64'); - const bakcupCompressedPrv = Buffer.from(sjcl.decrypt(walletPassphrase, encryptedMPCv2BackupKey), 'base64'); + let userCompressedPrv: Buffer; + let bakcupCompressedPrv: Buffer; + if (bitgo) { + userCompressedPrv = Buffer.from( + await bitgo.decryptAsync({ password: walletPassphrase, input: encryptedMPCv2UserKey }), + 'base64' + ); + bakcupCompressedPrv = Buffer.from( + await bitgo.decryptAsync({ password: walletPassphrase, input: encryptedMPCv2BackupKey }), + 'base64' + ); + } else { + userCompressedPrv = Buffer.from(sjcl.decrypt(walletPassphrase, encryptedMPCv2UserKey), 'base64'); + bakcupCompressedPrv = Buffer.from(sjcl.decrypt(walletPassphrase, encryptedMPCv2BackupKey), 'base64'); + } const userPrvJSON: DklsTypes.ReducedKeyShare = DklsTypes.getDecodedReducedKeyShare(userCompressedPrv); const backupPrvJSON: DklsTypes.ReducedKeyShare = DklsTypes.getDecodedReducedKeyShare(bakcupCompressedPrv); @@ -1576,16 +1623,22 @@ async function getMpcV2RecoveryKeySharesFromReducedKey( * @param walletPassphrase wallet passphrase * @returns key shares */ -function getKeyCombinedFromTssKeyShares( +async function getKeyCombinedFromTssKeyShares( encryptedGG18UserKey: string, encryptedGG18BackupKey: string, - walletPassphrase?: string -): [ECDSAMethodTypes.KeyCombined, ECDSAMethodTypes.KeyCombined] { + walletPassphrase?: string, + bitgo?: BitGoBase +): Promise<[ECDSAMethodTypes.KeyCombined, ECDSAMethodTypes.KeyCombined]> { let backupPrv; let userPrv; try { - backupPrv = sjcl.decrypt(walletPassphrase, encryptedGG18BackupKey); - userPrv = sjcl.decrypt(walletPassphrase, encryptedGG18UserKey); + if (bitgo) { + backupPrv = await bitgo.decryptAsync({ password: walletPassphrase, input: encryptedGG18BackupKey }); + userPrv = await bitgo.decryptAsync({ password: walletPassphrase, input: encryptedGG18UserKey }); + } else { + backupPrv = sjcl.decrypt(walletPassphrase, encryptedGG18BackupKey); + userPrv = sjcl.decrypt(walletPassphrase, encryptedGG18UserKey); + } } catch (e) { throw new Error(`Error decrypting backup keychain: ${e.message}`); } diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/baseTSSUtils.ts index 597ea4f7cc..3424737c38 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/baseTSSUtils.ts @@ -89,6 +89,9 @@ describe('Base TSS Utils', function () { mockBg.encrypt = sinon .stub() .callsFake((params) => encryptWithSjcl(params.password ?? '', params.input, params.adata)); + mockBg.encryptAsync = sinon + .stub() + .callsFake(async (params) => encryptWithSjcl(params.password ?? '', params.input, params.adata)); mockBg.decrypt = sinon.stub().callsFake((params) => sjcl.decrypt(params.password ?? '', params.input)); mockBg.decryptAsync = decryptAsyncStub; mockBitgo = mockBg; diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index e3f3b13d73..b9269d3d22 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -41,7 +41,7 @@ describe('ECDSA MPC v2', async () => { before('initialize EcdsaMPCv2Utils', async () => { const mockBg = {} as BitGoBase; mockBg.getEnv = sinon.stub().returns('test'); - mockBg.encrypt = sinon.stub().callsFake((params) => { + const encryptImpl = (params: { password: string; input: string; adata?: string }) => { const salt = randomBytes(8); const iv = randomBytes(16); return sjcl.encrypt(params.password, params.input, { @@ -54,10 +54,14 @@ describe('ECDSA MPC v2', async () => { ], adata: params.adata, }); - }); - mockBg.decrypt = sinon.stub().callsFake((params) => { + }; + const decryptImpl = (params: { password: string; input: string }) => { return sjcl.decrypt(params.password, params.input); - }); + }; + mockBg.encrypt = sinon.stub().callsFake(encryptImpl); + mockBg.encryptAsync = sinon.stub().callsFake(async (params) => encryptImpl(params)); + mockBg.decrypt = sinon.stub().callsFake(decryptImpl); + mockBg.decryptAsync = sinon.stub().callsFake(async (params) => decryptImpl(params)); const mockCoin = {} as IBaseCoin; mockCoin.getHashFunction = sinon.stub().callsFake(() => createKeccakHash('keccak256') as Hash); @@ -611,7 +615,9 @@ describe('ECDSA MPC v2', async () => { mockBgWithPost.getEnv = sinon.stub().returns('test'); mockBgWithPost.setRequestTracer = sinon.stub(); mockBgWithPost.encrypt = sinon.stub().returns('encrypted'); + mockBgWithPost.encryptAsync = sinon.stub().resolves('encrypted'); mockBgWithPost.decrypt = sinon.stub().returns('decrypted'); + mockBgWithPost.decryptAsync = sinon.stub().resolves('decrypted'); mockBgWithPost.post = sinon.stub().returns({ send: sinon.stub().returnsThis(), set: sinon.stub().returnsThis(), @@ -677,7 +683,9 @@ describe('ECDSA MPC v2', async () => { mockBgWithPost.getEnv = sinon.stub().returns('test'); mockBgWithPost.setRequestTracer = sinon.stub(); mockBgWithPost.encrypt = sinon.stub().returns('encrypted'); + mockBgWithPost.encryptAsync = sinon.stub().resolves('encrypted'); mockBgWithPost.decrypt = sinon.stub().returns('decrypted'); + mockBgWithPost.decryptAsync = sinon.stub().resolves('decrypted'); mockBgWithPost.post = sinon.stub().returns({ send: sinon.stub().returnsThis(), set: sinon.stub().returnsThis(),