diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index 06ff2cc540..6d7906b5c4 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'); @@ -2222,7 +2222,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @param {Buffer} seed * @returns {KeyPair} object with generated pub and prv */ - generateKeyPair(seed: Buffer): KeyPair { + generateKeyPair(seed?: Buffer): KeyPair { if (!seed) { // An extended private key has both a normal 256 bit private key and a 256 // bit chain code, both of which must be random. 512 bits is therefore the diff --git a/modules/account-lib/test/resources/eddsaKeyDeriver.ts b/modules/account-lib/test/resources/eddsaKeyDeriver.ts new file mode 100644 index 0000000000..d2112dbbd4 --- /dev/null +++ b/modules/account-lib/test/resources/eddsaKeyDeriver.ts @@ -0,0 +1,17 @@ +export const data = { + seed: { + validSeed: + 'e3e670f488500f300790bee3fc2fb075d82d279459fa53024168664aec73054f2f5a0abfcd5889100604b0c22a430c4f9e1ac92b43675ca7cef0f9550acd2afa', + expectedRootKeys: { + prv: 'rprvb047f0d8b29083e1933102983d3c611cbf3364039cf3c985730e242488936070:f008e38df59581e2851939f035bebbb18755fcb9d3864eb2aaa22be060ede6:2f5a0abfcd5889100604b0c22a430c4f9e1ac92b43675ca7cef0f9550acd2afa:049aa12f8beaf88eb1a404e4fc5a7ad290accd5b6bb37fa497f2e3bd56fa5990', + pub: 'rpubf008e38df59581e2851939f035bebbb18755fcb9d3864eb2aaa22be060ede6:2f5a0abfcd5889100604b0c22a430c4f9e1ac92b43675ca7cef0f9550acd2afa', + }, + }, + rootKeys1: { + prv: 'rprvb00584090478b47d1d1bf45ee8bbd012ec3634ec5eb40fb944c22602daac8979:6b0de9d546d71d95a04a9cfe0b6b8cd2f5da4961c6170ff5e8b7d27098e3a909:427b2e97eeab94519d08e651b48d3df9326e9b22615f7655ad8c256659ec45af:838b4de99dcaf2392cec3222b29146816b1d3dfc1a7cad0695959a53ec23fa77', + pub: 'rpub6b0de9d546d71d95a04a9cfe0b6b8cd2f5da4961c6170ff5e8b7d27098e3a909:427b2e97eeab94519d08e651b48d3df9326e9b22615f7655ad8c256659ec45af', + derivedPub: '8beba02ca52fcb799134bc5698ee7f2b9e4e254749ea6baa65d7ddfbab82e330', + derivedPrv: + 'dd6122533efd21fece9248f54e82b812d16996b47726f8cadc2a20f1ddac89098beba02ca52fcb799134bc5698ee7f2b9e4e254749ea6baa65d7ddfbab82e330', + }, +}; diff --git a/modules/account-lib/test/unit/utils/eddsaKeyDeriver.ts b/modules/account-lib/test/unit/utils/eddsaKeyDeriver.ts new file mode 100644 index 0000000000..d5da13211e --- /dev/null +++ b/modules/account-lib/test/unit/utils/eddsaKeyDeriver.ts @@ -0,0 +1,74 @@ +import assert from 'assert'; +import { EddsaKeyDeriver } from '@bitgo/sdk-core'; +import { data } from '../../resources/eddsaKeyDeriver'; + +describe('EddsaKeyDeriver', () => { + describe('createRootKeys', () => { + it('should create root keys without seed', async () => { + const rootKeys = await EddsaKeyDeriver.createRootKeys(); + + assert.ok(rootKeys.prv); + assert.equal(rootKeys.prv.length, EddsaKeyDeriver.ROOT_PRV_KEY_LENGTH); + assert.ok(rootKeys.prv.startsWith(EddsaKeyDeriver.ROOT_PRV_KEY_PREFIX)); + assert.ok(rootKeys.pub); + assert.equal(rootKeys.pub.length, EddsaKeyDeriver.ROOT_PUB_KEY_LENGTH); + assert.ok(rootKeys.pub.startsWith(EddsaKeyDeriver.ROOT_PUB_KEY_PREFIX)); + }); + + it('should create root keys with seed', async () => { + const seed = Buffer.from(data.seed.validSeed, 'hex'); + const rootKeys = await EddsaKeyDeriver.createRootKeys(seed); + + assert.ok(rootKeys.prv); + assert.equal(rootKeys.prv, data.seed.expectedRootKeys.prv); + assert.ok(rootKeys.pub); + assert.equal(rootKeys.pub, data.seed.expectedRootKeys.pub); + }); + + it('should throw for invalid seed', async () => { + const seed = Buffer.from('asdf12f1', 'hex'); + + assert.rejects( + async () => { + await EddsaKeyDeriver.createRootKeys(seed); + }, + { message: 'Invalid seed' }, + ); + }); + }); + + describe('deriveKeyWithSeed', () => { + const seed = 'seed123'; + const expectedPath = 'm/999999/240510315/85914100'; + it('should derive a pub key and path from for root public key with seed', async () => { + const rootPubKey = data.rootKeys1.pub; + + const derivedKey = await EddsaKeyDeriver.deriveKeyWithSeed(rootPubKey, seed); + + assert.equal(derivedKey.key.length, 64); + assert.equal(derivedKey.key, data.rootKeys1.derivedPub); + assert.equal(derivedKey.derivationPath, expectedPath); + }); + + it('should derive a private key and path from for root private key with seed', async () => { + const rootPrvKey = data.rootKeys1.prv; + + const derivedKey = await EddsaKeyDeriver.deriveKeyWithSeed(rootPrvKey, seed); + + assert.equal(derivedKey.key.length, 128); + assert.equal(derivedKey.key, data.rootKeys1.derivedPrv); + assert.equal(derivedKey.derivationPath, expectedPath); + }); + + it('should throw an error for invalid key format', async () => { + const invalidKey = 'invalid:key:format'; + + await assert.rejects( + async () => { + await EddsaKeyDeriver.deriveKeyWithSeed(invalidKey, seed); + }, + { message: 'Invalid key format' }, + ); + }); + }); +}); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts index 42cfc5b726..c03a238e6f 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts @@ -994,7 +994,7 @@ describe('TSS Ecdsa Utils:', async function () { }); // Seems to be flaky on CI, failed here: https://github.com/BitGo/BitGoJS/actions/runs/5902489990/job/16010623888?pr=3822 - it.skip('createOfflineMuDeltaShare should succeed', async function () { + xit('createOfflineMuDeltaShare should succeed', async function () { const mockPassword = 'password'; const alphaLength = 1536; const deltaLength = 64; diff --git a/modules/bitgo/test/v2/unit/wallet.ts b/modules/bitgo/test/v2/unit/wallet.ts index b7fb75afa2..bcffa172ec 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/package.json b/modules/sdk-coin-algo/package.json index aaceb5ed30..1959fff607 100644 --- a/modules/sdk-coin-algo/package.json +++ b/modules/sdk-coin-algo/package.json @@ -42,7 +42,6 @@ "dependencies": { "@bitgo/sdk-core": "^25.1.0", "@bitgo/statics": "^46.1.0", - "@bitgo/utxo-lib": "^9.34.0", "@hashgraph/cryptography": "1.1.2", "@stablelib/hex": "^1.0.0", "algosdk": "1.14.0", diff --git a/modules/sdk-coin-algo/src/algo.ts b/modules/sdk-coin-algo/src/algo.ts index 06c28268dc..e60858a4fb 100644 --- a/modules/sdk-coin-algo/src/algo.ts +++ b/modules/sdk-coin-algo/src/algo.ts @@ -1,7 +1,6 @@ /** * @prettier */ -import * as utxolib from '@bitgo/utxo-lib'; import * as _ from 'lodash'; import { SeedValidator } from './seedValidator'; import { coins, CoinFamily } from '@bitgo/statics'; @@ -10,7 +9,6 @@ import { AddressCoinSpecific, BaseCoin, BitGoBase, - Ed25519KeyDeriver, InvalidAddressError, InvalidKey, KeyIndices, @@ -26,6 +24,9 @@ import { UnexpectedAddressError, VerifyAddressOptions, VerifyTransactionOptions, + EddsaKeyDeriver, + NotImplementedError, + GenerateKeyPairAsyncOptions, } from '@bitgo/sdk-core'; import stellar from 'stellar-sdk'; @@ -169,12 +170,23 @@ export class Algo extends BaseCoin { return true; } - /** - * Generate ed25519 key pair - * - * @param seed - * @returns {Object} object with generated pub, prv - */ + /** inheritdoc */ + deriveKeyWithSeed(): { key: string; derivationPath: string } { + throw new NotImplementedError('use deriveKeyWithSeedAsync instead'); + } + + /** inheritdoc */ + async deriveKeyWithSeedAsync({ + key, + seed, + }: { + key: string; + seed: string; + }): Promise<{ key: any; derivationPath: string }> { + return await EddsaKeyDeriver.deriveKeyWithSeed(key, seed); + } + + /** inheritdoc */ generateKeyPair(seed?: Buffer): KeyPair { const keyPair = seed ? new AlgoLib.KeyPair({ seed }) : new AlgoLib.KeyPair(); const keys = keyPair.getKeys(); @@ -188,6 +200,16 @@ export class Algo extends BaseCoin { }; } + /** inheritdoc */ + async generateKeyPairAsync(options: GenerateKeyPairAsyncOptions = {}): Promise { + const { seed, rootKey } = options; + if (rootKey) { + const keypair = await EddsaKeyDeriver.createRootKeys(seed); + return keypair; + } + return this.generateKeyPair(seed); + } + /** * Return boolean indicating whether input is valid public key for the coin. * @@ -504,23 +526,6 @@ export class Algo extends BaseCoin { return true; } - /** @inheritDoc */ - deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): { derivationPath: string; key: string } { - const derivationPathInput = utxolib.crypto.hash256(Buffer.from(seed, 'utf8')).toString('hex'); - const derivationPathParts = [ - 999999, - parseInt(derivationPathInput.slice(0, 7), 16), - parseInt(derivationPathInput.slice(7, 14), 16), - ]; - const derivationPath = 'm/' + derivationPathParts.map((part) => `${part}'`).join('/'); - const derivedKey = Ed25519KeyDeriver.derivePath(derivationPath, key).key; - const keypair = new AlgoLib.KeyPair({ seed: derivedKey }); - return { - key: keypair.getAddress(), - derivationPath, - }; - } - decodeTx(txn: Buffer): unknown { return AlgoLib.algoUtils.decodeAlgoTxn(txn); } diff --git a/modules/sdk-coin-algo/test/unit/algo.ts b/modules/sdk-coin-algo/test/unit/algo.ts index da0307c1a1..087a4cd8df 100644 --- a/modules/sdk-coin-algo/test/unit/algo.ts +++ b/modules/sdk-coin-algo/test/unit/algo.ts @@ -37,6 +37,14 @@ describe('ALGO:', function () { }); }); + describe('sync methods error handling', () => { + it('should throw error for deriveKeyWithSeed()', () => { + (() => { + basecoin.deriveKeyWithSeed(); + }).should.throw('use deriveKeyWithSeedAsync instead'); + }); + }); + describe('Transfer Builder: ', () => { const buildBaseTransferTransaction = ({ destination, amount = 10000, sender, memo = '' }) => { const factory = new AlgoLib.TransactionBuilderFactory(coins.get('algo')); @@ -608,32 +616,18 @@ describe('ALGO:', function () { }); describe('Generate wallet key pair: ', () => { - it('should generate key pair', () => { - const kp = basecoin.generateKeyPair(); + it('should generate key pair', async () => { + const kp = await basecoin.generateKeyPairAsync(); basecoin.isValidPub(kp.pub).should.equal(true); basecoin.isValidPrv(kp.prv).should.equal(true); }); - it('should generate key pair from seed', () => { + it('should generate key pair from seed', async () => { const seed = Buffer.from('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60', 'hex'); - const kp = basecoin.generateKeyPair(seed); + const kp = await basecoin.generateKeyPairAsync({ seed }); basecoin.isValidPub(kp.pub).should.equal(true); basecoin.isValidPrv(kp.prv).should.equal(true); }); - - it('should deterministically derive keypair with seed', () => { - const derivedKeypair = basecoin.deriveKeyWithSeed({ - key: 'UBI2KNGT742KGIPHMZDJHHSADIT56HRDPVOOCCRYIETD4BAJLCBMQNSCNE', - seed: 'cold derivation seed', - }); - console.log(JSON.stringify(derivedKeypair)); - - basecoin.isValidPub(derivedKeypair.key).should.be.true(); - derivedKeypair.should.deepEqual({ - key: 'NAYUBR4HQKJBBTNNKXQIY7GMHUCHVAUH5DQ4ZVHVL67CUQLGHRWDKQAHYU', - derivationPath: "m/999999'/25725073'/5121434'", - }); - }); }); describe('Enable, disable and transfer Token ', () => { diff --git a/modules/sdk-coin-btc/src/inscriptionBuilder.ts b/modules/sdk-coin-btc/src/inscriptionBuilder.ts index 44fbbfa46f..a3ba2c7972 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.deriveKeyWithSeedAsync({ 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-dot/test/unit/keypair.ts b/modules/sdk-coin-dot/test/unit/keypair.ts index 3620661cd0..e783dceab0 100644 --- a/modules/sdk-coin-dot/test/unit/keypair.ts +++ b/modules/sdk-coin-dot/test/unit/keypair.ts @@ -42,23 +42,6 @@ describe('Dot KeyPair', () => { const keyPair = new KeyPair({ pub: bs58Account.publicKey }); should.equal(keyPair.getKeys().pub, publicKeyHexString); }); - - it('should be able to derive keypair with hardened derivation', () => { - // using ed25519 (polkadot.js uses sr25519) - const keyPair = new KeyPair({ - prv: account1.secretKey, - }); - const derivationIndex = 0; - const derived = keyPair.deriveHardened(`m/0'/0'/0'/${derivationIndex}'`); - const derivedKeyPair = new KeyPair({ - prv: derived.prv || '', - }); - should.exists(derivedKeyPair.getAddress(DotAddressFormat.substrate)); - should.exists(derivedKeyPair.getKeys().prv); - should.exists(derivedKeyPair.getKeys().pub); - should.equal(derivedKeyPair.getKeys().prv?.length, 64); - should.equal(derivedKeyPair.getKeys().pub?.length, 64); - }); }); describe('KeyPair validation', () => { diff --git a/modules/sdk-coin-hbar/src/hbar.ts b/modules/sdk-coin-hbar/src/hbar.ts index a1df0cfeec..0a82fb6955 100644 --- a/modules/sdk-coin-hbar/src/hbar.ts +++ b/modules/sdk-coin-hbar/src/hbar.ts @@ -20,6 +20,9 @@ import { TokenEnablementConfig, BaseBroadcastTransactionOptions, BaseBroadcastTransactionResult, + EddsaKeyDeriver, + NotImplementedError, + GenerateKeyPairAsyncOptions, } from '@bitgo/sdk-core'; import { BigNumber } from 'bignumber.js'; import * as stellar from 'stellar-sdk'; @@ -174,12 +177,23 @@ export class Hbar extends BaseCoin { } } - /** - * Generate Hedera Hashgraph key pair - * - * @param seed - * @returns {Object} object with generated pub, prv - */ + /** inheritdoc */ + deriveKeyWithSeed(): { key: string; derivationPath: string } { + throw new NotImplementedError('use deriveKeyWithSeedAsync instead'); + } + + /** inheritdoc */ + async deriveKeyWithSeedAsync({ + key, + seed, + }: { + key: string; + seed: string; + }): Promise<{ key: any; derivationPath: string }> { + return await EddsaKeyDeriver.deriveKeyWithSeed(key, seed); + } + + /** inheritdoc */ generateKeyPair(seed?: Buffer): KeyPair { const keyPair = seed ? new HbarKeyPair({ seed }) : new HbarKeyPair(); const keys = keyPair.getKeys(); @@ -194,6 +208,16 @@ export class Hbar extends BaseCoin { }; } + /** inheritdoc */ + async generateKeyPairAsync(options: GenerateKeyPairAsyncOptions = {}): Promise { + const { seed, rootKey } = options; + if (rootKey) { + const keypair = await EddsaKeyDeriver.createRootKeys(seed); + return keypair; + } + return this.generateKeyPair(seed); + } + async parseTransaction(params: ParseTransactionOptions): Promise { return {}; } diff --git a/modules/sdk-coin-hbar/test/unit/hbar.ts b/modules/sdk-coin-hbar/test/unit/hbar.ts index 50cd821961..621672c944 100644 --- a/modules/sdk-coin-hbar/test/unit/hbar.ts +++ b/modules/sdk-coin-hbar/test/unit/hbar.ts @@ -7,7 +7,7 @@ import { BigNumber } from 'bignumber.js'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { BitGoAPI } from '@bitgo/sdk-api'; import { TxData, Transfer } from '../../src/lib/iface'; -import { Wallet } from '@bitgo/sdk-core'; +import { EddsaKeyDeriver, Wallet } from '@bitgo/sdk-core'; import * as TestData from '../fixtures/hbar'; import { Hbar, Thbar, KeyPair, HbarToken } from '../../src'; @@ -30,6 +30,14 @@ describe('Hedera Hashgraph:', function () { token = bitgo.coin('thbar:usdc'); }); + describe('sync methods error handling', () => { + it('should throw error for deriveKeyWithSeed()', () => { + (() => { + basecoin.deriveKeyWithSeed(); + }).should.throw('use deriveKeyWithSeedAsync instead'); + }); + }); + it('should instantiate the coin', function () { const basecoin = bitgo.coin('hbar'); basecoin.should.be.an.instanceof(Hbar); @@ -195,18 +203,18 @@ describe('Hedera Hashgraph:', function () { }); describe('Keypairs:', () => { - it('should generate a keypair from random seed', function () { - const keyPair = basecoin.generateKeyPair(); + it('should generate a keypair from random seed', async function () { + const keyPair = await basecoin.generateKeyPairAsync(); keyPair.should.have.property('pub'); keyPair.should.have.property('prv'); basecoin.isValidPub(keyPair.pub).should.equal(true); }); - it('should generate a keypair from a seed', function () { + it('should generate a keypair from a seed', async function () { const seedText = '80350b4208d381fbfe2276a326603049fe500731c46d3c9936b5ce036b51377f'; const seed = Buffer.from(seedText, 'hex'); - const keyPair = basecoin.generateKeyPair(seed); + const keyPair = await basecoin.generateKeyPairAsync({ seed }); keyPair.prv.should.equal( '302e020100300506032b65700422042080350b4208d381fbfe2276a326603049fe500731c46d3c9936b5ce036b51377f' @@ -226,6 +234,32 @@ describe('Hedera Hashgraph:', function () { '302e020100300506032b6570042204205965b6bfe85e8d543c36bf1b39800d633948bfdf742160d522f2391c57b0b055' ); }); + + it('should generate rootKeys with seed', async function () { + const seedText = + '3ba996300549b5d303c411fb83fbd5440da67ef1d725d5dbf57974d9187b2c6a3c6de78fed110d6cc17b5d98cd10e6de299a67c24cd767e966ec061717f07c5e'; + const seed = Buffer.from(seedText, 'hex'); + const keyPair = await basecoin.generateKeyPairAsync({ seed, rootKey: true }); + + assert.equal( + keyPair.prv, + 'rprv50d51c4eba07fb365fe0f6522d6e16fb8958c6c39498f30e5d9e0e946e252d79:136aff358cb5503b0617943ec322f54998fab5df37220474b66026d833310122:3c6de78fed110d6cc17b5d98cd10e6de299a67c24cd767e966ec061717f07c5e:dad7c8d66bd039ee3e1886a2eb060e4b3812b142722d5074ce8364971592f669' + ); + assert.equal( + keyPair.pub, + 'rpub136aff358cb5503b0617943ec322f54998fab5df37220474b66026d833310122:3c6de78fed110d6cc17b5d98cd10e6de299a67c24cd767e966ec061717f07c5e' + ); + }); + + it('should generate rootKeys without seed', async function () { + const keyPair = await basecoin.generateKeyPairAsync({ rootKey: true }); + assert.ok(keyPair.prv); + assert.ok(keyPair.prv.startsWith('rprv')); + assert.equal(keyPair.prv.length, EddsaKeyDeriver.ROOT_PRV_KEY_LENGTH); + assert.ok(keyPair.pub); + assert.ok(keyPair.pub.startsWith('rpub')); + assert.equal(keyPair.pub.length, EddsaKeyDeriver.ROOT_PUB_KEY_LENGTH); + }); }); describe('Verify Transaction:', () => { diff --git a/modules/sdk-coin-hbar/test/unit/keyPair.ts b/modules/sdk-coin-hbar/test/unit/keyPair.ts index 7643b95b24..6724294854 100644 --- a/modules/sdk-coin-hbar/test/unit/keyPair.ts +++ b/modules/sdk-coin-hbar/test/unit/keyPair.ts @@ -10,6 +10,39 @@ const prv = testData.ACCOUNT_1.prvKeyWithPrefix; describe('Hedera Key Pair', () => { describe('should create a valid KeyPair', () => { + describe('from root keys', async () => { + const rootKey = { + prv: 'rprv68d6f1ff76f10f30f4bd21bc900d3e4ed2d0f5ba4f243080f3d12774d5cf6746:bb085bbfc58b8c12945c44a00e68816e183b7f9eca6fac9f1769dd63a981dbf3:028f2c66f941b4049cd5b7f76e43c30633e34abf3e0c7722274c536c8d6a7a85:32bfbd348ccd84e7c37552bfbb908e5aa9c4a605fcd0b5ccfad36f3bc7bc85bc', + pub: 'rpubbb085bbfc58b8c12945c44a00e68816e183b7f9eca6fac9f1769dd63a981dbf3:028f2c66f941b4049cd5b7f76e43c30633e34abf3e0c7722274c536c8d6a7a85', + }; + it('should create a valid KeyPair from root private', async () => { + const newKeyPair = new KeyPair({ prv: rootKey.prv }); + const keys = newKeyPair.getKeys(); + assert.ok(keys.prv); + assert.ok(keys.pub); + assert.equal(keys.prv.slice(0, 32), testData.ed25519PrivKeyPrefix); + assert.equal(keys.pub.slice(0, 24), testData.ed25519PubKeyPrefix); + const rawKeys = newKeyPair.getKeys(true); + assert.ok(rawKeys.prv); + assert.ok(rawKeys.pub); + assert.equal(rawKeys.prv.length, 64); + assert.equal(rawKeys.pub.length, 64); + assert.ok(rootKey.prv.includes(rawKeys.prv)); + assert.ok(rootKey.pub.includes(rawKeys.pub)); + }); + + it('should create a valid KeyPair from root pub', async () => { + const newKeyPair = new KeyPair({ pub: rootKey.pub }); + const keys = newKeyPair.getKeys(); + assert.ok(keys.pub); + assert.equal(keys.pub.slice(0, 24), testData.ed25519PubKeyPrefix); + const rawKeys = newKeyPair.getKeys(true); + assert.ok(rawKeys.pub); + assert.equal(rawKeys.pub.length, 64); + assert.ok(rootKey.pub.includes(rawKeys.pub)); + }); + }); + it('from an empty value', () => { const keyPair = new KeyPair(); should.exists(keyPair.getKeys().prv); diff --git a/modules/sdk-coin-sol/test/unit/keyPair.ts b/modules/sdk-coin-sol/test/unit/keyPair.ts index 612814db6c..5d3330c786 100644 --- a/modules/sdk-coin-sol/test/unit/keyPair.ts +++ b/modules/sdk-coin-sol/test/unit/keyPair.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import { KeyPair } from '../../src'; import should from 'should'; import * as testData from '../resources/sol'; -import { isValidPublicKey, isValidAddress, isValidPrivateKey } from '../../src/lib/utils'; +import { isValidPublicKey, isValidAddress } from '../../src/lib/utils'; describe('Sol KeyPair', function () { const defaultSeed = { seed: testData.accountWithSeed.seed }; @@ -158,34 +158,4 @@ describe('Sol KeyPair', function () { keyPair.verifySignature(message, signature).should.equal(false); }); }); - - describe('deriveHardened', () => { - it('should derive child key pairs', () => { - const rootKeyPair = new KeyPair(); - for (let i = 0; i < 50; i++) { - const path = `m/0'/0'/0'/${i}'`; - const derived = new KeyPair(rootKeyPair.deriveHardened(path)); - - isValidPublicKey(derived.getKeys().pub).should.be.true(); - isValidAddress(derived.getAddress()).should.be.true(); - - const derivedPrv = derived.getKeys().prv; - should.exist(derivedPrv); - isValidPrivateKey(derivedPrv as string | Uint8Array).should.be.true(); - - const rederived = new KeyPair(rootKeyPair.deriveHardened(path)); - rederived.getKeys().should.deepEqual(derived.getKeys()); - } - }); - - it('should not be able to derive without private key', () => { - const rootKeyPair = new KeyPair({ pub: testData.accountWithSeed.publicKey }); - assert.throws(() => rootKeyPair.deriveHardened("m/0'/0'/0'/0'"), /need private key to derive hardened keypair/); - }); - - it('should throw error for non-hardened path', () => { - const rootKeyPair = new KeyPair(); - assert.throws(() => rootKeyPair.deriveHardened('m/0/0/0/0'), /Invalid derivation path/); - }); - }); }); diff --git a/modules/sdk-coin-xlm/package.json b/modules/sdk-coin-xlm/package.json index 07766ac2e1..83e8b968d9 100644 --- a/modules/sdk-coin-xlm/package.json +++ b/modules/sdk-coin-xlm/package.json @@ -42,7 +42,6 @@ "dependencies": { "@bitgo/sdk-core": "^25.1.0", "@bitgo/statics": "^46.1.0", - "@bitgo/utxo-lib": "^9.34.0", "bignumber.js": "^9.1.1", "lodash": "^4.17.14", "stellar-sdk": "^10.0.1", diff --git a/modules/sdk-coin-xlm/src/xlm.ts b/modules/sdk-coin-xlm/src/xlm.ts index b2271a1d07..74699e099e 100644 --- a/modules/sdk-coin-xlm/src/xlm.ts +++ b/modules/sdk-coin-xlm/src/xlm.ts @@ -1,6 +1,5 @@ import assert from 'assert'; import * as _ from 'lodash'; -import * as utxolib from '@bitgo/utxo-lib'; import * as querystring from 'querystring'; import * as url from 'url'; import * as request from 'superagent'; @@ -12,7 +11,6 @@ import { BitGoBase, checkKrsProvider, common, - Ed25519KeyDeriver, ExtraPrebuildParamsOptions, InvalidAddressError, InvalidMemoIdError, @@ -33,6 +31,9 @@ import { VerifyAddressOptions as BaseVerifyAddressOptions, VerifyTransactionOptions as BaseVerifyTransactionOptions, Wallet, + EddsaKeyDeriver, + NotImplementedError, + GenerateKeyPairAsyncOptions, } from '@bitgo/sdk-core'; import { toBitgoRequest } from '@bitgo/sdk-api'; import { getStellarKeys } from './getStellarKeys'; @@ -209,12 +210,24 @@ export class Xlm extends BaseCoin { return 'https://horizon.stellar.org'; } - /** - * Generate a new key pair on the ed25519 curve - * @param seed - * @returns generated pub and prv - */ - generateKeyPair(seed: Buffer): KeyPair { + /** inheritdoc */ + deriveKeyWithSeed(): { key: string; derivationPath: string } { + throw new NotImplementedError('use deriveKeyWithSeedAsync instead'); + } + + /** inheritdoc */ + async deriveKeyWithSeedAsync({ + key, + seed, + }: { + key: string; + seed: string; + }): Promise<{ key: any; derivationPath: string }> { + return await EddsaKeyDeriver.deriveKeyWithSeed(key, seed); + } + + /** inheritdoc */ + generateKeyPair(seed?: Buffer): KeyPair { const pair = seed ? stellar.Keypair.fromRawEd25519Seed(seed) : stellar.Keypair.random(); return { pub: pair.publicKey(), @@ -222,6 +235,16 @@ export class Xlm extends BaseCoin { }; } + /** inheritdoc */ + async generateKeyPairAsync(options: GenerateKeyPairAsyncOptions = {}): Promise { + const { seed, rootKey } = options; + if (rootKey) { + const keypair = await EddsaKeyDeriver.createRootKeys(seed); + return keypair; + } + return this.generateKeyPair(seed); + } + /** * Get decoded ed25519 public key from raw data * @@ -782,7 +805,7 @@ export class Xlm extends BaseCoin { } seed = stellar.StrKey.decodeEd25519SecretSeed(rootPrv); } - const keyPair = this.generateKeyPair(seed); + const keyPair = await this.generateKeyPairAsync({ seed }); // extend the wallet initialization params walletParams.rootPrivateKey = keyPair.prv; return walletParams; @@ -1112,31 +1135,6 @@ export class Xlm extends BaseCoin { return true; } - /** - * Derive a hardened child public key from a master key seed using an additional seed for randomness. - * - * Due to technical differences between keypairs on the ed25519 curve and the secp256k1 curve, - * only hardened private key derivation is supported. - * - * @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 } { - const derivationPathInput = utxolib.crypto.hash256(Buffer.from(seed, 'utf8')).toString('hex'); - const derivationPathParts = [ - 999999, - parseInt(derivationPathInput.slice(0, 7), 16), - parseInt(derivationPathInput.slice(7, 14), 16), - ]; - const derivationPath = 'm/' + derivationPathParts.map((part) => `${part}'`).join('/'); - const derivedKey = Ed25519KeyDeriver.derivePath(derivationPath, key).key; - const keypair = stellar.Keypair.fromRawEd25519Seed(derivedKey); - return { - key: keypair.publicKey(), - derivationPath, - }; - } - /** * stellar-sdk has two overloads for toXDR, and typescript can't seem to figure out the * correct one to use, so we have to be very explicit as to which one we want. diff --git a/modules/sdk-coin-xlm/test/unit/xlm.ts b/modules/sdk-coin-xlm/test/unit/xlm.ts index 436eabda56..06e8aeb422 100644 --- a/modules/sdk-coin-xlm/test/unit/xlm.ts +++ b/modules/sdk-coin-xlm/test/unit/xlm.ts @@ -124,9 +124,12 @@ describe('XLM:', function () { }); }); - it('should validate pub key', () => { - const { pub } = basecoin.keychains().create(); - basecoin.isValidPub(pub).should.equal(true); + describe('sync methods error handling', () => { + it('should throw error for deriveKeyWithSeed()', () => { + (() => { + basecoin.deriveKeyWithSeed(); + }).should.throw('use deriveKeyWithSeedAsync instead'); + }); }); it('should validate stellar username', function () { @@ -276,7 +279,7 @@ describe('XLM:', function () { }); it('should supplement wallet generation with provided private key', async function () { - const rootPrivateKey = basecoin.generateKeyPair().prv; + const rootPrivateKey = (await basecoin.generateKeyPairAsync()).prv; const walletParams = await basecoin.supplementGenerateWallet({ rootPrivateKey }); walletParams.should.have.property('rootPrivateKey'); walletParams.rootPrivateKey.should.equal(rootPrivateKey); @@ -385,8 +388,8 @@ describe('XLM:', function () { validSignature.should.equal(true); }); - it('should fail to verify the wrong signature on a tx', function () { - const keyPair = basecoin.generateKeyPair(); + it('should fail to verify the wrong signature on a tx', async function () { + const keyPair = await basecoin.generateKeyPairAsync(); const tx = new stellar.Transaction(halfSignedTransaction.halfSigned.txBase64, stellar.Networks.TESTNET); const validSignature = basecoin.verifySignature(keyPair.pub, tx.hash(), tx.signatures[0].signature()); validSignature.should.equal(false); @@ -863,8 +866,8 @@ describe('XLM:', function () { }); describe('Keypairs:', () => { - it('should generate a keypair from random seed', function () { - const keyPair = basecoin.generateKeyPair(); + it('should generate a keypair from random seed', async function () { + const keyPair = await basecoin.generateKeyPairAsync(); keyPair.should.have.property('pub'); keyPair.should.have.property('prv'); @@ -875,9 +878,9 @@ describe('XLM:', function () { basecoin.isValidPrv(keyPair.prv).should.equal(true); }); - it('should generate a keypair from seed', function () { + it('should generate a keypair from seed', async function () { const seed = randomBytes(32); - const keyPair = basecoin.generateKeyPair(seed); + const keyPair = await basecoin.generateKeyPairAsync({ seed }); keyPair.should.have.property('pub'); keyPair.should.have.property('prv'); @@ -891,21 +894,14 @@ describe('XLM:', function () { stellar.StrKey.encodeEd25519SecretSeed(seed).should.equal(secret); }); - it('should deterministically derive a child key from master seed and entropy seed', () => { - const seed = Buffer.alloc(32).fill(0).toString('hex'); - const masterSeed = '0x01020304050607080910111213141516171819202122232425262728293031'; - - const derivedKey = basecoin.deriveKeyWithSeed({ key: masterSeed, seed }); - - derivedKey.should.have.properties({ - key: 'GCJR3ORBWOKGFA3FTGYDDQVFEEMCYXFHY6KAUOTU4MQMFHK4LLSWWGLW', - derivationPath: "m/999999'/230673453'/206129755'", - }); - }); - it('should validate pub key', () => { const { pub } = basecoin.keychains().create(); basecoin.isValidPub(pub).should.equal(true); }); + + it('should validate pub key with Async method', async () => { + const { pub } = await basecoin.keychains().createAsync(); + basecoin.isValidPub(pub).should.equal(true); + }); }); }); diff --git a/modules/sdk-core/src/account-lib/baseCoin/ed25519KeyPair.ts b/modules/sdk-core/src/account-lib/baseCoin/ed25519KeyPair.ts index a62b397be0..582e9a2231 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/ed25519KeyPair.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/ed25519KeyPair.ts @@ -6,10 +6,10 @@ import { toHex, toUint8Array, } from '../util/crypto'; -import { Ed25519KeyDeriver } from '../util/ed25519KeyDeriver'; import { BaseKeyPair } from './baseKeyPair'; import { AddressFormat, DotAddressFormat } from './enum'; import { isPrivateKey, isPublicKey, isSeed, DefaultKeys, KeyPairOptions } from './iface'; +import { EddsaKeyDeriver } from '../util/eddsaKeyDeriver'; const DEFAULT_SEED_SIZE_BYTES = 32; @@ -53,7 +53,11 @@ export abstract class Ed25519KeyPair implements BaseKeyPair { /** @inheritdoc */ recordKeysFromPrivateKey(prv: string): void { - if (isValidEd25519Seed(prv)) { + if (EddsaKeyDeriver.isValidRootPrvKey(prv)) { + const { prv: parsedPrv, pub: parsedPub } = EddsaKeyDeriver.parseRootPrvKey(prv); + const naclKeyPair = nacl.sign.keyPair.fromSecretKey(Buffer.from(parsedPrv + parsedPub, 'hex')); + this.setKeyPair(naclKeyPair); + } else if (isValidEd25519Seed(prv)) { const decodedPrv = toUint8Array(prv); const naclKeyPair = nacl.sign.keyPair.fromSeed(decodedPrv); this.setKeyPair(naclKeyPair); @@ -68,7 +72,10 @@ export abstract class Ed25519KeyPair implements BaseKeyPair { /** @inheritdoc */ recordKeysFromPublicKey(pub: string): void { - if (isValidEd25519PublicKey(pub)) { + if (EddsaKeyDeriver.isValidRootPubKey(pub)) { + const { pub: parsedPub } = EddsaKeyDeriver.parseRootPubKey(pub); + this.keyPair = { pub: parsedPub }; + } else if (isValidEd25519PublicKey(pub)) { this.keyPair = { pub }; } else { this.keyPair = this.recordKeysFromPublicKeyInProtocolFormat(pub); @@ -122,21 +129,4 @@ export abstract class Ed25519KeyPair implements BaseKeyPair { const publicKey = toUint8Array(this.keyPair.pub); return nacl.sign.detached.verify(messageToVerify, signature, publicKey); } - - /** - * Derives a hardened child key pair using this key pair's secret key - * as the seed. - * - * @param path derivation path - */ - deriveHardened(path: string): DefaultKeys { - if (!this.keyPair?.prv) { - throw new Error('need private key to derive hardened keypair'); - } - - const seed = Ed25519KeyDeriver.derivePath(path, this.keyPair.prv).key; - const derivedKeyPair = nacl.sign.keyPair.fromSeed(seed); - - return this.getKeyPair(derivedKeyPair); - } } diff --git a/modules/sdk-core/src/account-lib/index.ts b/modules/sdk-core/src/account-lib/index.ts index a1cdc4feaa..f72e1a9e29 100644 --- a/modules/sdk-core/src/account-lib/index.ts +++ b/modules/sdk-core/src/account-lib/index.ts @@ -5,5 +5,7 @@ export * from './mpc'; export * from './util/crypto'; // Deprecated export * as acountLibCrypto from './util/crypto'; +// Deprecated export * from './util/ed25519KeyDeriver'; export * from './staking'; +export * from './util/eddsaKeyDeriver'; diff --git a/modules/sdk-core/src/account-lib/mpc/util.ts b/modules/sdk-core/src/account-lib/mpc/util.ts index ad5f2c522e..a9465276eb 100644 --- a/modules/sdk-core/src/account-lib/mpc/util.ts +++ b/modules/sdk-core/src/account-lib/mpc/util.ts @@ -6,6 +6,7 @@ import { bigIntToBufferBE, clamp, getPaillierPublicKey, + getDerivationPath, } from '@bitgo/sdk-lib-mpc'; /** @@ -19,4 +20,5 @@ export { bigIntToBufferBE, clamp, getPaillierPublicKey, + getDerivationPath, }; diff --git a/modules/sdk-core/src/account-lib/util/ed25519KeyDeriver.ts b/modules/sdk-core/src/account-lib/util/ed25519KeyDeriver.ts index acd98bf763..7ce712897b 100644 --- a/modules/sdk-core/src/account-lib/util/ed25519KeyDeriver.ts +++ b/modules/sdk-core/src/account-lib/util/ed25519KeyDeriver.ts @@ -14,6 +14,8 @@ export interface HdKeypair { * * https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0005.md * https://github.com/satoshilabs/slips/blob/master/slip-0010.md + * + * @deprecated */ export class Ed25519KeyDeriver { /** diff --git a/modules/sdk-core/src/account-lib/util/eddsaKeyDeriver.ts b/modules/sdk-core/src/account-lib/util/eddsaKeyDeriver.ts new file mode 100644 index 0000000000..4774066f84 --- /dev/null +++ b/modules/sdk-core/src/account-lib/util/eddsaKeyDeriver.ts @@ -0,0 +1,123 @@ +import { createHash } from 'crypto'; +import nacl from 'tweetnacl'; + +import { + Eddsa, + bigIntFromBufferLE, + bigIntToBufferLE, + clamp, + Ed25519BIP32, + bigIntFromBufferBE, + getDerivationPath, +} from '../mpc'; + +export type RootKeys = { + prv: string; + pub: string; +}; + +export class EddsaKeyDeriver { + static readonly ROOT_PUB_KEY_PREFIX = 'rpub'; + static readonly ROOT_PRV_KEY_PREFIX = 'rprv'; + static readonly ROOT_PRV_KEY_LENGTH = 263; + static readonly ROOT_PUB_KEY_LENGTH = 133; + + static async createRootKeys(seed?: Buffer): Promise { + if (seed && seed.length !== 64) { + throw new Error('Invalid seed length, requires 64 bytes Buffer'); + } + await Eddsa.initialize(); + const startingSeed = seed || Buffer.from(nacl.randomBytes(64)); + const hash = createHash('sha512').update(startingSeed.slice(0, 32)).digest(); + + const chaincode = startingSeed.slice(32).toString('hex'); + const prefix = hash.slice(32).toString('hex'); + + const sk = clamp(bigIntFromBufferLE(hash.slice(0, 32))); + const skString = bigIntToBufferLE(sk).toString('hex'); + const pkString = bigIntToBufferLE(Eddsa.curve.basePointMult(sk)).toString('hex'); + + return { + prv: this.formatRootPrvKey(skString, pkString, chaincode, prefix), + pub: this.formatRootPubKey(pkString, chaincode), + }; + } + + private static formatRootPubKey(pub: string, chaincode: string): string { + return this.ROOT_PUB_KEY_PREFIX + pub + ':' + chaincode; + } + + private static formatRootPrvKey(prv: string, pub: string, chaincode: string, prefix: string): string { + return this.ROOT_PRV_KEY_PREFIX + prv + ':' + pub + ':' + chaincode + ':' + prefix; + } + + static parseRootPubKey(pub: string): { pub: string; chaincode: string } { + const parts = pub.replace(this.ROOT_PUB_KEY_PREFIX, '').split(':'); + if (parts.length !== 2) { + throw new Error('Invalid public key'); + } + return { + pub: parts[0], + chaincode: parts[1], + }; + } + + static parseRootPrvKey(prv: string): { prv: string; pub: string; chaincode: string; prefix: string } { + const parts = prv.replace(this.ROOT_PRV_KEY_PREFIX, '').split(':'); + if (parts.length !== 4) { + throw new Error('Invalid private key'); + } + return { + prv: parts[0], + pub: parts[1], + chaincode: parts[2], + prefix: parts[3], + }; + } + + static async deriveKeyWithSeed(key: string, seed: string): Promise<{ key: string; derivationPath: string }> { + const hdTree = await Ed25519BIP32.initialize(); + const derivationPath = getDerivationPath(seed); + + if (key.startsWith(this.ROOT_PUB_KEY_PREFIX)) { + const { pub, chaincode } = this.parseRootPubKey(key); + + const publicKeychain = { + pk: bigIntFromBufferLE(Buffer.from(pub, 'hex')), + chaincode: bigIntFromBufferBE(Buffer.from(chaincode, 'hex')), + }; + + const derivedKeychain = hdTree.publicDerive(publicKeychain, derivationPath); + const derivedPub = bigIntToBufferLE(derivedKeychain.pk).toString('hex'); + + return { key: derivedPub, derivationPath }; + } else if (key.startsWith(this.ROOT_PRV_KEY_PREFIX)) { + await Eddsa.initialize(); + + const { prv, chaincode, prefix } = this.parseRootPrvKey(key); + const skBI = bigIntFromBufferLE(Buffer.from(prv, 'hex')); + + const privateKeychain = { + sk: skBI, + pk: Eddsa.curve.basePointMult(skBI), + chaincode: bigIntFromBufferBE(Buffer.from(chaincode, 'hex')), + prefix: bigIntFromBufferBE(Buffer.from(prefix, 'hex')), + }; + const derivedKeychain = hdTree.privateDerive(privateKeychain, derivationPath); + const derivedPrv = bigIntToBufferLE(derivedKeychain.sk).toString('hex'); + const derivedPub = bigIntToBufferLE(derivedKeychain.pk).toString('hex'); + + return { key: derivedPrv + derivedPub, derivationPath }; + } else { + throw new Error('Invalid key format'); + } + } + + static isValidRootPrvKey(prv: string): boolean { + return prv.length === this.ROOT_PRV_KEY_LENGTH && prv.startsWith(this.ROOT_PRV_KEY_PREFIX); + } + + static isValidRootPubKey(pub: string): boolean { + return pub.length === this.ROOT_PUB_KEY_LENGTH && pub.startsWith(this.ROOT_PUB_KEY_PREFIX); + } +} diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index 3ad0ba52dc..2b6bb7857e 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -41,6 +41,7 @@ import { BuildNftTransferDataOptions, BaseBroadcastTransactionOptions, BaseBroadcastTransactionResult, + GenerateKeyPairAsyncOptions, } from './iBaseCoin'; import { IInscriptionBuilder } from '../inscriptionBuilder'; import { Hash } from 'crypto'; @@ -378,6 +379,8 @@ export abstract class BaseCoin implements IBaseCoin { * @param key * @param seed * @returns {{key: string, derivationPath: string}} + * + * @deprecated use deriveKeyWithSeedAsync instead */ deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): { key: string; derivationPath: string } { function sha256(input) { @@ -397,6 +400,22 @@ export abstract class BaseCoin implements IBaseCoin { }; } + /** + * The cold wallet tool uses this function to derive an extended key that is based on the passed key and seed + * @param key + * @param seed + * @returns {{key: string, derivationPath: string}} + */ + async deriveKeyWithSeedAsync({ + key, + seed, + }: { + key: string; + seed: string; + }): Promise<{ key: string; derivationPath: string }> { + return this.deriveKeyWithSeed({ key, seed }); + } + /** * Specifies what key we will need for signing - right now we just need the * user key. @@ -428,11 +447,24 @@ export abstract class BaseCoin implements IBaseCoin { abstract parseTransaction(params: ParseTransactionOptions): Promise; /** + * Generate a key pair on the curve used by the coin - * * @param seed + * + * @deprecated use generateKeyPairAsync instead + */ + abstract generateKeyPair(seed?: Buffer, rootKey?: boolean): KeyPair; + + /** + * Generate a key pair on the curve used by the coin + * @param options - optional parameters + * @param options.seed - optional - seed to use for key generation + * @param options.rootKey - optional - true to generate a root key (if supported by the coin), defaults to false + * @returns {Promise} - the generated key pair */ - abstract generateKeyPair(seed?: Buffer): KeyPair; + async generateKeyPairAsync(options?: GenerateKeyPairAsyncOptions): Promise { + return this.generateKeyPair(options?.seed); + } /** * Return boolean indicating whether input is valid public key for the coin. diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index bb1eac3df7..a2ab480712 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -445,6 +445,11 @@ export interface BaseBroadcastTransactionResult { txId: string; } +export interface GenerateKeyPairAsyncOptions { + seed?: Buffer; + rootKey?: boolean; +} + export interface IBaseCoin { type: string; tokenConfig?: BaseTokenConfig; @@ -498,11 +503,13 @@ export interface IBaseCoin { newWalletObject(walletParams: any): IWallet; feeEstimate(params: FeeEstimateOptions): Promise; deriveKeyWithSeed(params: DeriveKeyWithSeedOptions): { key: string; derivationPath: string }; + deriveKeyWithSeedAsync(params: DeriveKeyWithSeedOptions): Promise<{ key: string; derivationPath: string }>; keyIdsForSigning(): number[]; preCreateBitGo(params: PrecreateBitGoOptions): void; initiateRecovery(params: InitiateRecoveryOptions): never; parseTransaction(params: ParseTransactionOptions): Promise; generateKeyPair(seed?: Buffer): KeyPair; + generateKeyPairAsync(options?: GenerateKeyPairAsyncOptions): Promise; isValidPub(pub: string): boolean; isValidMofNSetup(params: ValidMofNOptions): boolean; isValidAddress(address: string): boolean; diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index bf2b5894bd..9348593533 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -175,6 +175,7 @@ export interface IKeychains { updatePassword(params: UpdatePasswordOptions): Promise; updateSingleKeychainPassword(params?: UpdateSingleKeychainPasswordOptions): Keychain; create(params?: { seed?: Buffer }): KeyPair; + createAsync(params?: { seed?: Buffer; rootKey?: boolean }): Promise; add(params?: AddKeychainOptions): Promise; createBitGo(params?: CreateBitGoOptions): Promise; createBackup(params?: CreateBackupOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/keychain/keychains.ts b/modules/sdk-core/src/bitgo/keychain/keychains.ts index 8efaf76fa1..3c39c02a9f 100644 --- a/modules/sdk-core/src/bitgo/keychain/keychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/keychains.ts @@ -173,11 +173,22 @@ export class Keychains implements IKeychains { /** * Create a public/private key pair * @param params.seed + * @deprecated use createAsync instead */ create(params: { seed?: Buffer } = {}): KeyPair { return this.baseCoin.generateKeyPair(params.seed); } + /** + * Create a public/private key pair + * @param params - optional parameters + * @param params.seed - optional - seed to use for key generation + * @param params.rootKey - optional - true to generate a root key (if supported by the coin), defaults to false + */ + async createAsync(params: { seed?: Buffer; rootKey?: boolean } = {}): Promise { + return this.baseCoin.generateKeyPairAsync(params); + } + /** * Add a keychain to BitGo's records * @param params 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 0a7aa69c2f..a1de0a0ce3 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -1711,7 +1711,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; @@ -1743,7 +1747,7 @@ export class Wallet implements IWallet { } return this.baseCoin.signTransaction({ ...signTransactionParams, - prv: this.getUserPrv(presign as GetUserPrvOptions), + prv: await this.getUserPrv(presign as GetUserPrvOptions), }); } @@ -1774,7 +1778,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, @@ -1810,7 +1814,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, @@ -1824,7 +1828,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') { @@ -1845,7 +1849,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..10672223c7 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.deriveKeyWithSeedAsync({ key: params.userKey, seed: params.coldDerivationSeed, }); @@ -384,7 +384,7 @@ export class Wallets implements IWallets { throw new Error('cannot generate user keypair without passphrase'); } // Create the user key. - userKeychain = this.baseCoin.keychains().create(); + userKeychain = await this.baseCoin.keychains().createAsync(); userKeychain.encryptedPrv = this.bitgo.encrypt({ password: passphrase, input: userKeychain.prv }); userKeychainParams = { pub: userKeychain.pub,