diff --git a/modules/sdk-coin-sui/src/sui.ts b/modules/sdk-coin-sui/src/sui.ts index 00d5554666..943220237f 100644 --- a/modules/sdk-coin-sui/src/sui.ts +++ b/modules/sdk-coin-sui/src/sui.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import crypto from 'crypto'; import { BaseBroadcastTransactionOptions, @@ -5,9 +6,12 @@ import { BaseCoin, BaseTransaction, BitGoBase, + decryptKeychainPrivateKey, EDDSAMethods, EDDSAMethodTypes, + EddsaSigningMaterial, Environments, + getEddsaSigningMaterial as sharedGetEddsaSigningMaterial, KeyPair, MPCAlgorithm, MPCRecoveryOptions, @@ -20,6 +24,7 @@ import { ParsedTransaction, ParseTransactionOptions as BaseParseTransactionOptions, RecoveryTxRequest, + signEddsaMpcV2RecoveryTx, SignedTransaction, SignTransactionOptions, TransactionExplanation, @@ -463,7 +468,7 @@ export class Sui extends BaseCoin { return this.buildUnsignedSweepTransaction(txBuilder, senderAddress, bitgoKey, idx, derivationPath); } - await this.signRecoveryTransaction(txBuilder, params, derivationPath, derivedPublicKey, false); + await this.signRecoveryTransaction(txBuilder, params, derivationPath, derivedPublicKey, bitgoKey, false); const tx = (await txBuilder.build()) as TransferTransaction; return { transactions: [ @@ -554,7 +559,7 @@ export class Sui extends BaseCoin { return this.buildUnsignedSweepTransaction(txBuilder, senderAddress, bitgoKey, idx, derivationPath, token); } - await this.signRecoveryTransaction(txBuilder, params, derivationPath, derivedPublicKey, true); + await this.signRecoveryTransaction(txBuilder, params, derivationPath, derivedPublicKey, bitgoKey, true); const tx = (await txBuilder.build()) as TokenTransferTransaction; return { transactions: [ @@ -630,55 +635,64 @@ export class Sui extends BaseCoin { return { txRequests: [txRequest] }; } + /** + * Detects whether a keycard's decrypted plaintext is MPCv1 JSON or MPCv2 CBOR. + * Protected so tests can stub it via sinon; wraps the shared sdk-core helper. + */ + protected async getEddsaSigningMaterial(userKey: string, walletPassphrase: string): Promise { + return sharedGetEddsaSigningMaterial(userKey, walletPassphrase, this.bitgo); + } + + // Protected so tests can stub via instance overrides — direct module function bindings + // cannot be intercepted by sinon after import. + protected async signSuiMpcV2Recovery(params: Parameters[0]): Promise { + return signEddsaMpcV2RecoveryTx(params); + } + private async signRecoveryTransaction( txBuilder: TransactionBuilder, params: MPCRecoveryOptions, derivationPath: string, derivedPublicKey: string, + bitgoKey: string, isTokenTransaction: boolean ) { // TODO(BG-51092): This looks like a common part which can be extracted out too const unsignedTx = isTokenTransaction ? ((await txBuilder.build()) as TokenTransferTransaction) : ((await txBuilder.build()) as TransferTransaction); - if (!params.userKey) { - throw new Error('missing userKey'); - } - if (!params.backupKey) { - throw new Error('missing backupKey'); - } - if (!params.walletPassphrase) { - throw new Error('missing wallet passphrase'); - } + assert(params.userKey, 'missing userKey'); + assert(params.backupKey, 'missing backupKey'); + assert(params.walletPassphrase, 'missing wallet passphrase'); // Clean up whitespace from entered values const userKey = params.userKey.replace(/\s/g, ''); const backupKey = params.backupKey.replace(/\s/g, ''); - // Decrypt private keys from KeyCard values - let userPrv: string; - try { - userPrv = await this.bitgo.decrypt({ - input: userKey, - password: params.walletPassphrase, + const signingMaterial = await this.getEddsaSigningMaterial(userKey, params.walletPassphrase); + + if (signingMaterial.version === 'v2') { + const signature = await this.signSuiMpcV2Recovery({ + message: unsignedTx.signablePayload, + userKey: signingMaterial.encryptedUserKey, + backupKey, + walletPassphrase: params.walletPassphrase, + bitgoKey, + derivationPath, + bitgo: this.bitgo, }); - } catch (e) { - throw new Error(`Error decrypting user keychain: ${e.message}`); + txBuilder.addSignature({ pub: derivedPublicKey }, signature); + return; } + /** TODO BG-52419 Implement Codec for parsing */ - const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial; + const userSigningMaterial = JSON.parse(signingMaterial.userPrv) as EDDSAMethodTypes.UserSigningMaterial; - let backupPrv: string; - try { - backupPrv = await this.bitgo.decrypt({ - input: backupKey, - password: params.walletPassphrase, - }); - } catch (e) { - throw new Error(`Error decrypting backup keychain: ${e.message}`); + const backupPrv = await decryptKeychainPrivateKey(this.bitgo, { encryptedPrv: backupKey }, params.walletPassphrase); + if (!backupPrv) { + throw new Error('Error decrypting backup keychain: invalid password or corrupted key'); } const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial; - /* ********************** END ***********************************/ // add signature const signatureHex = await EDDSAMethods.getTSSSignature( diff --git a/modules/sdk-coin-sui/test/unit/sui.ts b/modules/sdk-coin-sui/test/unit/sui.ts index 8917af8a2e..e368ae96c7 100644 --- a/modules/sdk-coin-sui/test/unit/sui.ts +++ b/modules/sdk-coin-sui/test/unit/sui.ts @@ -1,19 +1,22 @@ import should from 'should'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; -import { BitGoAPI } from '@bitgo/sdk-api'; +import { BitGoAPI, encrypt } from '@bitgo/sdk-api'; import { Sui, TokenTransferTransaction, TransferTransaction, Tsui } from '../../src'; import * as testData from '../resources/sui'; import _ from 'lodash'; import sinon from 'sinon'; import BigNumber from 'bignumber.js'; import assert from 'assert'; +import nacl from 'tweetnacl'; import { SuiTransactionType } from '../../src/lib/iface'; import { getBuilderFactory } from './getBuilderFactory'; import { keys } from '../resources/sui'; import { Buffer } from 'buffer'; -import { common, TransactionPrebuild, Wallet } from '@bitgo/sdk-core'; +import { common, EDDSAMethods, TransactionPrebuild, Wallet } from '@bitgo/sdk-core'; +import { MPSUtil } from '@bitgo/sdk-lib-mpc'; import nock from 'nock'; +import utils from '../../src/lib/utils'; describe('SUI:', function () { let bitgo: TestBitGoAPI; @@ -728,6 +731,169 @@ describe('SUI:', function () { }); }); + describe('Recover Transactions (MPCv2):', () => { + const sandBox = sinon.createSandbox(); + const recoveryDestination = '0x00e4eaa6a291fe02918452e645b5653cd260a5fc0fb35f6193d580916aa9e389'; + const walletPassphrase = 'p$Sw { const sandBox = sinon.createSandbox(); const coinType = '0x36dbef866a1d62bf7328989a10fb2f07d769f4ee587c0de4a0a256e57e0a58a8::deep::DEEP'; @@ -1038,6 +1204,105 @@ describe('SUI:', function () { }); }); + describe('Recover Token Transactions (MPCv2):', () => { + const sandBox = sinon.createSandbox(); + const coinType = '0x36dbef866a1d62bf7328989a10fb2f07d769f4ee587c0de4a0a256e57e0a58a8::deep::DEEP'; + const recoveryDestination = '0x00e4eaa6a291fe02918452e645b5653cd260a5fc0fb35f6193d580916aa9e389'; + const walletPassphrase = 'p$Sw { const sandBox = sinon.createSandbox(); const senderAddress0 = '0x91f25e237b83a00a62724fdc4a81e43f494dc6b41a1241492826d36e4d131da3';