diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index 06ff2cc540..c21dc465bd 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -2011,7 +2011,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { const walletPassphrase = buildParams.walletPassphrase; const userKeychain = await this.keychains().get({ id: wallet.keyIds()[0] }); - const userPrv = wallet.getUserPrv({ keychain: userKeychain, walletPassphrase }); + const userPrv = await wallet.getUserPrv({ keychain: userKeychain, walletPassphrase }); const userPrvBuffer = bip32.fromBase58(userPrv).privateKey; if (!userPrvBuffer) { throw new Error('invalid userPrv'); diff --git a/modules/bitgo/test/v2/unit/wallet.ts b/modules/bitgo/test/v2/unit/wallet.ts index 528e36fa62..6dfe5758f9 100644 --- a/modules/bitgo/test/v2/unit/wallet.ts +++ b/modules/bitgo/test/v2/unit/wallet.ts @@ -324,7 +324,7 @@ describe('V2 Wallet:', function () { prv, coldDerivationSeed: '123', }; - wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); + await wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); }); it('should use the user keychain derivedFromParentWithSeed as the cold derivation seed if none is provided', async () => { @@ -337,7 +337,7 @@ describe('V2 Wallet:', function () { type: 'independent', }, }; - wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); + await wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); }); it('should prefer the explicit cold derivation seed to the user keychain derivedFromParentWithSeed', async () => { @@ -351,7 +351,7 @@ describe('V2 Wallet:', function () { type: 'independent', }, }; - wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); + await wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv); }); it('should return the prv provided for TSS SMC', async () => { @@ -379,7 +379,7 @@ describe('V2 Wallet:', function () { prv, keychain, }; - wallet.getUserPrv(userPrvOptions).should.eql(prv); + await wallet.getUserPrv(userPrvOptions).should.eql(prv); }); }); diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 62e96d9ef9..0054d8bfc8 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -495,7 +495,7 @@ export async function handleV2Sign(req: express.Request) { let privKey = decryptPrivKey(bitgo, encryptedPrivKey, walletPw); const coin = bitgo.coin(req.params.coin); if (req.body.derivationSeed) { - privKey = coin.deriveKeyWithSeed({ key: privKey, seed: req.body.derivationSeed }).key; + privKey = (await coin.deriveKeyWithSeed({ key: privKey, seed: req.body.derivationSeed })).key; } try { return await coin.signTransaction({ ...req.body, prv: privKey }); diff --git a/modules/sdk-coin-algo/src/algo.ts b/modules/sdk-coin-algo/src/algo.ts index 06c28268dc..ad536413cc 100644 --- a/modules/sdk-coin-algo/src/algo.ts +++ b/modules/sdk-coin-algo/src/algo.ts @@ -505,7 +505,7 @@ export class Algo extends BaseCoin { } /** @inheritDoc */ - deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): { derivationPath: string; key: string } { + async deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): Promise { const derivationPathInput = utxolib.crypto.hash256(Buffer.from(seed, 'utf8')).toString('hex'); const derivationPathParts = [ 999999, diff --git a/modules/sdk-coin-algo/test/unit/algo.ts b/modules/sdk-coin-algo/test/unit/algo.ts index da0307c1a1..3bfcdacf4b 100644 --- a/modules/sdk-coin-algo/test/unit/algo.ts +++ b/modules/sdk-coin-algo/test/unit/algo.ts @@ -621,8 +621,8 @@ describe('ALGO:', function () { basecoin.isValidPrv(kp.prv).should.equal(true); }); - it('should deterministically derive keypair with seed', () => { - const derivedKeypair = basecoin.deriveKeyWithSeed({ + it('should deterministically derive keypair with seed', async () => { + const derivedKeypair = await basecoin.deriveKeyWithSeed({ key: 'UBI2KNGT742KGIPHMZDJHHSADIT56HRDPVOOCCRYIETD4BAJLCBMQNSCNE', seed: 'cold derivation seed', }); diff --git a/modules/sdk-coin-btc/src/inscriptionBuilder.ts b/modules/sdk-coin-btc/src/inscriptionBuilder.ts index 44fbbfa46f..a5940b2a80 100644 --- a/modules/sdk-coin-btc/src/inscriptionBuilder.ts +++ b/modules/sdk-coin-btc/src/inscriptionBuilder.ts @@ -40,7 +40,7 @@ export class InscriptionBuilder implements IInscriptionBuilder { const user = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] }); assert(user.pub); - const derived = this.coin.deriveKeyWithSeed({ key: user.pub, seed: inscriptionData.toString() }); + const derived = await this.coin.deriveKeyWithSeed({ key: user.pub, seed: inscriptionData.toString() }); const compressedPublicKey = xpubToCompressedPub(derived.key); const xOnlyPublicKey = utxolib.bitgo.outputScripts.toXOnlyPublicKey(Buffer.from(compressedPublicKey, 'hex')); @@ -220,7 +220,7 @@ export class InscriptionBuilder implements IInscriptionBuilder { }, })) as HalfSignedUtxoTransaction; - const derived = this.coin.deriveKeyWithSeed({ key: xprv, seed: inscriptionData.toString() }); + const derived = await this.coin.deriveKeyWithSeed({ key: xprv, seed: inscriptionData.toString() }); const prv = xprvToRawPrv(derived.key); const fullySignedRevealTransaction = await inscriptions.signRevealTransaction( @@ -250,7 +250,7 @@ export class InscriptionBuilder implements IInscriptionBuilder { txPrebuild: PrebuildTransactionResult ): Promise { const userKeychain = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] }); - const prv = this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase }); + const prv = await this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase }); const halfSigned = (await this.wallet.signTransaction({ prv, txPrebuild })) as HalfSignedUtxoTransaction; return this.wallet.submitTransaction({ halfSigned }); diff --git a/modules/sdk-coin-hbar/package.json b/modules/sdk-coin-hbar/package.json index b34c5c8728..8955df07f1 100644 --- a/modules/sdk-coin-hbar/package.json +++ b/modules/sdk-coin-hbar/package.json @@ -42,6 +42,7 @@ "dependencies": { "@bitgo/sdk-coin-algo": "^1.25.0", "@bitgo/sdk-core": "^25.1.0", + "@bitgo/sdk-lib-mpc": "^8.32.0", "@bitgo/statics": "^46.1.0", "@hashgraph/proto": "2.12.0", "@hashgraph/sdk": "2.29.0", diff --git a/modules/sdk-coin-hbar/src/hbar.ts b/modules/sdk-coin-hbar/src/hbar.ts index a1df0cfeec..45f4d10e42 100644 --- a/modules/sdk-coin-hbar/src/hbar.ts +++ b/modules/sdk-coin-hbar/src/hbar.ts @@ -20,6 +20,7 @@ import { TokenEnablementConfig, BaseBroadcastTransactionOptions, BaseBroadcastTransactionResult, + Eddsa, } from '@bitgo/sdk-core'; import { BigNumber } from 'bignumber.js'; import * as stellar from 'stellar-sdk'; @@ -35,6 +36,17 @@ import { Hbar as HbarUnit, } from '@hashgraph/sdk'; import { PUBLIC_KEY_PREFIX } from './lib/keyPair'; +import { + Ed25519Bip32HdTree, + bigIntFromBufferBE, + bigIntFromBufferLE, + bigIntToBufferBE, + bigIntToBufferLE, + clamp, + getDerivationPath, +} from '@bitgo/sdk-lib-mpc'; +import { createHash } from 'crypto'; +import nacl from 'tweetnacl'; export interface HbarSignTransactionOptions extends SignTransactionOptions { txPrebuild: TransactionPrebuild; @@ -592,4 +604,78 @@ export class Hbar extends BaseCoin { return { txId: transactionResponse.transactionId.toString(), status: transactionReceipt.status.toString() }; } + + /** @inheritDoc */ + async deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): Promise<{ key: any; derivationPath: string }> { + const hdTree = await Ed25519Bip32HdTree.initialize(); + + const derivationPath = getDerivationPath(seed); + + if (key.startsWith('rpub')) { + // generic code that could be moved out of this function + const [pub, cc] = key.replace('rpub', '').split(':'); + + const pk = bigIntFromBufferLE(Buffer.from(pub, 'hex')); + const chaincode = bigIntFromBufferBE(Buffer.from(cc, 'hex')); + + const pubKeychain = { pk, chaincode }; + const derivedKeychain = hdTree.publicDerive(pubKeychain, derivationPath); + const derivedPub = bigIntToBufferLE(derivedKeychain.pk).toString('hex'); + + // coinspecific code + const derivedHbarPub = new HbarKeyPair({ pub: derivedPub }).getKeys().pub; + + return { key: derivedHbarPub, derivationPath }; + } else if (key.startsWith('rprv')) { + await Eddsa.initialize(); + // generic code that could be moved out of this function + const [prv, cc, prfx] = key.replace('rprv', '').split(':'); + const chaincode = bigIntFromBufferBE(Buffer.from(cc, 'hex')); + const sk = bigIntFromBufferLE(Buffer.from(prv, 'hex')); + const prefix = bigIntFromBufferBE(Buffer.from(prfx, 'hex')); + + // coinspecific code to get the public key + const rootKeyPair = new HbarKeyPair({ prv: prv }); + const pub = rootKeyPair.getKeys(true).pub; + + // generic code that could be moved out of this function + const pk = bigIntFromBufferLE(Buffer.from(pub, 'hex')); + const derivedKeychain = hdTree.privateDerive({ sk, pk, chaincode, prefix }, derivationPath); + const derivedPrv = bigIntToBufferLE(derivedKeychain.sk).toString('hex'); + // const derivedPub = bigIntToBufferLE(derivedKeychain.pk).toString('hex'); + + // coinspecific code + const derivedHbarPrv = new HbarKeyPair({ prv: derivedPrv }).getKeys().prv; + + return { key: derivedHbarPrv, derivationPath }; + } else { + throw new Error('Invalid key format'); + } + } + + createRootKeys(): { prv: string; pub: string } { + const seed = Buffer.from(nacl.randomBytes(64)); + const hash = createHash('sha512').update(seed.slice(0, 32)).digest(); + + const chaincode = bigIntFromBufferBE(seed.slice(32, 64)); + const prefix = bigIntFromBufferBE(hash.slice(32, 64)); + + const sk = clamp(bigIntFromBufferLE(hash.slice(0, 32))); + + const pk = new HbarKeyPair({ prv: bigIntToBufferLE(sk).toString('hex') }).getKeys(true).pub; + + const pub = 'rpub' + pk + ':' + bigIntToBufferBE(chaincode).toString('hex'); + const prv = + 'rprv' + + bigIntToBufferLE(sk).toString('hex') + + ':' + + bigIntToBufferBE(chaincode).toString('hex') + + ':' + + bigIntToBufferBE(prefix).toString('hex'); + + return { + prv, + pub, + }; + } } diff --git a/modules/sdk-coin-hbar/test/unit/hbar.ts b/modules/sdk-coin-hbar/test/unit/hbar.ts index 50cd821961..45fcb7473c 100644 --- a/modules/sdk-coin-hbar/test/unit/hbar.ts +++ b/modules/sdk-coin-hbar/test/unit/hbar.ts @@ -939,4 +939,33 @@ describe('Hedera Hashgraph:', function () { ); }); }); + + describe('Key Derivation', function () { + it('seedDerivationPath should be correct', async function () { + const expectedDerivationPath = 'm/999999/25725073/5121434'; + const expectedPub = '302a300506032b657003210044a0c69e1b13ce87410402c96c62bfdccf315ea150d1537b6a43d47acbef60fc'; + const expectedPrv = + '302e020100300506032b657004220420245072d243a8e4232a33ecf9578ecb7ed926926edcb7993318f48f45a7cf7e02'; + + // const rootKeyPair = basecoin.createRootKeys() + + const rootKeyPair = { + prv: 'rprvd011c015c8ca989b36a5ee7b73661dc854c50a2cb8e91db73f36d76b97cf7e42:9138336fdba9096b6343b9c8b444baf084528725d58a78531607b8534613e41d:8623583b56ebbe8bbd80e6599682e2b1242cabe97cbe013575db04039c028687', + pub: 'rpubb765e558aff0fdab8ed42e1745a0fb2ebc2d13512701fad02b147b5c01503eba:9138336fdba9096b6343b9c8b444baf084528725d58a78531607b8534613e41d', + }; + const seed = 'cold derivation seed'; + + const derivedPub = await basecoin.deriveKeyWithSeed({ key: rootKeyPair.pub, seed }); + assert.ok(derivedPub.derivationPath === expectedDerivationPath); + assert.ok(derivedPub.key === expectedPub); + + const derivedPrv = await basecoin.deriveKeyWithSeed({ key: rootKeyPair.prv, seed }); + assert.ok(derivedPrv.derivationPath === expectedDerivationPath); + derivedPrv.key.should.equal(expectedPrv); + + const derivedKeychain = new KeyPair({ prv: derivedPrv.key }).getKeys(); + derivedKeychain.pub.should.equal(expectedPub); + derivedKeychain.prv!.should.equal(expectedPrv); + }); + }); }); diff --git a/modules/sdk-coin-xlm/src/xlm.ts b/modules/sdk-coin-xlm/src/xlm.ts index b2271a1d07..8577003dba 100644 --- a/modules/sdk-coin-xlm/src/xlm.ts +++ b/modules/sdk-coin-xlm/src/xlm.ts @@ -1121,7 +1121,7 @@ export class Xlm extends BaseCoin { * @param key seed for the master key. Note: Not the public key or encoded private key. This is the raw seed. * @param entropySeed random seed which is hashed to generate the derivation path */ - deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): { derivationPath: string; key: string } { + async deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): Promise { const derivationPathInput = utxolib.crypto.hash256(Buffer.from(seed, 'utf8')).toString('hex'); const derivationPathParts = [ 999999, diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index 3ad0ba52dc..2456e62d37 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -379,7 +379,13 @@ export abstract class BaseCoin implements IBaseCoin { * @param seed * @returns {{key: string, derivationPath: string}} */ - deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): { key: string; derivationPath: string } { + async deriveKeyWithSeed({ + key, + seed, + }: { + key: string; + seed: string; + }): Promise<{ key: string; derivationPath: string }> { function sha256(input) { return crypto.createHash('sha256').update(input).digest(); } diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index bb1eac3df7..8542a394cc 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -497,7 +497,7 @@ export interface IBaseCoin { ): Promise; newWalletObject(walletParams: any): IWallet; feeEstimate(params: FeeEstimateOptions): Promise; - deriveKeyWithSeed(params: DeriveKeyWithSeedOptions): { key: string; derivationPath: string }; + deriveKeyWithSeed(params: DeriveKeyWithSeedOptions): Promise<{ key: string; derivationPath: string }>; keyIdsForSigning(): number[]; preCreateBitGo(params: PrecreateBitGoOptions): void; initiateRecovery(params: InitiateRecoveryOptions): never; diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index a4ce4b1a0c..d9f7fe457d 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -735,7 +735,7 @@ export interface IWallet { removeUser(params?: RemoveUserOptions): Promise; prebuildTransaction(params?: PrebuildTransactionOptions): Promise; signTransaction(params?: WalletSignTransactionOptions): Promise; - getUserPrv(params?: GetUserPrvOptions): string; + getUserPrv(params?: GetUserPrvOptions): Promise; prebuildAndSignTransaction(params?: PrebuildAndSignTransactionOptions): Promise; accelerateTransaction(params?: AccelerateTransactionOptions): Promise; submitTransaction(params?: SubmitTransactionOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 17ce9ed8b6..880a32998c 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -1705,7 +1705,11 @@ export class Wallet implements IWallet { }); if (this.multisigType() === 'tss') { - return this.signTransactionTss({ ...presign, prv: this.getUserPrv(presign as GetUserPrvOptions), apiVersion }); + return this.signTransactionTss({ + ...presign, + prv: await this.getUserPrv(presign as GetUserPrvOptions), + apiVersion, + }); } let { pubs } = params; @@ -1737,7 +1741,7 @@ export class Wallet implements IWallet { } return this.baseCoin.signTransaction({ ...signTransactionParams, - prv: this.getUserPrv(presign as GetUserPrvOptions), + prv: await this.getUserPrv(presign as GetUserPrvOptions), }); } @@ -1768,7 +1772,7 @@ export class Wallet implements IWallet { ...params, walletData: this._wallet, tssUtils: this.tssUtils, - prv: this.getUserPrv(userPrvOptions), + prv: await this.getUserPrv(userPrvOptions), keychain: keychains[0], backupKeychain: keychains.length > 1 ? keychains[1] : null, bitgoKeychain: keychains.length > 2 ? keychains[2] : null, @@ -1804,7 +1808,7 @@ export class Wallet implements IWallet { ...params, walletData: this._wallet, tssUtils: this.tssUtils, - prv: this.getUserPrv(userPrvOptions), + prv: await this.getUserPrv(userPrvOptions), keychain: keychains[0], backupKeychain: keychains.length > 1 ? keychains[1] : null, bitgoKeychain: keychains.length > 2 ? keychains[2] : null, @@ -1818,7 +1822,7 @@ export class Wallet implements IWallet { * @param [params.keychain / params.key] (object) or params.prv (string) * @param params.walletPassphrase (string) */ - getUserPrv(params: GetUserPrvOptions = {}): string { + async getUserPrv(params: GetUserPrvOptions = {}): Promise { const userKeychain = params.keychain || params.key; let userPrv = params.prv; if (userPrv && typeof userPrv !== 'string') { @@ -1839,7 +1843,7 @@ export class Wallet implements IWallet { if (userPrv && params.coldDerivationSeed) { // the derivation only makes sense when a key already exists - const derivation = this.baseCoin.deriveKeyWithSeed({ key: userPrv, seed: params.coldDerivationSeed }); + const derivation = await this.baseCoin.deriveKeyWithSeed({ key: userPrv, seed: params.coldDerivationSeed }); userPrv = derivation.key; } else if (!userPrv) { if (!userKeychain || typeof userKeychain !== 'object') { diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index e4b67ec5f1..362936d0f5 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -371,7 +371,7 @@ export class Wallets implements IWallets { userKeychainParams = userKeychain; if (params.coldDerivationSeed) { // the derivation only makes sense when a key already exists - const derivation = this.baseCoin.deriveKeyWithSeed({ + const derivation = await this.baseCoin.deriveKeyWithSeed({ key: params.userKey, seed: params.coldDerivationSeed, });