diff --git a/src/BIP44CoinTypeNode.test.ts b/src/BIP44CoinTypeNode.test.ts index 4ffb55a7..dd991be8 100644 --- a/src/BIP44CoinTypeNode.test.ts +++ b/src/BIP44CoinTypeNode.test.ts @@ -9,12 +9,31 @@ import { getBIP44AddressKeyDeriver, } from '.'; import fixtures from '../test/fixtures'; +import type { CryptographicFunctions } from './cryptography'; +import { hmacSha512, pbkdf2Sha512 } from './cryptography'; import { encodeExtendedKey, PRIVATE_KEY_VERSION } from './extended-keys'; import { mnemonicPhraseToBytes } from './utils'; const defaultBip39NodeToken = `bip39:${fixtures.local.mnemonic}` as const; const defaultBip39BytesToken = mnemonicPhraseToBytes(fixtures.local.mnemonic); +/** + * Get mock cryptographic functions for testing. The functions are wrappers + * around the real implementations, but they also track how many times they + * were called. + * + * @returns The mock cryptographic functions. + */ +function getMockFunctions(): CryptographicFunctions { + const mockHmacSha512 = jest.fn().mockImplementation(hmacSha512); + const mockPbkdf2Sha512 = jest.fn().mockImplementation(pbkdf2Sha512); + + return { + hmacSha512: mockHmacSha512, + pbkdf2Sha512: mockPbkdf2Sha512, + }; +} + describe('BIP44CoinTypeNode', () => { describe('fromJSON', () => { it('initializes a BIP44CoinTypeNode (serialized BIP44Node)', async () => { @@ -52,6 +71,36 @@ describe('BIP44CoinTypeNode', () => { }); }); + it('initializes a BIP44CoinTypeNode (serialized BIP44Node) with custom cryptographic functions', async () => { + const bip44Node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + defaultBip39NodeToken, + BIP44PurposeNodeToken, + `bip32:60'`, + ], + }); + + const functions = getMockFunctions(); + + const coinType = 60; + const pathString = `m / bip32:44' / bip32:${coinType}'`; + const node = await BIP44CoinTypeNode.fromJSON( + bip44Node.toJSON(), + coinType, + functions, + ); + + await node.deriveBIP44AddressKey({ address_index: 0 }); + + expect(node.coin_type).toStrictEqual(coinType); + expect(node.depth).toBe(2); + expect(node.privateKey).toStrictEqual(bip44Node.privateKey); + expect(node.publicKey).toStrictEqual(bip44Node.publicKey); + expect(node.path).toStrictEqual(pathString); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(3); + }); + it('throws if node has invalid depth', async () => { const arbitraryCoinType = 78; @@ -237,6 +286,26 @@ describe('BIP44CoinTypeNode', () => { expect(node.toJSON()).toStrictEqual(stringNode.toJSON()); }); + it('initializes a BIP44CoinTypeNode (derivation path) with custom cryptographic functions', async () => { + const functions = getMockFunctions(); + const node = await BIP44CoinTypeNode.fromDerivationPath( + [defaultBip39NodeToken, BIP44PurposeNodeToken, `bip32:60'`], + functions, + ); + + const coinType = 60; + const pathString = `m / bip32:44' / bip32:${coinType}'`; + + expect(node.coin_type).toStrictEqual(coinType); + expect(node.depth).toBe(2); + expect(node.privateKeyBytes).toHaveLength(32); + expect(node.publicKeyBytes).toHaveLength(65); + expect(node.path).toStrictEqual(pathString); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(3); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + }); + it('throws if derivation path has invalid depth', async () => { await expect( BIP44CoinTypeNode.fromDerivationPath([ @@ -365,6 +434,30 @@ describe('BIP44CoinTypeNode', () => { expect(childNode.privateKey).toStrictEqual(node.privateKey); expect(childNode.chainCode).toStrictEqual(node.chainCode); }); + + it('keeps the same cryptographic functions in the child node', async () => { + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [...coinTypePath, `bip32:0'`, `bip32:0`, `bip32:0`], + }); + + const functions = getMockFunctions(); + const coinTypeNode = await BIP44CoinTypeNode.fromDerivationPath( + [defaultBip39NodeToken, BIP44PurposeNodeToken, `bip32:60'`], + functions, + ); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(3); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + + const childNode = await coinTypeNode.deriveBIP44AddressKey({ + address_index: 0, + }); + + expect(childNode.privateKey).toStrictEqual(node.privateKey); + expect(childNode.chainCode).toStrictEqual(node.chainCode); + expect(functions.hmacSha512).toHaveBeenCalledTimes(6); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + }); }); describe('publicKey', () => { diff --git a/src/BIP44CoinTypeNode.ts b/src/BIP44CoinTypeNode.ts index bb065c5c..97c38198 100644 --- a/src/BIP44CoinTypeNode.ts +++ b/src/BIP44CoinTypeNode.ts @@ -10,6 +10,7 @@ import type { HardenedBIP32Node, } from './constants'; import { BIP_32_HARDENED_OFFSET } from './constants'; +import type { CryptographicFunctions } from './cryptography'; import type { SupportedCurve } from './curves'; import { deriveChildNode } from './SLIP10Node'; import type { CoinTypeToAddressIndices } from './utils'; @@ -75,19 +76,28 @@ export class BIP44CoinTypeNode implements BIP44CoinTypeNodeInterface { * @param json - The {@link JsonBIP44Node} for the key of this node. * @param coin_type - The coin_type index of this node. Must be a non-negative * integer. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ - static async fromJSON(json: JsonBIP44Node, coin_type: number) { + static async fromJSON( + json: JsonBIP44Node, + coin_type: number, + cryptographicFunctions?: CryptographicFunctions, + ) { validateCoinType(coin_type); validateCoinTypeNodeDepth(json.depth); - const node = await BIP44Node.fromExtendedKey({ - depth: json.depth, - index: json.index, - parentFingerprint: json.parentFingerprint, - chainCode: hexStringToBytes(json.chainCode), - privateKey: nullableHexStringToBytes(json.privateKey), - publicKey: hexStringToBytes(json.publicKey), - }); + const node = await BIP44Node.fromExtendedKey( + { + depth: json.depth, + index: json.index, + parentFingerprint: json.parentFingerprint, + chainCode: hexStringToBytes(json.chainCode), + privateKey: nullableHexStringToBytes(json.privateKey), + publicKey: hexStringToBytes(json.publicKey), + }, + cryptographicFunctions, + ); return new BIP44CoinTypeNode(node, coin_type); } @@ -107,13 +117,21 @@ export class BIP44CoinTypeNode implements BIP44CoinTypeNodeInterface { * `0 / 1 / 2 / 3 / 4 / 5` * * @param derivationPath - The derivation path for the key of this node. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ - static async fromDerivationPath(derivationPath: CoinTypeHDPathTuple) { + static async fromDerivationPath( + derivationPath: CoinTypeHDPathTuple, + cryptographicFunctions?: CryptographicFunctions, + ) { validateCoinTypeNodeDepth(derivationPath.length - 1); - const node = await BIP44Node.fromDerivationPath({ - derivationPath, - }); + const node = await BIP44Node.fromDerivationPath( + { + derivationPath, + }, + cryptographicFunctions, + ); // Split the bip32 string token and extract the coin_type index. const pathPart = derivationPath[BIP_44_COIN_TYPE_DEPTH].split( @@ -324,11 +342,14 @@ function validateCoinType(coin_type: unknown): asserts coin_type is number { * @param indices.account - The `account` index. Default: `0`. * @param indices.change - The `change` index. Default: `0`. * @param indices.address_index - The `address_index` index. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived `address_index` key for the specified derivation path. */ export async function deriveBIP44AddressKey( parentKeyOrNode: BIP44CoinTypeNode | JsonBIP44CoinTypeNode | string, { account = 0, change = 0, address_index }: CoinTypeToAddressIndices, + cryptographicFunctions?: CryptographicFunctions, ): Promise { const path = getBIP44CoinTypeToAddressPathTuple({ account, @@ -336,11 +357,14 @@ export async function deriveBIP44AddressKey( address_index, }); - const node = await getNode(parentKeyOrNode); - const childNode = await deriveChildNode({ - path, - node, - }); + const node = await getNode(parentKeyOrNode, cryptographicFunctions); + const childNode = await deriveChildNode( + { + path, + node, + }, + cryptographicFunctions, + ); return new BIP44Node(childNode); } @@ -391,16 +415,19 @@ export type BIP44AddressKeyDeriver = { * This node contains a BIP-44 key of depth 2, `coin_type`. * @param accountAndChangeIndices - The `account` and `change` indices that * will be used to derive addresses. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The deriver function for the derivation path specified by the * `coin_type` node, `account`, and `change` indices. */ export async function getBIP44AddressKeyDeriver( node: BIP44CoinTypeNode | JsonBIP44CoinTypeNode | string, accountAndChangeIndices?: Omit, + cryptographicFunctions?: CryptographicFunctions, ) { const { account = 0, change = 0 } = accountAndChangeIndices ?? {}; - const actualNode = await getNode(node); + const actualNode = await getNode(node, cryptographicFunctions); const accountNode = getHardenedBIP32NodeToken(account); const changeNode = getBIP32NodeToken(change); @@ -409,16 +436,19 @@ export async function getBIP44AddressKeyDeriver( address_index: number, isHardened = false, ): Promise => { - const slip10Node = await deriveChildNode({ - path: [ - accountNode, - changeNode, - isHardened - ? getHardenedBIP32NodeToken(address_index) - : getUnhardenedBIP32NodeToken(address_index), - ], - node: actualNode, - }); + const slip10Node = await deriveChildNode( + { + path: [ + accountNode, + changeNode, + isHardened + ? getHardenedBIP32NodeToken(address_index) + : getUnhardenedBIP32NodeToken(address_index), + ], + node: actualNode, + }, + cryptographicFunctions, + ); return new BIP44Node(slip10Node); }; @@ -441,9 +471,13 @@ export async function getBIP44AddressKeyDeriver( * The depth of the node is validated to be a valid coin type node. * * @param node - A BIP-44 coin type node, JSON node or extended key. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. This is + * only used if the node is an extended key string or JSON object. */ async function getNode( node: BIP44CoinTypeNode | JsonBIP44CoinTypeNode | string, + cryptographicFunctions?: CryptographicFunctions, ): Promise { if (node instanceof BIP44CoinTypeNode) { validateCoinTypeNodeDepth(node.depth); @@ -452,7 +486,10 @@ async function getNode( } if (typeof node === 'string') { - const bip44Node = await BIP44Node.fromExtendedKey(node); + const bip44Node = await BIP44Node.fromExtendedKey( + node, + cryptographicFunctions, + ); const coinTypeNode = await BIP44CoinTypeNode.fromNode( bip44Node, bip44Node.index - BIP_32_HARDENED_OFFSET, @@ -463,5 +500,9 @@ async function getNode( return coinTypeNode; } - return BIP44CoinTypeNode.fromJSON(node, node.coin_type); + return BIP44CoinTypeNode.fromJSON( + node, + node.coin_type, + cryptographicFunctions, + ); } diff --git a/src/BIP44Node.test.ts b/src/BIP44Node.test.ts index ba8f0193..fb04594b 100644 --- a/src/BIP44Node.test.ts +++ b/src/BIP44Node.test.ts @@ -2,6 +2,8 @@ import { bytesToHex } from '@metamask/utils'; import { BIP44Node, BIP44PurposeNodeToken, secp256k1 } from '.'; import fixtures from '../test/fixtures'; +import type { CryptographicFunctions } from './cryptography'; +import { hmacSha512, pbkdf2Sha512 } from './cryptography'; import { compressPublicKey } from './curves/secp256k1'; import { createBip39KeyFromSeed, deriveChildKey } from './derivers/bip39'; import { @@ -14,6 +16,23 @@ import { hexStringToBytes, mnemonicPhraseToBytes } from './utils'; const defaultBip39NodeToken = `bip39:${fixtures.local.mnemonic}` as const; const defaultBip39BytesToken = mnemonicPhraseToBytes(fixtures.local.mnemonic); +/** + * Get mock cryptographic functions for testing. The functions are wrappers + * around the real implementations, but they also track how many times they + * were called. + * + * @returns The mock cryptographic functions. + */ +function getMockFunctions(): CryptographicFunctions { + const mockHmacSha512 = jest.fn().mockImplementation(hmacSha512); + const mockPbkdf2Sha512 = jest.fn().mockImplementation(pbkdf2Sha512); + + return { + hmacSha512: mockHmacSha512, + pbkdf2Sha512: mockPbkdf2Sha512, + }; +} + describe('BIP44Node', () => { describe('fromExtendedKey', () => { it('initializes a new node from a private key', async () => { @@ -43,6 +62,42 @@ describe('BIP44Node', () => { }); }); + it('initializes a new node from a private key with custom cryptographic functions', async () => { + const { privateKey, chainCode } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); + + const functions = getMockFunctions(); + + // Ethereum coin type node + const node = await BIP44Node.fromExtendedKey( + { + privateKey, + chainCode, + depth: 2, + parentFingerprint: 1, + index: 0, + }, + functions, + ); + + await node.derive([`bip32:0'`]); + + expect(node.depth).toBe(2); + expect(node.toJSON()).toStrictEqual({ + depth: node.depth, + masterFingerprint: node.masterFingerprint, + parentFingerprint: node.parentFingerprint, + index: node.index, + privateKey: node.privateKey, + publicKey: node.publicKey, + chainCode: node.chainCode, + }); + + expect(functions.hmacSha512).toHaveBeenCalled(); + }); + it('initializes a new node from JSON', async () => { const { privateKey, chainCode } = await deriveChildKey({ path: fixtures.local.mnemonic, @@ -61,6 +116,30 @@ describe('BIP44Node', () => { expect(await BIP44Node.fromJSON(node.toJSON())).toStrictEqual(node); }); + it('initializes a new node from JSON with custom cryptographic functions', async () => { + const { privateKey, chainCode } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); + + // Ethereum coin type node + const baseNode = await BIP44Node.fromExtendedKey({ + privateKey, + chainCode, + depth: 2, + parentFingerprint: 1, + index: 0, + }); + + const functions = getMockFunctions(); + const node = await BIP44Node.fromJSON(baseNode.toJSON(), functions); + + await node.derive([`bip32:0'`]); + + expect(node).toStrictEqual(baseNode); + expect(functions.hmacSha512).toHaveBeenCalled(); + }); + it.each(fixtures.bip32)( 'initializes a node from an extended public key', async ({ keys }) => { @@ -155,6 +234,35 @@ describe('BIP44Node', () => { expect(node.toJSON()).toStrictEqual(stringNode.toJSON()); }); + it('initializes a new node from a derivation path with custom cryptographic functions', async () => { + const functions = getMockFunctions(); + + // Ethereum coin type node + const node = await BIP44Node.fromDerivationPath( + { + derivationPath: [ + defaultBip39NodeToken, + BIP44PurposeNodeToken, + `bip32:60'`, + ], + }, + functions, + ); + + expect(node.depth).toBe(2); + expect(node.toJSON()).toStrictEqual({ + depth: node.depth, + masterFingerprint: node.masterFingerprint, + parentFingerprint: node.parentFingerprint, + index: node.index, + privateKey: node.privateKey, + publicKey: node.publicKey, + chainCode: node.chainCode, + }); + expect(functions.hmacSha512).toHaveBeenCalled(); + expect(functions.pbkdf2Sha512).toHaveBeenCalled(); + }); + it('throws an error if attempting to modify the fields of a node', async () => { const node: any = await BIP44Node.fromDerivationPath({ derivationPath: [ @@ -310,6 +418,40 @@ describe('BIP44Node', () => { expect(childNode.publicKey).toBe(targetNode.publicKey); }); + it('keeps the same cryptographic functions in the child node', async () => { + const coinTypeNode = `bip32:40'`; + const targetNode = await BIP44Node.fromDerivationPath({ + derivationPath: [ + defaultBip39NodeToken, + BIP44PurposeNodeToken, + coinTypeNode, + ], + }); + + const functions = getMockFunctions(); + const node = await BIP44Node.fromDerivationPath( + { + derivationPath: [defaultBip39NodeToken, BIP44PurposeNodeToken], + }, + functions, + ); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(2); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + + const childNode = await node.derive([coinTypeNode]); + await childNode.derive([`bip32:0'`]); + + expect(childNode).toMatchObject({ + depth: targetNode.depth, + privateKey: targetNode.privateKey, + publicKey: targetNode.publicKey, + }); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(4); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + }); + it('throws if the parent node is already a leaf node', async () => { const node = await BIP44Node.fromDerivationPath({ derivationPath: [ @@ -567,6 +709,31 @@ describe('BIP44Node', () => { expect(neuterNode.privateKey).toBeUndefined(); expect(neuterNode.privateKeyBytes).toBeUndefined(); }); + + it('keeps the same cryptographic functions in the child node', async () => { + const functions = getMockFunctions(); + const node = await BIP44Node.fromDerivationPath( + { + derivationPath: [ + defaultBip39NodeToken, + BIP44PurposeNodeToken, + `bip32:0'`, + `bip32:0'`, + ], + }, + functions, + ); + + const neuterNode = node.neuter(); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(4); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + + await neuterNode.derive([`bip32:0`]); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(5); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + }); }); describe('toJSON', () => { diff --git a/src/BIP44Node.ts b/src/BIP44Node.ts index 30c0590f..08df42f0 100644 --- a/src/BIP44Node.ts +++ b/src/BIP44Node.ts @@ -13,6 +13,7 @@ import { MAX_BIP_44_DEPTH, MIN_BIP_44_DEPTH, } from './constants'; +import type { CryptographicFunctions } from './cryptography'; import type { SupportedCurve } from './curves'; import { decodeExtendedKey, PRIVATE_KEY_VERSION } from './extended-keys'; import { SLIP10Node, validateBIP32Depth } from './SLIP10Node'; @@ -104,9 +105,14 @@ export class BIP44Node implements BIP44NodeInterface { * for documentation. * * @param json - The JSON representation of a SLIP-10 node. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ - static async fromJSON(json: JsonBIP44Node): Promise { - return BIP44Node.fromExtendedKey(json); + static async fromJSON( + json: JsonBIP44Node, + cryptographicFunctions?: CryptographicFunctions, + ): Promise { + return BIP44Node.fromExtendedKey(json, cryptographicFunctions); } /** @@ -124,9 +130,12 @@ export class BIP44Node implements BIP44NodeInterface { * @param options.publicKey - The public key for the node. If a private key is * specified, this parameter is ignored. * @param options.chainCode - The chain code for the node. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ static async fromExtendedKey( options: BIP44ExtendedKeyOptions | string, + cryptographicFunctions?: CryptographicFunctions, ): Promise { if (typeof options === 'string') { const extendedKey = decodeExtendedKey(options); @@ -136,24 +145,30 @@ export class BIP44Node implements BIP44NodeInterface { if (extendedKey.version === PRIVATE_KEY_VERSION) { const { privateKey } = extendedKey; - return BIP44Node.fromExtendedKey({ - depth, - parentFingerprint, - index, - privateKey, - chainCode, - }); + return BIP44Node.fromExtendedKey( + { + depth, + parentFingerprint, + index, + privateKey, + chainCode, + }, + cryptographicFunctions, + ); } const { publicKey } = extendedKey; - return BIP44Node.fromExtendedKey({ - depth, - parentFingerprint, - index, - publicKey, - chainCode, - }); + return BIP44Node.fromExtendedKey( + { + depth, + parentFingerprint, + index, + publicKey, + chainCode, + }, + cryptographicFunctions, + ); } const { @@ -167,15 +182,18 @@ export class BIP44Node implements BIP44NodeInterface { validateBIP44Depth(depth); - const node = await SLIP10Node.fromExtendedKey({ - privateKey, - publicKey, - chainCode, - depth, - parentFingerprint, - index, - curve: 'secp256k1', - }); + const node = await SLIP10Node.fromExtendedKey( + { + privateKey, + publicKey, + chainCode, + depth, + parentFingerprint, + index, + curve: 'secp256k1', + }, + cryptographicFunctions, + ); return new BIP44Node(node); } @@ -200,17 +218,23 @@ export class BIP44Node implements BIP44NodeInterface { * @param options - An object containing the derivation path. * @param options.derivationPath - The rooted HD tree path that will be used * to derive the key of this node. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ - static async fromDerivationPath({ - derivationPath, - }: BIP44DerivationPathOptions): Promise { + static async fromDerivationPath( + { derivationPath }: BIP44DerivationPathOptions, + cryptographicFunctions?: CryptographicFunctions, + ): Promise { validateBIP44Depth(derivationPath.length - 1); validateBIP44DerivationPath(derivationPath, MIN_BIP_44_DEPTH); - const node = await SLIP10Node.fromDerivationPath({ - derivationPath, - curve: 'secp256k1', - }); + const node = await SLIP10Node.fromDerivationPath( + { + derivationPath, + curve: 'secp256k1', + }, + cryptographicFunctions, + ); return new BIP44Node(node); } diff --git a/src/SLIP10Node.test.ts b/src/SLIP10Node.test.ts index a5384a22..dc387ad2 100644 --- a/src/SLIP10Node.test.ts +++ b/src/SLIP10Node.test.ts @@ -2,6 +2,8 @@ import { bytesToHex, hexToBytes } from '@metamask/utils'; import fixtures from '../test/fixtures'; import { BIP44PurposeNodeToken } from './constants'; +import type { CryptographicFunctions } from './cryptography'; +import { pbkdf2Sha512, hmacSha512 } from './cryptography'; import { ed25519, secp256k1 } from './curves'; import { compressPublicKey } from './curves/secp256k1'; import { createBip39KeyFromSeed, deriveChildKey } from './derivers/bip39'; @@ -12,6 +14,23 @@ import { hexStringToBytes, mnemonicPhraseToBytes } from './utils'; const defaultBip39NodeToken = `bip39:${fixtures.local.mnemonic}` as const; const defaultBip39BytesToken = mnemonicPhraseToBytes(fixtures.local.mnemonic); +/** + * Get mock cryptographic functions for testing. The functions are wrappers + * around the real implementations, but they also track how many times they + * were called. + * + * @returns The mock cryptographic functions. + */ +function getMockFunctions(): CryptographicFunctions { + const mockHmacSha512 = jest.fn().mockImplementation(hmacSha512); + const mockPbkdf2Sha512 = jest.fn().mockImplementation(pbkdf2Sha512); + + return { + hmacSha512: mockHmacSha512, + pbkdf2Sha512: mockPbkdf2Sha512, + }; +} + describe('SLIP10Node', () => { describe('constructor', () => { it('throws an error when the constructor guard is not provided', async () => { @@ -206,6 +225,48 @@ describe('SLIP10Node', () => { ); }); + it('initializes a new node from a private key with custom cryptographic functions', async () => { + const { privateKeyBytes, chainCodeBytes } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); + + const functions = getMockFunctions(); + const node = await SLIP10Node.fromExtendedKey( + { + privateKey: privateKeyBytes, + chainCode: chainCodeBytes, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'secp256k1', + }, + functions, + ); + + await node.derive(['bip32:0']); + + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toHaveLength(32); + expect(node.publicKeyBytes).toHaveLength(65); + expect(functions.hmacSha512).toHaveBeenCalled(); + }); + + it('initializes a new node from JSON with custom cryptographic functions', async () => { + const baseNode = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); + + const functions = getMockFunctions(); + const node = await SLIP10Node.fromJSON(baseNode.toJSON(), functions); + + await node.derive(['bip32:0']); + + expect(node).toStrictEqual(baseNode); + expect(functions.hmacSha512).toHaveBeenCalled(); + }); + it('throws if no public or private key is specified', async () => { await expect( SLIP10Node.fromExtendedKey({ @@ -422,6 +483,25 @@ describe('SLIP10Node', () => { expect(node.publicKey).toBe(publicKey); }); + it('initializes a new node from a private key with custom cryptographic functions', async () => { + const { extendedKey, privateKey, chainCode } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); + + const functions = getMockFunctions(); + const node = await SLIP10Node.fromExtendedKey(extendedKey, functions); + + await node.derive(['bip32:0']); + + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toHaveLength(32); + expect(node.publicKeyBytes).toHaveLength(65); + expect(node.privateKey).toBe(privateKey); + expect(node.chainCode).toBe(chainCode); + expect(functions.hmacSha512).toHaveBeenCalled(); + }); + it('throws if the extended key is invalid', async () => { await expect(SLIP10Node.fromExtendedKey('foo')).rejects.toThrow( 'Invalid extended key: Value is not base58-encoded, or the checksum is invalid.', @@ -551,6 +631,37 @@ describe('SLIP10Node', () => { expect(node.toJSON()).toStrictEqual(stringNode.toJSON()); }); + it('initializes a new node from a derivation path with custom cryptographic functions', async () => { + const functions = getMockFunctions(); + + // Ethereum coin type node + const node = await SLIP10Node.fromDerivationPath( + { + derivationPath: [ + defaultBip39NodeToken, + BIP44PurposeNodeToken, + `bip32:60'`, + ], + curve: 'secp256k1', + }, + functions, + ); + + expect(node.depth).toBe(2); + expect(node.toJSON()).toStrictEqual({ + depth: node.depth, + masterFingerprint: node.masterFingerprint, + parentFingerprint: node.parentFingerprint, + index: node.index, + curve: 'secp256k1', + privateKey: node.privateKey, + publicKey: node.publicKey, + chainCode: node.chainCode, + }); + expect(functions.hmacSha512).toHaveBeenCalled(); + expect(functions.pbkdf2Sha512).toHaveBeenCalled(); + }); + it('throws if the derivation path is empty', async () => { await expect( SLIP10Node.fromDerivationPath({ @@ -678,6 +789,28 @@ describe('SLIP10Node', () => { }); }); + it('keeps the same cryptographic functions in the child node', async () => { + const functions = getMockFunctions(); + + const coinTypeNode = `bip32:40'`; + const node = await SLIP10Node.fromDerivationPath( + { + derivationPath: [defaultBip39NodeToken, BIP44PurposeNodeToken], + curve: 'secp256k1', + }, + functions, + ); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(2); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + + const childNode = await node.derive([coinTypeNode]); + await childNode.derive(['bip32:0']); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(4); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + }); + it('throws when trying to derive a hardened node without a private key', async () => { const node = await SLIP10Node.fromDerivationPath({ derivationPath: [defaultBip39NodeToken, BIP44PurposeNodeToken], @@ -983,6 +1116,32 @@ describe('SLIP10Node', () => { expect(neuterNode.privateKey).toBeUndefined(); expect(neuterNode.privateKeyBytes).toBeUndefined(); }); + + it('keeps the same cryptographic functions in the child node', async () => { + const functions = getMockFunctions(); + + const node = await SLIP10Node.fromDerivationPath( + { + derivationPath: [ + defaultBip39NodeToken, + BIP44PurposeNodeToken, + `bip32:0'`, + `bip32:0'`, + ], + curve: 'secp256k1', + }, + functions, + ); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(4); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + + const neuterNode = node.neuter(); + await neuterNode.derive(['bip32:0']); + + expect(functions.hmacSha512).toHaveBeenCalledTimes(5); + expect(functions.pbkdf2Sha512).toHaveBeenCalledTimes(1); + }); }); describe('toJSON', () => { diff --git a/src/SLIP10Node.ts b/src/SLIP10Node.ts index dddb0eb9..9860a915 100644 --- a/src/SLIP10Node.ts +++ b/src/SLIP10Node.ts @@ -4,6 +4,7 @@ import type { BIP44CoinTypeNode } from './BIP44CoinTypeNode'; import type { BIP44Node } from './BIP44Node'; import type { RootedSLIP10PathTuple, SLIP10PathTuple } from './constants'; import { BYTES_KEY_LENGTH } from './constants'; +import type { CryptographicFunctions } from './cryptography'; import type { SupportedCurve } from './curves'; import { getCurveByName } from './curves'; import { deriveKeyFromPath } from './derivation'; @@ -124,9 +125,14 @@ export class SLIP10Node implements SLIP10NodeInterface { * for documentation. * * @param json - The JSON representation of a SLIP-10 node. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ - static async fromJSON(json: JsonSLIP10Node): Promise { - return SLIP10Node.fromExtendedKey(json); + static async fromJSON( + json: JsonSLIP10Node, + cryptographicFunctions?: CryptographicFunctions, + ): Promise { + return SLIP10Node.fromExtendedKey(json, cryptographicFunctions); } /** @@ -138,8 +144,13 @@ export class SLIP10Node implements SLIP10NodeInterface { * validation fails. * * @param extendedKey - The BIP-32 extended key string. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ - static async fromExtendedKey(extendedKey: string): Promise; + static async fromExtendedKey( + extendedKey: string, + cryptographicFunctions?: CryptographicFunctions, + ): Promise; /** * Create a new SLIP-10 node from a key and chain code. You must specify @@ -162,12 +173,15 @@ export class SLIP10Node implements SLIP10NodeInterface { * specified, this parameter is ignored. * @param options.chainCode - The chain code for the node. * @param options.curve - The curve used by the node. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ static async fromExtendedKey( // These signatures could technically be combined, but it's easier to // document them separately. // eslint-disable-next-line @typescript-eslint/unified-signatures options: SLIP10ExtendedKeyOptions, + cryptographicFunctions?: CryptographicFunctions, ): Promise; /** @@ -193,9 +207,12 @@ export class SLIP10Node implements SLIP10NodeInterface { * specified, this parameter is ignored. * @param options.chainCode - The chain code for the node. * @param options.curve - The curve used by the node. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. */ static async fromExtendedKey( options: SLIP10ExtendedKeyOptions | string, + cryptographicFunctions?: CryptographicFunctions, ): Promise { if (typeof options === 'string') { const extendedKey = decodeExtendedKey(options); @@ -205,28 +222,34 @@ export class SLIP10Node implements SLIP10NodeInterface { if (extendedKey.version === PRIVATE_KEY_VERSION) { const { privateKey } = extendedKey; - return SLIP10Node.fromExtendedKey({ + return SLIP10Node.fromExtendedKey( + { + depth, + parentFingerprint, + index, + privateKey, + chainCode, + // BIP-32 key serialisation assumes `secp256k1`. + curve: 'secp256k1', + }, + cryptographicFunctions, + ); + } + + const { publicKey } = extendedKey; + + return SLIP10Node.fromExtendedKey( + { depth, parentFingerprint, index, - privateKey, + publicKey, chainCode, // BIP-32 key serialisation assumes `secp256k1`. curve: 'secp256k1', - }); - } - - const { publicKey } = extendedKey; - - return SLIP10Node.fromExtendedKey({ - depth, - parentFingerprint, - index, - publicKey, - chainCode, - // BIP-32 key serialisation assumes `secp256k1`. - curve: 'secp256k1', - }); + }, + cryptographicFunctions, + ); } const { @@ -276,6 +299,7 @@ export class SLIP10Node implements SLIP10NodeInterface { publicKey: await curveObject.getPublicKey(privateKeyBytes), curve, }, + cryptographicFunctions, this.#constructorGuard, ); } @@ -293,6 +317,7 @@ export class SLIP10Node implements SLIP10NodeInterface { publicKey: publicKeyBytes, curve, }, + cryptographicFunctions, this.#constructorGuard, ); } @@ -323,12 +348,14 @@ export class SLIP10Node implements SLIP10NodeInterface { * @param options.derivationPath - The rooted HD tree path that will be used * to derive the key of this node. * @param options.curve - The curve used by the node. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns A new SLIP-10 node. */ - static async fromDerivationPath({ - derivationPath, - curve, - }: SLIP10DerivationPathOptions) { + static async fromDerivationPath( + { derivationPath, curve }: SLIP10DerivationPathOptions, + cryptographicFunctions?: CryptographicFunctions, + ) { validateCurve(curve); if (!derivationPath) { @@ -341,11 +368,14 @@ export class SLIP10Node implements SLIP10NodeInterface { ); } - return await deriveKeyFromPath({ - path: derivationPath, - depth: derivationPath.length - 1, - curve, - }); + return await deriveKeyFromPath( + { + path: derivationPath, + depth: derivationPath.length - 1, + curve, + }, + cryptographicFunctions, + ); } static #constructorGuard = Symbol('SLIP10Node.constructor'); @@ -366,6 +396,8 @@ export class SLIP10Node implements SLIP10NodeInterface { public readonly publicKeyBytes: Uint8Array; + #cryptographicFunctions: CryptographicFunctions; + // eslint-disable-next-line no-restricted-syntax private constructor( { @@ -378,6 +410,7 @@ export class SLIP10Node implements SLIP10NodeInterface { publicKey, curve, }: SLIP10NodeConstructorOptions, + cryptographicFunctions: CryptographicFunctions = {}, constructorGuard?: symbol, ) { assert( @@ -393,6 +426,7 @@ export class SLIP10Node implements SLIP10NodeInterface { this.privateKeyBytes = privateKey; this.publicKeyBytes = publicKey; this.curve = curve; + this.#cryptographicFunctions = cryptographicFunctions; Object.freeze(this); } @@ -491,6 +525,7 @@ export class SLIP10Node implements SLIP10NodeInterface { publicKey: this.publicKeyBytes, curve: this.curve, }, + this.#cryptographicFunctions, SLIP10Node.#constructorGuard, ); } @@ -506,10 +541,13 @@ export class SLIP10Node implements SLIP10NodeInterface { * @returns The {@link SLIP10Node} corresponding to the derived child key. */ public async derive(path: SLIP10PathTuple): Promise { - return await deriveChildNode({ - path, - node: this, - }); + return await deriveChildNode( + { + path, + node: this, + }, + this.#cryptographicFunctions, + ); } // This is documented in the interface of this class. @@ -638,12 +676,14 @@ type DeriveChildNodeArgs = { * @param options - The options to use when deriving the child key. * @param options.node - The node to derive from. * @param options.path - The path to the child node / key. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived key and depth. */ -export async function deriveChildNode({ - path, - node, -}: DeriveChildNodeArgs): Promise { +export async function deriveChildNode( + { path, node }: DeriveChildNodeArgs, + cryptographicFunctions?: CryptographicFunctions, +): Promise { if (path.length === 0) { throw new Error( 'Invalid HD tree derivation path: Deriving a path of length 0 is not defined.', @@ -655,9 +695,12 @@ export async function deriveChildNode({ const newDepth = node.depth + path.length; validateBIP32Depth(newDepth); - return await deriveKeyFromPath({ - path, - node, - depth: newDepth, - }); + return await deriveKeyFromPath( + { + path, + node, + depth: newDepth, + }, + cryptographicFunctions, + ); } diff --git a/src/cryptography.test.ts b/src/cryptography.test.ts new file mode 100644 index 00000000..75909c29 --- /dev/null +++ b/src/cryptography.test.ts @@ -0,0 +1,135 @@ +import { bytesToHex } from '@metamask/utils'; +import { webcrypto } from 'crypto'; + +import { + hmacSha512, + keccak256, + pbkdf2Sha512, + ripemd160, + sha256, +} from './cryptography'; +import * as utils from './utils'; + +// Node.js <20 doesn't have `globalThis.crypto`, so we need to define it. +// TODO: Remove this once we drop support for Node.js <20. +Object.defineProperty(globalThis, 'crypto', { value: webcrypto }); + +describe('hmacSha512', () => { + it('returns the HMAC-SHA-512 when using a custom implementation', async () => { + const key = new Uint8Array(32); + const data = new Uint8Array(32); + + const hash = new Uint8Array(64).fill(1); + const customHmacSha512 = jest.fn().mockResolvedValue(hash); + + const result = await hmacSha512(key, data, { + hmacSha512: customHmacSha512, + }); + + expect(result).toBe(hash); + expect(customHmacSha512).toHaveBeenCalledWith(key, data); + }); + + it('returns the HMAC-SHA-512 when using the Web Crypto API', async () => { + const key = new Uint8Array(32); + const data = new Uint8Array(32); + + const result = await hmacSha512(key, data); + expect(bytesToHex(result)).toBe( + '0xbae46cebebbb90409abc5acf7ac21fdb339c01ce15192c52fb9e8aa11a8de9a4ea15a045f2be245fbb98916a9ae81b353e33b9c42a55380c5158241daeb3c6dd', + ); + }); + + it('returns the HMAC-SHA-512 when using the fallback', async () => { + jest.spyOn(utils, 'isWebCryptoSupported').mockReturnValueOnce(false); + + const key = new Uint8Array(32); + const data = new Uint8Array(32); + + const result = await hmacSha512(key, data); + expect(bytesToHex(result)).toBe( + '0xbae46cebebbb90409abc5acf7ac21fdb339c01ce15192c52fb9e8aa11a8de9a4ea15a045f2be245fbb98916a9ae81b353e33b9c42a55380c5158241daeb3c6dd', + ); + }); +}); + +describe('keccak256', () => { + it('returns the keccak-256 hash of the data', () => { + const data = new Uint8Array(32).fill(1); + const hash = keccak256(data); + + expect(bytesToHex(hash)).toBe( + '0xcebc8882fecbec7fb80d2cf4b312bec018884c2d66667c67a90508214bd8bafc', + ); + }); +}); + +describe('pbkdf2Sha512', () => { + it('returns the PBKDF2-SHA-512 when using a custom implementation', async () => { + const password = new Uint8Array(32); + const salt = new Uint8Array(32); + const iterations = 1000; + const keyLength = 64; + + const hash = new Uint8Array(64).fill(1); + const customPbkdf2Sha512 = jest.fn().mockResolvedValue(hash); + + const result = await pbkdf2Sha512(password, salt, iterations, keyLength, { + pbkdf2Sha512: customPbkdf2Sha512, + }); + + expect(result).toBe(hash); + expect(customPbkdf2Sha512).toHaveBeenCalledWith( + password, + salt, + iterations, + keyLength, + ); + }); + + it('returns the PBKDF2-SHA-512 when using the Web Crypto API', async () => { + const password = new Uint8Array(32); + const salt = new Uint8Array(32); + const iterations = 1000; + const keyLength = 64; + + const result = await pbkdf2Sha512(password, salt, iterations, keyLength); + expect(bytesToHex(result)).toBe( + '0xab3d65e9e6341a924c752a77b8dc6b78f1e6db5d31df7dd0cc534039dd9662a97bcaf0b959fe78248a49859c7952ddb25d66840f052b27ef1ab60b9446c0c9fd', + ); + }); + + it('returns the PBKDF2-SHA-512 when using the fallback', async () => { + jest.spyOn(utils, 'isWebCryptoSupported').mockReturnValueOnce(false); + + const password = new Uint8Array(32); + const salt = new Uint8Array(32); + const iterations = 1000; + const keyLength = 64; + + const result = await pbkdf2Sha512(password, salt, iterations, keyLength); + expect(bytesToHex(result)).toBe( + '0xab3d65e9e6341a924c752a77b8dc6b78f1e6db5d31df7dd0cc534039dd9662a97bcaf0b959fe78248a49859c7952ddb25d66840f052b27ef1ab60b9446c0c9fd', + ); + }); +}); + +describe('ripemd160', () => { + it('returns the RIPEMD-160 hash of the data', () => { + const data = new Uint8Array(32).fill(1); + const hash = ripemd160(data); + + expect(bytesToHex(hash)).toBe('0x422d0010f16ae8539c53eb57a912890244a9eb5a'); + }); +}); + +describe('sha256', () => { + it('returns the SHA-256 hash of the data', () => { + const data = new Uint8Array(32).fill(1); + const hash = sha256(data); + + expect(bytesToHex(hash)).toBe( + '0x72cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793', + ); + }); +}); diff --git a/src/cryptography.ts b/src/cryptography.ts new file mode 100644 index 00000000..1e32c306 --- /dev/null +++ b/src/cryptography.ts @@ -0,0 +1,178 @@ +import { hmac as nobleHmac } from '@noble/hashes/hmac'; +import { pbkdf2Async as noblePbkdf2 } from '@noble/hashes/pbkdf2'; +import { ripemd160 as nobleRipemd160 } from '@noble/hashes/ripemd160'; +import { sha256 as nobleSha256 } from '@noble/hashes/sha256'; +import { keccak_256 as nobleKeccak256 } from '@noble/hashes/sha3'; +import { sha512 as nobleSha512 } from '@noble/hashes/sha512'; + +import { isWebCryptoSupported } from './utils'; + +export type CryptographicFunctions = { + /** + * Compute the HMAC-SHA-512 of the given data using the given key. + * + * @param key - The key to use. + * @param data - The data to hash. + * @returns The HMAC-SHA-512 of the data. + */ + hmacSha512?: (key: Uint8Array, data: Uint8Array) => Promise; + + /** + * Compute the PBKDF2 of the given password, salt, iterations, and key length. + * The hash function used is SHA-512. + * + * @param password - The password to hash. + * @param salt - The salt to use. + * @param iterations - The number of iterations. + * @param keyLength - The desired key length in bytes. + * @returns The PBKDF2 of the password. + */ + pbkdf2Sha512?: ( + password: Uint8Array, + salt: Uint8Array, + iterations: number, + keyLength: number, + ) => Promise; +}; + +/** + * Compute the HMAC-SHA-512 of the given data using the given key. + * + * This function uses the Web Crypto API if available, falling back to a + * JavaScript implementation if not. + * + * @param key - The key to use. + * @param data - The data to hash. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. + * @returns The HMAC-SHA-512 of the data. + */ +export async function hmacSha512( + key: Uint8Array, + data: Uint8Array, + cryptographicFunctions: CryptographicFunctions = {}, +): Promise { + if (cryptographicFunctions.hmacSha512) { + return await cryptographicFunctions.hmacSha512(key, data); + } + + if (isWebCryptoSupported()) { + /* eslint-disable no-restricted-globals */ + const subtleKey = await crypto.subtle.importKey( + 'raw', + key, + { name: 'HMAC', hash: 'SHA-512' }, + false, + ['sign'], + ); + + const result = await crypto.subtle.sign('HMAC', subtleKey, data); + return new Uint8Array(result); + /* eslint-enable no-restricted-globals */ + } + + return nobleHmac(nobleSha512, key, data); +} + +/** + * Compute the Keccak-256 of the given data synchronously. + * + * Right now this is just a wrapper around `keccak256` from the `@noble/hashes` + * package, but it's here in case we want to change the implementation in the + * future to allow for asynchronous hashing. + * + * @param data - The data to hash. + * @returns The Keccak-256 of the data. + */ +export function keccak256(data: Uint8Array): Uint8Array { + return nobleKeccak256(data); +} + +/** + * Compute the PBKDF2 of the given password, salt, iterations, and key length. + * The hash function used is SHA-512. + * + * @param password - The password to hash. + * @param salt - The salt to use. + * @param iterations - The number of iterations. + * @param keyLength - The desired key length. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. + * @returns The PBKDF2 of the password. + */ +export async function pbkdf2Sha512( + password: Uint8Array, + salt: Uint8Array, + iterations: number, + keyLength: number, + cryptographicFunctions: CryptographicFunctions = {}, +): Promise { + if (cryptographicFunctions.pbkdf2Sha512) { + return await cryptographicFunctions.pbkdf2Sha512( + password, + salt, + iterations, + keyLength, + ); + } + + if (isWebCryptoSupported()) { + /* eslint-disable no-restricted-globals */ + const key = await crypto.subtle.importKey( + 'raw', + password, + { name: 'PBKDF2' }, + false, + ['deriveBits'], + ); + + const derivedBits = await crypto.subtle.deriveBits( + { + name: 'PBKDF2', + salt, + iterations, + hash: { name: 'SHA-512' }, + }, + key, + // `keyLength` is the number of bytes, but `deriveBits` expects the + // number of bits, so we multiply by 8. + keyLength * 8, + ); + + return new Uint8Array(derivedBits); + /* eslint-enable no-restricted-globals */ + } + + return await noblePbkdf2(nobleSha512, password, salt, { + c: iterations, + dkLen: keyLength, + }); +} + +/** + * Compute the RIPEMD-160 of the given data. + * + * Right now this is just a wrapper around `ripemd160` from the `@noble/hashes` + * package, but it's here in case we want to change the implementation in the + * future to allow for asynchronous hashing. + * + * @param data - The data to hash. + * @returns The RIPEMD-160 of the data. + */ +export function ripemd160(data: Uint8Array): Uint8Array { + return nobleRipemd160(data); +} + +/** + * Compute the SHA-256 of the given data synchronously. + * + * Right now this is just a wrapper around `sha256` from the `@noble/hashes` + * package, but it's here in case we want to change the implementation in the + * future to allow for asynchronous hashing. + * + * @param data - The data to hash. + * @returns The SHA-256 of the data. + */ +export function sha256(data: Uint8Array): Uint8Array { + return nobleSha256(data); +} diff --git a/src/derivation.ts b/src/derivation.ts index acd6ebab..373ce604 100755 --- a/src/derivation.ts +++ b/src/derivation.ts @@ -10,6 +10,7 @@ import { SLIP_10_PATH_REGEX, CIP_3_PATH_REGEX, } from './constants'; +import type { CryptographicFunctions } from './cryptography'; import type { SupportedCurve } from './curves'; import { getCurveByName } from './curves'; import type { Deriver } from './derivers'; @@ -63,10 +64,13 @@ type DeriveKeyFromPathArgs = * BIP-39 seed phrases must be lowercase, space-delimited, and 12-24 words long. * @param args.node - The node to derive from. * @param args.depth - The depth of the segment. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived key. */ export async function deriveKeyFromPath( args: DeriveKeyFromPathArgs, + cryptographicFunctions?: CryptographicFunctions, ): Promise { const { path, depth = path.length } = args; @@ -112,21 +116,27 @@ export async function deriveKeyFromPath( assert(hasDeriver(pathType), `Unknown derivation type: "${pathType}".`); const deriver = derivers[pathType] as Deriver; - return await deriver.deriveChildKey({ - path: pathPart, - node: derivedNode, - curve: getCurveByName(curve), - }); + return await deriver.deriveChildKey( + { + path: pathPart, + node: derivedNode, + curve: getCurveByName(curve), + }, + cryptographicFunctions, + ); } // Only the first path segment can be a Uint8Array. assert(index === 0, getMalformedError()); - return await derivers.bip39.deriveChildKey({ - path: pathNode, - node: derivedNode, - curve: getCurveByName(curve), - }); + return await derivers.bip39.deriveChildKey( + { + path: pathNode, + node: derivedNode, + curve: getCurveByName(curve), + }, + cryptographicFunctions, + ); }, Promise.resolve(node as SLIP10Node)); } diff --git a/src/derivers/bip32.ts b/src/derivers/bip32.ts index 69b5aa8d..f7db7008 100755 --- a/src/derivers/bip32.ts +++ b/src/derivers/bip32.ts @@ -1,8 +1,9 @@ import { assert } from '@metamask/utils'; -import { keccak_256 as keccak256 } from '@noble/hashes/sha3'; import type { DeriveChildKeyArgs } from '.'; import { BYTES_KEY_LENGTH } from '../constants'; +import type { CryptographicFunctions } from '../cryptography'; +import { keccak256 } from '../cryptography'; import { secp256k1 } from '../curves'; import type { SLIP10Node } from '../SLIP10Node'; import { isValidBytesKey, validateBIP32Index } from '../utils'; @@ -63,17 +64,20 @@ export function publicKeyToEthAddress(key: Uint8Array) { * @param options.path - The derivation path part to derive. * @param options.node - The node to derive from. * @param options.curve - The curve to use for derivation. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived child key as a {@link SLIP10Node}. */ export async function deriveChildKey( options: DeriveChildKeyArgs, + cryptographicFunctions?: CryptographicFunctions, ): Promise { assert( options.curve.name === 'secp256k1', 'Invalid curve: Only secp256k1 is supported by BIP-32.', ); - return sharedDeriveChildKey(options, handleError); + return sharedDeriveChildKey(options, handleError, cryptographicFunctions); } /** @@ -82,12 +86,15 @@ export async function deriveChildKey( * * @param _ - The error that was thrown. * @param options - The options for deriving a child key. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The options for deriving a child key with the child index * incremented by one. */ async function handleError( _: unknown, options: DeriveNodeArgs, + cryptographicFunctions?: CryptographicFunctions, ): Promise { const { childIndex, privateKey, publicKey, isHardened, curve, chainCode } = options; @@ -102,10 +109,13 @@ async function handleError( curve, }); - const newEntropy = generateEntropy({ - chainCode, - extension: secretExtension, - }); + const newEntropy = await generateEntropy( + { + chainCode, + extension: secretExtension, + }, + cryptographicFunctions, + ); return { ...options, @@ -119,7 +129,7 @@ async function handleError( childIndex: childIndex + 1, }); - const newEntropy = generateEntropy({ + const newEntropy = await generateEntropy({ chainCode, extension: publicExtension, }); diff --git a/src/derivers/bip39.test.ts b/src/derivers/bip39.test.ts index 23379982..c19211d9 100644 --- a/src/derivers/bip39.test.ts +++ b/src/derivers/bip39.test.ts @@ -1,19 +1,97 @@ import { assert, bigIntToBytes, + bytesToHex, concatBytes, hexToBytes, } from '@metamask/utils'; -import * as hmacModule from '@noble/hashes/hmac'; import fixtures from '../../test/fixtures'; +import * as cryptography from '../cryptography'; import { secp256k1, ed25519Bip32, type Curve } from '../curves'; +import { mnemonicPhraseToBytes } from '../utils'; import { entropyToCip3MasterNode, createBip39KeyFromSeed, deriveChildKey, + mnemonicToSeed, + mnemonicToEntropy, } from './bip39'; +const TEST_MNEMONIC_PHRASE = + 'pill frown erosion humor invest inquiry rich garment seek such mention punch'; + +describe('mnemonicToSeed', () => { + describe('without passphrase', () => { + const seed = new Uint8Array([ + 213, 198, 189, 89, 252, 121, 48, 207, 56, 105, 8, 152, 129, 116, 186, 218, + 26, 71, 225, 55, 201, 122, 153, 178, 5, 235, 40, 132, 179, 248, 166, 147, + 18, 128, 248, 25, 184, 206, 113, 170, 71, 235, 73, 144, 0, 134, 22, 244, + 18, 229, 222, 139, 246, 28, 123, 131, 16, 215, 191, 216, 252, 159, 213, + 235, + ]); + + it('generates the right seed for a string mnemonic phrase', async () => { + const generatedSeed = await mnemonicToSeed(TEST_MNEMONIC_PHRASE); + expect(generatedSeed).toStrictEqual(seed); + }); + + it('generates the right seed for a Uint8Array mnemonic phrase', async () => { + const mnemonic = mnemonicPhraseToBytes(TEST_MNEMONIC_PHRASE); + const generatedSeed = await mnemonicToSeed(mnemonic); + expect(generatedSeed).toStrictEqual(seed); + }); + + it('throws if the length of the mnemonic phrase is invalid', async () => { + await expect(mnemonicToSeed('test')).rejects.toThrow( + 'Invalid mnemonic phrase: The mnemonic phrase must consist of 12, 15, 18, 21, or 24 words.', + ); + }); + + it('throws if the mnemonic phrase contains invalid words', async () => { + await expect( + mnemonicToSeed( + 'test test test test test test test test test invalid mnemonic phrase', + ), + ).rejects.toThrow( + 'Invalid mnemonic phrase: The mnemonic phrase contains an unknown word.', + ); + }); + }); + + describe('with passphrase', () => { + const passphrase = 'passphrase'; + const seed = new Uint8Array([ + 180, 211, 212, 196, 151, 216, 92, 25, 11, 35, 14, 186, 80, 80, 141, 156, + 245, 11, 25, 118, 50, 75, 80, 36, 116, 113, 11, 112, 36, 86, 70, 188, 92, + 156, 172, 167, 83, 159, 47, 149, 92, 107, 130, 66, 39, 251, 34, 169, 115, + 143, 121, 110, 166, 28, 221, 93, 252, 165, 155, 127, 19, 138, 107, 135, + ]); + + it('generates the right seed for a string mnemonic phrase', async () => { + const generatedSeed = await mnemonicToSeed( + TEST_MNEMONIC_PHRASE, + passphrase, + ); + + expect(generatedSeed).toStrictEqual(seed); + }); + + it('generates the right seed for a Uint8Array mnemonic phrase', async () => { + const mnemonic = mnemonicPhraseToBytes(TEST_MNEMONIC_PHRASE); + const generatedSeed = await mnemonicToSeed(mnemonic, passphrase); + expect(generatedSeed).toStrictEqual(seed); + }); + }); +}); + +describe('mnemonicToEntropy', () => { + it('converts a mnemonic phrase to entropy', async () => { + const entropy = await mnemonicToEntropy(TEST_MNEMONIC_PHRASE); + expect(bytesToHex(entropy)).toBe('0xa4cbb132b7875ee9ae52ffc31b0e2c56'); + }); +}); + describe('createBip39KeyFromSeed', () => { const RANDOM_SEED = hexToBytes( '0xea82e6ee9d319c083007d0b011a37b0e480ae02417a988ac90355abd53cd04fc', @@ -37,7 +115,9 @@ describe('createBip39KeyFromSeed', () => { it('throws if the private key is zero', async () => { // Mock the hmac function to return a zero private key. - jest.spyOn(hmacModule, 'hmac').mockImplementation(() => new Uint8Array(64)); + jest + .spyOn(cryptography, 'hmacSha512') + .mockResolvedValueOnce(new Uint8Array(64)); await expect( createBip39KeyFromSeed(RANDOM_SEED, secp256k1), @@ -57,10 +137,8 @@ describe('createBip39KeyFromSeed', () => { // Mock the hmac function to return a private key larger than the curve order. jest - .spyOn(hmacModule, 'hmac') - .mockImplementation(() => - concatBytes([privateKey, new Uint8Array(32)]), - ); + .spyOn(cryptography, 'hmacSha512') + .mockResolvedValueOnce(concatBytes([privateKey, new Uint8Array(32)])); await expect( createBip39KeyFromSeed(RANDOM_SEED, secp256k1), diff --git a/src/derivers/bip39.ts b/src/derivers/bip39.ts index 5981f2f1..076ad8d8 100755 --- a/src/derivers/bip39.ts +++ b/src/derivers/bip39.ts @@ -1,17 +1,188 @@ -import { mnemonicToEntropy, mnemonicToSeed } from '@metamask/scure-bip39'; import { wordlist as englishWordlist } from '@metamask/scure-bip39/dist/wordlists/english'; -import { assert } from '@metamask/utils'; -import { hmac } from '@noble/hashes/hmac'; -import { pbkdf2 } from '@noble/hashes/pbkdf2'; -import { sha512 } from '@noble/hashes/sha512'; +import { assert, stringToBytes } from '@metamask/utils'; import type { DeriveChildKeyArgs } from '.'; import type { BIP39StringNode } from '../constants'; import { BYTES_KEY_LENGTH } from '../constants'; +import type { CryptographicFunctions } from '../cryptography'; +import { sha256, hmacSha512, pbkdf2Sha512 } from '../cryptography'; import type { Curve } from '../curves'; import { SLIP10Node } from '../SLIP10Node'; import { getFingerprint } from '../utils'; +const MNEMONIC_PHRASE_LENGTHS = [12, 15, 18, 21, 24]; + +/** + * Validate a BIP-39 mnemonic phrase. The phrase must: + * + * - Consist of 12, 15, 18, 21, or 24 words. + * - Contain only words from the English wordlist. + * + * @param mnemonicPhrase - The mnemonic phrase to validate. + * @throws If the mnemonic phrase is invalid. + */ +function validateMnemonicPhrase(mnemonicPhrase: string) { + const words = mnemonicPhrase.split(' '); + + assert( + MNEMONIC_PHRASE_LENGTHS.includes(words.length), + `Invalid mnemonic phrase: The mnemonic phrase must consist of 12, 15, 18, 21, or 24 words.`, + ); + + assert( + words.every((word) => englishWordlist.includes(word)), + 'Invalid mnemonic phrase: The mnemonic phrase contains an unknown word.', + ); +} + +/** + * Get the mnemonic phrase from a mnemonic phrase or a `Uint8Array` of indices. + * If the mnemonic is a `Uint8Array`, it is assumed to contain the indices of + * the words in the wordlist. + * + * @param mnemonic - The mnemonic phrase or indices. + * @param wordlist - The wordlist to use. + * @returns The mnemonic phrase. + */ +function getMnemonicPhrase( + mnemonic: string | Uint8Array, + wordlist: string[], +): string { + if (typeof mnemonic === 'string') { + validateMnemonicPhrase(mnemonic); + return mnemonic; + } + + const mnemonicString = Array.from(new Uint16Array(mnemonic.buffer)) + .map((i) => wordlist[i]) + .join(' '); + + validateMnemonicPhrase(mnemonicString); + + return mnemonicString; +} + +/** + * Convert a BIP-39 mnemonic phrase to a seed. + * + * @param mnemonic - The BIP-39 mnemonic phrase to convert. If the mnemonic is a + * `Uint8Array`, it is assumed to contain the indices of the words in the + * English wordlist. + * @param passphrase - The passphrase to use. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. + */ +export async function mnemonicToSeed( + mnemonic: string | Uint8Array, + passphrase = '', + cryptographicFunctions?: CryptographicFunctions, +) { + const salt = `mnemonic${passphrase}`.normalize('NFKD'); + const mnemonicString = getMnemonicPhrase(mnemonic, englishWordlist); + + return await pbkdf2Sha512( + stringToBytes(mnemonicString), + stringToBytes(salt), + 2048, + 64, + cryptographicFunctions, + ); +} + +/** + * Convert a `Uint8Array` of bytes to an array of bits. + * + * @param bytes - The `Uint8Array` to convert. + * @returns The array of bits. + */ +function bytesToBits(bytes: Uint8Array): number[] { + const bits: number[] = []; + for (const byte of bytes) { + for (let i = 7; i >= 0; i--) { + // eslint-disable-next-line no-bitwise + bits.push((byte >> i) & 1); + } + } + + return bits; +} + +/** + * Convert an array of bits to a `Uint8Array`. + * + * @param bits - The array of bits. + * @returns The `Uint8Array`. + */ +export function bitsToBytes(bits: number[]): Uint8Array { + if (bits.length % 8 !== 0) { + throw new Error('The number of bits must be a multiple of 8.'); + } + + const bytes = new Uint8Array(bits.length / 8); + + for (let i = 0; i < bits.length; i += 8) { + let byte = 0; + for (let j = 0; j < 8; j++) { + const value = bits[i + j]; + assert(value === 0 || value === 1, 'Invalid bit value.'); + + // eslint-disable-next-line no-bitwise + byte |= value << (7 - j); + } + bytes[i / 8] = byte; + } + + return bytes; +} + +/** + * Get the checksum for the entropy. + * + * @param entropy - The entropy. + * @returns The checksum as an array of bits. + */ +function getChecksum(entropy: Uint8Array): number[] { + const hash = sha256(entropy); + return bytesToBits(hash).slice(0, entropy.length / 4); +} + +/** + * Convert a BIP-39 mnemonic phrase to entropy. + * + * @param mnemonic - The BIP-39 mnemonic phrase to convert. + * @returns The entropy. + */ +export async function mnemonicToEntropy( + mnemonic: string | Uint8Array, +): Promise { + const words = getMnemonicPhrase(mnemonic, englishWordlist).split(' '); + const bits = words.flatMap((word) => { + const index = englishWordlist.indexOf(word); + const binaryBits = []; + + for (let i = 10; i >= 0; i--) { + // eslint-disable-next-line no-bitwise + binaryBits.push((index >> i) & 1); + } + + return binaryBits; + }); + + const checksumLength = bits.length % 32; + const entropy = bits.slice(0, -checksumLength); + const checksum = bits.slice(-checksumLength); + + const newChecksum = getChecksum(bitsToBytes(entropy)); + + assert( + checksum.length === newChecksum.length && + checksum.every((bit, index) => bit === newChecksum[index]), + 'Invalid checksum: The checksum does not match the entropy.', + ); + + return bitsToBytes(entropy); +} + /** * Convert a BIP-39 mnemonic phrase to a multi path. * @@ -28,22 +199,26 @@ export function bip39MnemonicToMultipath(mnemonic: string): BIP39StringNode { * @param options - The options for creating the node. * @param options.path - The multi path. * @param options.curve - The curve to use for derivation. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The node. */ -export async function deriveChildKey({ - path, - curve, -}: DeriveChildKeyArgs): Promise { +export async function deriveChildKey( + { path, curve }: DeriveChildKeyArgs, + cryptographicFunctions?: CryptographicFunctions, +): Promise { switch (curve.masterNodeGenerationSpec) { case 'slip10': return createBip39KeyFromSeed( - await mnemonicToSeed(path, englishWordlist), + await mnemonicToSeed(path, '', cryptographicFunctions), curve, + cryptographicFunctions, ); case 'cip3': return entropyToCip3MasterNode( - mnemonicToEntropy(path, englishWordlist), + await mnemonicToEntropy(path), curve, + cryptographicFunctions, ); default: throw new Error('Unsupported master node generation spec.'); @@ -55,19 +230,22 @@ export async function deriveChildKey({ * * @param seed - The cryptographic seed bytes. * @param curve - The curve to use. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns An object containing the corresponding BIP-39 master key and chain * code. */ export async function createBip39KeyFromSeed( seed: Uint8Array, curve: Extract, + cryptographicFunctions?: CryptographicFunctions, ): Promise { assert( seed.length >= 16 && seed.length <= 64, 'Invalid seed: The seed must be between 16 and 64 bytes long.', ); - const key = hmac(sha512, curve.secret, seed); + const key = await hmacSha512(curve.secret, seed, cryptographicFunctions); const privateKey = key.slice(0, BYTES_KEY_LENGTH); const chainCode = key.slice(BYTES_KEY_LENGTH); @@ -81,15 +259,18 @@ export async function createBip39KeyFromSeed( curve.compressedPublicKeyLength, ); - return SLIP10Node.fromExtendedKey({ - privateKey, - chainCode, - masterFingerprint, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: curve.name, - }); + return SLIP10Node.fromExtendedKey( + { + privateKey, + chainCode, + masterFingerprint, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: curve.name, + }, + cryptographicFunctions, + ); } /** @@ -100,21 +281,27 @@ export async function createBip39KeyFromSeed( * * @param entropy - The entropy value. * @param curve - The curve to use. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The root key pair consisting of 64-byte private key and 32-byte chain code. */ export async function entropyToCip3MasterNode( entropy: Uint8Array, curve: Extract, + cryptographicFunctions?: CryptographicFunctions, ): Promise { assert( entropy.length >= 16 && entropy.length <= 64, 'Invalid entropy: The entropy must be between 16 and 64 bytes long.', ); - const rootNode = pbkdf2(sha512, curve.secret, entropy, { - c: 4096, - dkLen: 96, - }); + const rootNode = await pbkdf2Sha512( + curve.secret, + entropy, + 4096, + 96, + cryptographicFunctions, + ); // Consistent with the Icarus derivation scheme. // https://github.com/cardano-foundation/CIPs/blob/09d7d8ee1bd64f7e6b20b5a6cae088039dce00cb/CIP-0003/Icarus.md @@ -134,13 +321,16 @@ export async function entropyToCip3MasterNode( curve.compressedPublicKeyLength, ); - return SLIP10Node.fromExtendedKey({ - privateKey, - chainCode, - masterFingerprint, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: curve.name, - }); + return SLIP10Node.fromExtendedKey( + { + privateKey, + chainCode, + masterFingerprint, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: curve.name, + }, + cryptographicFunctions, + ); } diff --git a/src/derivers/cip3.ts b/src/derivers/cip3.ts index 31a3daeb..95357f42 100644 --- a/src/derivers/cip3.ts +++ b/src/derivers/cip3.ts @@ -2,6 +2,7 @@ import { assert, bytesToHex, concatBytes, hexToBytes } from '@metamask/utils'; import type { DeriveChildKeyArgs } from '.'; import { BIP_32_HARDENED_OFFSET } from '../constants'; +import type { CryptographicFunctions } from '../cryptography'; import { type Curve, mod } from '../curves'; import { SLIP10Node } from '../SLIP10Node'; import { numberToUint32 } from '../utils'; @@ -145,21 +146,24 @@ const Z_TAGS = { }; /** - * Derives a private child key. + * Derive a private child key. * - * Following "Section V. BIP32-ED25519: SPECIFICATION, C.1,2" in https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf. + * Following "Section V. BIP32-ED25519: SPECIFICATION, C.1,2" in + * https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf. * - * @param param1 - The parameters for deriving a child key. - * @param param1.parentNode - The parent node containing private key, chain code, and public key. - * @param param1.childIndex - The index of the child key. - * @param param1.isHardened - Indicates if the child key is hardened. + * @param options - The parameters for deriving a child key. + * @param options.parentNode - The parent node containing private key, chain + * code, and public key. + * @param options.childIndex - The index of the child key. + * @param options.isHardened - Indicates if the child key is hardened. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived child key. */ -export const derivePrivateKey = async ({ - parentNode, - childIndex, - isHardened, -}: DeriveWithPrivateArgs) => { +export const derivePrivateKey = async ( + { parentNode, childIndex, isHardened }: DeriveWithPrivateArgs, + cryptographicFunctions?: CryptographicFunctions, +) => { // extension = i >= 2^31 ? (0x00||kp||i) : (0x02||Ap||i) const extension = isHardened ? getKeyExtension( @@ -170,10 +174,13 @@ export const derivePrivateKey = async ({ : getKeyExtension(Z_TAGS.normal, parentNode.publicKeyBytes, childIndex); // entropy = Fcp(extension) - const entropy = generateEntropy({ - chainCode: parentNode.chainCodeBytes, - extension, - }); + const entropy = await generateEntropy( + { + chainCode: parentNode.chainCodeBytes, + extension, + }, + cryptographicFunctions, + ); const zl = entropy.subarray(0, 32); const zr = entropy.subarray(32); @@ -202,21 +209,28 @@ const CHAIN_CODE_TAGS = { }; /** - * Derives a child chainCode. + * Derive a child chainCode. * - * Following "Section V. BIP32-ED25519: SPECIFICATION, C.3" in https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf. + * Following "Section V. BIP32-ED25519: SPECIFICATION, C.3" in + * https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf. * - * @param param1 - The parameters for deriving a child chainCode. - * @param param1.parentNode - The parent node containing optionally a private key, chain code, and public key. - * @param param1.childIndex - The index of the child key. - * @param param1.isHardened - Indicates if the child key is hardened. + * @param options - The parameters for deriving a child chainCode. + * @param options.parentNode - The parent node containing optionally a private + * key, chain code, and public key. + * @param options.childIndex - The index of the child key. + * @param options.isHardened - Indicates if the child key is hardened. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived child chainCode. */ -export const deriveChainCode = async ({ - parentNode, - childIndex, - isHardened, -}: DeriveWithPrivateArgs | DeriveWithoutPrivateArgs) => { +export const deriveChainCode = async ( + { + parentNode, + childIndex, + isHardened, + }: DeriveWithPrivateArgs | DeriveWithoutPrivateArgs, + cryptographicFunctions?: CryptographicFunctions, +) => { // extension = i >= 2^31 ? (0x01||kp||i) : (0x03||Ap||i) const extension = isHardened ? getKeyExtension( @@ -231,10 +245,13 @@ export const deriveChainCode = async ({ ); // entropy = Fcp(extension) - const entropy = generateEntropy({ - chainCode: parentNode.chainCodeBytes, - extension, - }); + const entropy = await generateEntropy( + { + chainCode: parentNode.chainCodeBytes, + extension, + }, + cryptographicFunctions, + ); return entropy.subarray(32); }; @@ -248,21 +265,23 @@ type DerivePublicKeyArgs = DeriveWithoutPrivateArgs & { }; /** - * Derives a public key. + * Derive a public key. * - * Following "Section V. BIP32-ED25519: SPECIFICATION, D" in https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf. + * Following "Section V. BIP32-ED25519: SPECIFICATION, D" in + * https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf. * - * @param param1 - The parameters for deriving a child public key. - * @param param1.parentNode - The parent node containing chain code, and public key. - * @param param1.childIndex - The index of the child key. - * @param param1.curve - Derivation curve. + * @param options - The parameters for deriving a child public key. + * @param options.parentNode - The parent node containing chain code, and public key. + * @param options.childIndex - The index of the child key. + * @param options.curve - Derivation curve. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived child public key. */ -export const derivePublicKey = async ({ - parentNode, - childIndex, - curve, -}: DerivePublicKeyArgs) => { +export const derivePublicKey = async ( + { parentNode, childIndex, curve }: DerivePublicKeyArgs, + cryptographicFunctions?: CryptographicFunctions, +) => { // extension = (0x02||Ap||i) const extension = getKeyExtension( PUBLIC_KEY_TAGS.normal, @@ -271,10 +290,13 @@ export const derivePublicKey = async ({ ); // entropy = Fcp(extension) - const entropy = generateEntropy({ - chainCode: parentNode.chainCodeBytes, - extension, - }); + const entropy = await generateEntropy( + { + chainCode: parentNode.chainCodeBytes, + extension, + }, + cryptographicFunctions, + ); const zl = entropy.slice(0, 32); @@ -296,10 +318,13 @@ type Cip3DeriveChildKeyArgs = DeriveChildKeyArgs & { * Derive a SLIP-10 child key with a given path from a parent key. * * @param options - The options for deriving a child key. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns SLIP10Node. */ export async function deriveChildKey( options: Cip3DeriveChildKeyArgs, + cryptographicFunctions?: CryptographicFunctions, ): Promise { const { curve, node, path } = options; validateNode(node); @@ -330,27 +355,36 @@ export async function deriveChildKey( publicKeyBytes, }; - const privateKey = await derivePrivateKey({ - parentNode, - childIndex, - isHardened, - }); + const privateKey = await derivePrivateKey( + { + parentNode, + childIndex, + isHardened, + }, + cryptographicFunctions, + ); - const chainCode = await deriveChainCode({ - parentNode, - childIndex, - isHardened, - }); + const chainCode = await deriveChainCode( + { + parentNode, + childIndex, + isHardened, + }, + cryptographicFunctions, + ); - return SLIP10Node.fromExtendedKey({ - privateKey: bytesToHex(privateKey), - chainCode: bytesToHex(chainCode), - masterFingerprint, - depth: depth + 1, - parentFingerprint, - index: actualChildIndex, - curve: curve.name, - }); + return SLIP10Node.fromExtendedKey( + { + privateKey: bytesToHex(privateKey), + chainCode: bytesToHex(chainCode), + masterFingerprint, + depth: depth + 1, + parentFingerprint, + index: actualChildIndex, + curve: curve.name, + }, + cryptographicFunctions, + ); } assert( @@ -363,26 +397,35 @@ export async function deriveChildKey( publicKeyBytes, }; - const publicKey = await derivePublicKey({ - parentNode, - childIndex, - isHardened: false, - curve, - }); + const publicKey = await derivePublicKey( + { + parentNode, + childIndex, + isHardened: false, + curve, + }, + cryptographicFunctions, + ); - const chainCode = await deriveChainCode({ - parentNode, - childIndex, - isHardened: false, - }); + const chainCode = await deriveChainCode( + { + parentNode, + childIndex, + isHardened: false, + }, + cryptographicFunctions, + ); - return SLIP10Node.fromExtendedKey({ - publicKey: bytesToHex(publicKey), - chainCode: bytesToHex(chainCode), - masterFingerprint, - depth: depth + 1, - parentFingerprint, - index: actualChildIndex, - curve: curve.name, - }); + return SLIP10Node.fromExtendedKey( + { + publicKey: bytesToHex(publicKey), + chainCode: bytesToHex(chainCode), + masterFingerprint, + depth: depth + 1, + parentFingerprint, + index: actualChildIndex, + curve: curve.name, + }, + cryptographicFunctions, + ); } diff --git a/src/derivers/index.test.ts b/src/derivers/index.test.ts index fd290212..1b5f1ce2 100644 --- a/src/derivers/index.test.ts +++ b/src/derivers/index.test.ts @@ -1,7 +1,8 @@ -import { createBip39KeyFromSeed } from '.'; +import { createBip39KeyFromSeed, mnemonicToSeed } from '.'; describe('index', () => { it('has expected exports', () => { expect(createBip39KeyFromSeed).toBeDefined(); + expect(mnemonicToSeed).toBeDefined(); }); }); diff --git a/src/derivers/index.ts b/src/derivers/index.ts index 6f363e74..95c5882b 100644 --- a/src/derivers/index.ts +++ b/src/derivers/index.ts @@ -1,3 +1,4 @@ +import type { CryptographicFunctions } from '../cryptography'; import type { Curve } from '../curves'; import type { SLIP10Node } from '../SLIP10Node'; import * as bip32 from './bip32'; @@ -21,7 +22,10 @@ export type DeriveChildKeyArgs = { }; export type Deriver = { - deriveChildKey: (args: DeriveChildKeyArgs) => Promise; + deriveChildKey: ( + args: DeriveChildKeyArgs, + cryptographicFunctions?: CryptographicFunctions, + ) => Promise; }; export const derivers = { @@ -31,4 +35,4 @@ export const derivers = { cip3, }; -export { createBip39KeyFromSeed } from './bip39'; +export { createBip39KeyFromSeed, mnemonicToSeed } from './bip39'; diff --git a/src/derivers/shared.ts b/src/derivers/shared.ts index fcae861d..b3865734 100644 --- a/src/derivers/shared.ts +++ b/src/derivers/shared.ts @@ -4,11 +4,11 @@ import { concatBytes, hexToBytes, } from '@metamask/utils'; -import { hmac } from '@noble/hashes/hmac'; -import { sha512 } from '@noble/hashes/sha512'; import type { DeriveChildKeyArgs, DerivedKeys } from '.'; import { BIP_32_HARDENED_OFFSET, UNPREFIXED_PATH_REGEX } from '../constants'; +import type { CryptographicFunctions } from '../cryptography'; +import { hmacSha512 } from '../cryptography'; import type { Curve } from '../curves'; import { mod } from '../curves'; import { SLIP10Node } from '../SLIP10Node'; @@ -17,6 +17,7 @@ import { isValidBytesKey, numberToUint32 } from '../utils'; type ErrorHandler = ( error: unknown, options: DeriveNodeArgs, + cryptographicFunctions?: CryptographicFunctions, ) => Promise; /** @@ -31,11 +32,14 @@ type ErrorHandler = ( * @param options.curve - The curve to use for derivation. * @param handleError - A function that can handle errors that occur during * derivation. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived node. */ export async function deriveChildKey( { path, node, curve }: DeriveChildKeyArgs, handleError: ErrorHandler, + cryptographicFunctions?: CryptographicFunctions, ) { validateNode(node); @@ -59,10 +63,13 @@ export async function deriveChildKey( curve, }); - const entropy = generateEntropy({ - chainCode: node.chainCodeBytes, - extension: secretExtension, - }); + const entropy = await generateEntropy( + { + chainCode: node.chainCodeBytes, + extension: secretExtension, + }, + cryptographicFunctions, + ); return await deriveNode( { @@ -71,6 +78,7 @@ export async function deriveChildKey( ...args, }, handleError, + cryptographicFunctions, ); } @@ -79,10 +87,13 @@ export async function deriveChildKey( childIndex, }); - const entropy = generateEntropy({ - chainCode: node.chainCodeBytes, - extension: publicExtension, - }); + const entropy = await generateEntropy( + { + chainCode: node.chainCodeBytes, + extension: publicExtension, + }, + cryptographicFunctions, + ); return await deriveNode( { @@ -91,6 +102,7 @@ export async function deriveChildKey( ...args, }, handleError, + cryptographicFunctions, ); } @@ -139,14 +151,14 @@ type DeriveSecretExtensionArgs = { * @param options.masterFingerprint - The fingerprint of the master key. * @param options.curve - The curve to use for deriving the child key. * @param handleError - A function to handle errors during derivation. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived child key as {@link SLIP10Node}. */ async function deriveNode( options: DeriveNodeArgs, - handleError: ( - error: unknown, - args: DeriveNodeArgs, - ) => Promise, + handleError: ErrorHandler, + cryptographicFunctions?: CryptographicFunctions, ): Promise { const { privateKey, @@ -162,29 +174,39 @@ async function deriveNode( try { if (privateKey) { - return await derivePrivateChildKey({ + return await derivePrivateChildKey( + { + entropy, + privateKey, + depth, + masterFingerprint, + parentFingerprint, + childIndex, + isHardened, + curve, + }, + cryptographicFunctions, + ); + } + + return await derivePublicChildKey( + { entropy, - privateKey, + publicKey, depth, masterFingerprint, parentFingerprint, childIndex, - isHardened, curve, - }); - } - - return await derivePublicChildKey({ - entropy, - publicKey, - depth, - masterFingerprint, - parentFingerprint, - childIndex, - curve, - }); + }, + cryptographicFunctions, + ); } catch (error) { - return await deriveNode(await handleError(error, options), handleError); + return await deriveNode( + await handleError(error, options, cryptographicFunctions), + handleError, + cryptographicFunctions, + ); } } @@ -297,18 +319,23 @@ type DerivePrivateChildKeyArgs = { * @param args.childIndex - The child index to derive. * @param args.isHardened - Whether the child index is hardened. * @param args.curve - The curve to use for derivation. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived {@link SLIP10Node}. */ -async function derivePrivateChildKey({ - entropy, - privateKey, - depth, - masterFingerprint, - parentFingerprint, - childIndex, - isHardened, - curve, -}: DerivePrivateChildKeyArgs): Promise { +async function derivePrivateChildKey( + { + entropy, + privateKey, + depth, + masterFingerprint, + parentFingerprint, + childIndex, + isHardened, + curve, + }: DerivePrivateChildKeyArgs, + cryptographicFunctions?: CryptographicFunctions, +): Promise { const actualChildIndex = childIndex + (isHardened ? BIP_32_HARDENED_OFFSET : 0); @@ -319,15 +346,18 @@ async function derivePrivateChildKey({ curve, }); - return await SLIP10Node.fromExtendedKey({ - privateKey: childPrivateKey, - chainCode: childChainCode, - depth: depth + 1, - masterFingerprint, - parentFingerprint, - index: actualChildIndex, - curve: curve.name, - }); + return await SLIP10Node.fromExtendedKey( + { + privateKey: childPrivateKey, + chainCode: childChainCode, + depth: depth + 1, + masterFingerprint, + parentFingerprint, + index: actualChildIndex, + curve: curve.name, + }, + cryptographicFunctions, + ); } type GeneratePublicKeyArgs = { @@ -383,17 +413,22 @@ type DerivePublicChildKeyArgs = { * @param args.parentFingerprint - The fingerprint of the parent node. * @param args.childIndex - The child index to derive. * @param args.curve - The curve to use for derivation. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The derived {@link SLIP10Node}. */ -export async function derivePublicChildKey({ - entropy, - publicKey, - depth, - masterFingerprint, - parentFingerprint, - childIndex, - curve, -}: DerivePublicChildKeyArgs): Promise { +export async function derivePublicChildKey( + { + entropy, + publicKey, + depth, + masterFingerprint, + parentFingerprint, + childIndex, + curve, + }: DerivePublicChildKeyArgs, + cryptographicFunctions?: CryptographicFunctions, +): Promise { const { publicKey: childPublicKey, chainCode: childChainCode } = generatePublicKey({ publicKey, @@ -401,15 +436,18 @@ export async function derivePublicChildKey({ curve, }); - return await SLIP10Node.fromExtendedKey({ - publicKey: childPublicKey, - chainCode: childChainCode, - depth: depth + 1, - masterFingerprint, - parentFingerprint, - index: childIndex, - curve: curve.name, - }); + return await SLIP10Node.fromExtendedKey( + { + publicKey: childPublicKey, + chainCode: childChainCode, + depth: depth + 1, + masterFingerprint, + parentFingerprint, + index: childIndex, + curve: curve.name, + }, + cryptographicFunctions, + ); } /** @@ -462,10 +500,15 @@ type GenerateEntropyArgs = { * @param args - The arguments for generating entropy. * @param args.chainCode - The parent chain code bytes. * @param args.extension - The extension bytes. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The generated entropy bytes. */ -export function generateEntropy({ chainCode, extension }: GenerateEntropyArgs) { - return hmac(sha512, chainCode, extension); +export async function generateEntropy( + { chainCode, extension }: GenerateEntropyArgs, + cryptographicFunctions?: CryptographicFunctions, +) { + return await hmacSha512(chainCode, extension, cryptographicFunctions); } /** diff --git a/src/derivers/slip10.ts b/src/derivers/slip10.ts index 80f3dbf0..dee04b32 100755 --- a/src/derivers/slip10.ts +++ b/src/derivers/slip10.ts @@ -2,6 +2,7 @@ import { concatBytes } from '@metamask/utils'; import type { DeriveChildKeyArgs } from '.'; import { BIP_32_HARDENED_OFFSET } from '../constants'; +import type { CryptographicFunctions } from '../cryptography'; import type { SLIP10Node } from '../SLIP10Node'; import { numberToUint32 } from '../utils'; import { @@ -14,13 +15,20 @@ import type { DeriveNodeArgs } from './shared'; * Derive a SLIP-10 child key with a given path from a parent key. * * @param options - The options for deriving a child key. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns A tuple containing the derived private key, public key and chain * code. */ export async function deriveChildKey( options: DeriveChildKeyArgs, + cryptographicFunctions?: CryptographicFunctions, ): Promise { - return await sharedDeriveChildKey(options, handleError); + return await sharedDeriveChildKey( + options, + handleError, + cryptographicFunctions, + ); } /** @@ -28,11 +36,14 @@ export async function deriveChildKey( * * @param error - The error that occurred. * @param options - The options that were used for derivation. + * @param cryptographicFunctions - The cryptographic functions to use. If + * provided, these will be used instead of the built-in implementations. * @returns The new options to use for derivation. */ async function handleError( error: unknown, options: DeriveNodeArgs, + cryptographicFunctions?: CryptographicFunctions, ): Promise { const { curve, isHardened, childIndex, entropy, chainCode } = options; @@ -50,14 +61,17 @@ async function handleError( // generated as follows: // Key material (32 bytes), child chain code (32 bytes) = // HMAC-SHA512(parent chain code, 0x01 || chain code from invalid key || index). - const newEntropy = generateEntropy({ - chainCode, - extension: concatBytes([ - 0x01, - entropy.slice(32, 64), - numberToUint32(actualChildIndex), - ]), - }); + const newEntropy = await generateEntropy( + { + chainCode, + extension: concatBytes([ + 0x01, + entropy.slice(32, 64), + numberToUint32(actualChildIndex), + ]), + }, + cryptographicFunctions, + ); return { ...options, diff --git a/src/index.test.ts b/src/index.test.ts index 5b4d2873..0cc826e7 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -9,6 +9,7 @@ import { mnemonicPhraseToBytes, ed25519Bip32, getBIP44CoinTypeToAddressPathTuple, + mnemonicToSeed, } from '.'; // This is purely for coverage shenanigans @@ -25,5 +26,6 @@ describe('index', () => { expect(createBip39KeyFromSeed).toBeDefined(); expect(mnemonicPhraseToBytes).toBeDefined(); expect(getBIP44CoinTypeToAddressPathTuple).toBeDefined(); + expect(mnemonicToSeed).toBeDefined(); }); }); diff --git a/src/index.ts b/src/index.ts index d946de8d..d4b46ddb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,4 +34,4 @@ export { isValidBIP32PathSegment, mnemonicPhraseToBytes, } from './utils'; -export { createBip39KeyFromSeed } from './derivers'; +export { createBip39KeyFromSeed, mnemonicToSeed } from './derivers'; diff --git a/src/utils.test.ts b/src/utils.test.ts index ca38cba1..dd1eae38 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,9 +1,8 @@ -import { mnemonicToSeed } from '@metamask/scure-bip39'; -import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; import { hexToBytes, stringToBytes } from '@metamask/utils'; import fixtures from '../test/fixtures'; import { BIP44Node } from './BIP44Node'; +import { mnemonicToSeed } from './derivers'; import { getBIP32NodeToken, getBIP44ChangePathString, @@ -425,8 +424,8 @@ describe('mnemonicPhraseToBytes', () => { 'converts a mnemonic phrase to a Uint8Array', async (mnemonicPhrase) => { const array = mnemonicPhraseToBytes(mnemonicPhrase); - expect(await mnemonicToSeed(array, wordlist)).toStrictEqual( - await mnemonicToSeed(mnemonicPhrase, wordlist), + expect(await mnemonicToSeed(array)).toStrictEqual( + await mnemonicToSeed(mnemonicPhrase), ); }, ); diff --git a/src/utils.ts b/src/utils.ts index a07e18f6..f48d9d0f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,5 @@ import { wordlist as englishWordlist } from '@metamask/scure-bip39/dist/wordlists/english'; import { assert, createDataView, hexToBytes } from '@metamask/utils'; -import { ripemd160 } from '@noble/hashes/ripemd160'; -import { sha256 } from '@noble/hashes/sha256'; import { base58check as scureBase58check } from '@scure/base'; import type { @@ -19,6 +17,7 @@ import { MAX_UNHARDENED_BIP_32_INDEX, UNPREFIXED_BIP_32_PATH_REGEX, } from './constants'; +import { ripemd160, sha256 } from './cryptography'; import type { SupportedCurve } from './curves'; import { curves } from './curves'; @@ -453,3 +452,13 @@ export function numberToUint32(value: number, littleEndian = false) { return bytes; } + +/** + * A utility function to check if the Web Crypto API is supported in the current + * environment. + * + * @returns Whether the Web Crypto API is supported. + */ +export function isWebCryptoSupported() { + return Boolean(globalThis.crypto?.subtle); +} diff --git a/test/reference-implementations.test.ts b/test/reference-implementations.test.ts index aa7f6fc4..25525e5d 100644 --- a/test/reference-implementations.test.ts +++ b/test/reference-implementations.test.ts @@ -1,3 +1,5 @@ +import { webcrypto } from 'crypto'; + import type { SLIP10Node, HDPathTuple } from '../src'; import { BIP44Node, BIP44PurposeNodeToken } from '../src'; import { ed25519, secp256k1 } from '../src/curves'; @@ -7,306 +9,637 @@ import { getBIP44CoinTypeToAddressPathTuple, hexStringToBytes, } from '../src/utils'; +import * as utils from '../src/utils'; import fixtures from './fixtures'; +// Node.js <20 doesn't have `globalThis.crypto`, so we need to define it. +// TODO: Remove this once we drop support for Node.js <20. +Object.defineProperty(globalThis, 'crypto', { value: webcrypto }); + describe('reference implementation tests', () => { - describe('local', () => { - const { addresses, mnemonic } = fixtures.local; - const mnemonicBip39Node = `bip39:${mnemonic}` as const; - - describe('BIP44Node', () => { - it('derives the expected keys', async () => { - // Ethereum coin type node - const node = await BIP44Node.fromDerivationPath({ - derivationPath: [ - mnemonicBip39Node, - BIP44PurposeNodeToken, - `bip32:60'`, - ], + describe('using web crypto API', () => { + beforeEach(() => { + jest.spyOn(utils, 'isWebCryptoSupported').mockReturnValue(false); + }); + + describe('local', () => { + const { addresses, mnemonic } = fixtures.local; + const mnemonicBip39Node = `bip39:${mnemonic}` as const; + + describe('BIP44Node', () => { + it('derives the expected keys', async () => { + // Ethereum coin type node + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + mnemonicBip39Node, + BIP44PurposeNodeToken, + `bip32:60'`, + ], + }); + + for (let index = 0; index < addresses.length; index++) { + const expectedAddress = addresses[index]; + const childNode = await node.derive( + getBIP44CoinTypeToAddressPathTuple({ address_index: index }), + ); + + expect(childNode.address).toStrictEqual(expectedAddress); + } }); + }); - for (let index = 0; index < addresses.length; index++) { - const expectedAddress = addresses[index]; - const childNode = await node.derive( - getBIP44CoinTypeToAddressPathTuple({ address_index: index }), - ); + describe('deriveKeyFromPath', () => { + it('derives the expected keys', async () => { + // Ethereum coin type key + const node = await deriveKeyFromPath({ + path: [mnemonicBip39Node, BIP44PurposeNodeToken, `bip32:60'`], + curve: 'secp256k1', + }); + + for (let index = 0; index < addresses.length; index++) { + const expectedAddress = addresses[index]; + const { address } = await deriveKeyFromPath({ + path: getBIP44CoinTypeToAddressPathTuple({ + address_index: index, + }), + node, + }); - expect(childNode.address).toStrictEqual(expectedAddress); - } + expect(address).toStrictEqual(expectedAddress); + } + }); }); }); - describe('deriveKeyFromPath', () => { - it('derives the expected keys', async () => { - // Ethereum coin type key - const node = await deriveKeyFromPath({ - path: [mnemonicBip39Node, BIP44PurposeNodeToken, `bip32:60'`], - curve: 'secp256k1', + describe('eth-hd-keyring', () => { + const { mnemonic, addresses } = fixtures['eth-hd-keyring']; + const mnemonicBip39Node = `bip39:${mnemonic}` as const; + + describe('BIP44Node', () => { + it('derives the same keys as the reference implementation', async () => { + // Ethereum coin type node + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + mnemonicBip39Node, + BIP44PurposeNodeToken, + `bip32:60'`, + ], + }); + + const numberOfAccounts = 5; + for (let i = 0; i < numberOfAccounts; i++) { + const path = getBIP44CoinTypeToAddressPathTuple({ + address_index: i, + }); + const address = await node + .derive(path) + .then((childNode) => childNode.address); + + expect(address).toBe(addresses[i]); + } }); - for (let index = 0; index < addresses.length; index++) { - const expectedAddress = addresses[index]; - const { address } = await deriveKeyFromPath({ - path: getBIP44CoinTypeToAddressPathTuple({ address_index: index }), - node, + it('derives the same keys as the reference implementation using public key derivation', async () => { + // Ethereum coin type node + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + mnemonicBip39Node, + BIP44PurposeNodeToken, + `bip32:60'`, + ], }); - expect(address).toStrictEqual(expectedAddress); - } + const numberOfAccounts = 5; + for (let i = 0; i < numberOfAccounts; i++) { + const [account, change, index] = getBIP44CoinTypeToAddressPathTuple( + { + address_index: i, + }, + ); + const parentNode = await node.derive([account, change]); + + const address = await parentNode + .neuter() + .derive([index]) + .then((childNode) => childNode.address); + + expect(address).toBe(addresses[i]); + } + }); + }); + + describe('deriveKeyFromPath', () => { + it('derives the same keys as the reference implementation', async () => { + // Ethereum coin type key + const node = await deriveKeyFromPath({ + path: [mnemonicBip39Node, BIP44PurposeNodeToken, `bip32:60'`], + curve: 'secp256k1', + }); + + const numberOfAccounts = 5; + const ourAccounts = []; + for (let i = 0; i < numberOfAccounts; i++) { + ourAccounts.push( + await deriveKeyFromPath({ + path: getBIP44CoinTypeToAddressPathTuple({ address_index: i }), + node, + }).then(({ address }) => address), + ); + } + + expect(addresses).toStrictEqual(ourAccounts); + }); }); }); - }); - describe('eth-hd-keyring', () => { - const { mnemonic, addresses } = fixtures['eth-hd-keyring']; - const mnemonicBip39Node = `bip39:${mnemonic}` as const; - - describe('BIP44Node', () => { - it('derives the same keys as the reference implementation', async () => { - // Ethereum coin type node - const node = await BIP44Node.fromDerivationPath({ - derivationPath: [ - mnemonicBip39Node, - BIP44PurposeNodeToken, - `bip32:60'`, - ], + describe('ethereumjs-wallet', () => { + const { sampleAddressIndices, hexSeed, privateKey, address, path } = + fixtures['ethereumjs-wallet']; + const seed = hexStringToBytes(hexSeed); + + describe('BIP44Node', () => { + it('derives the same keys as the reference implementation', async () => { + const parentNode = await createBip39KeyFromSeed(seed, secp256k1); + const node = await parentNode.derive(path.ours.tuple); + + expect(node.privateKey).toStrictEqual(privateKey); + expect(node.address).toStrictEqual(address); + + for (const { index, address: theirAddress } of sampleAddressIndices) { + const ourAddress = await node + .derive([`bip32:${index}`]) + .then((childNode) => childNode.address); + + expect(ourAddress).toStrictEqual(theirAddress); + } }); - const numberOfAccounts = 5; - for (let i = 0; i < numberOfAccounts; i++) { - const path = getBIP44CoinTypeToAddressPathTuple({ address_index: i }); - const address = await node - .derive(path) - .then((childNode) => childNode.address); + it('derives the same keys as the reference implementation using public key derivation', async () => { + const parentNode = await createBip39KeyFromSeed(seed, secp256k1); + const node = await parentNode.derive(path.ours.tuple); - expect(address).toBe(addresses[i]); - } - }); + expect(node.privateKey).toStrictEqual(privateKey); + expect(node.address).toStrictEqual(address); + + for (const { index, address: theirAddress } of sampleAddressIndices) { + const ourAddress = await node + .neuter() + .derive([`bip32:${index}`]) + .then((childNode) => childNode.address); - it('derives the same keys as the reference implementation using public key derivation', async () => { - // Ethereum coin type node - const node = await BIP44Node.fromDerivationPath({ - derivationPath: [ - mnemonicBip39Node, - BIP44PurposeNodeToken, - `bip32:60'`, - ], + expect(ourAddress).toStrictEqual(theirAddress); + } }); + }); - const numberOfAccounts = 5; - for (let i = 0; i < numberOfAccounts; i++) { - const [account, change, index] = getBIP44CoinTypeToAddressPathTuple({ - address_index: i, + describe('deriveKeyFromPath', () => { + it('derives the same keys as the reference implementation', async () => { + const node = await createBip39KeyFromSeed(seed, secp256k1); + const childNode = await deriveKeyFromPath({ + path: path.ours.tuple, + node, }); - const parentNode = await node.derive([account, change]); - const address = await parentNode - .neuter() - .derive([index]) - .then((childNode) => childNode.address); + expect(childNode.privateKey).toStrictEqual(privateKey); + expect(childNode.address).toStrictEqual(address); - expect(address).toBe(addresses[i]); - } + for (const { index, address: theirAddress } of sampleAddressIndices) { + const childChildNode = await deriveKeyFromPath({ + path: [`bip32:${index}`], + node: childNode, + }); + + expect(childChildNode.address).toStrictEqual(theirAddress); + } + }); }); }); - describe('deriveKeyFromPath', () => { - it('derives the same keys as the reference implementation', async () => { - // Ethereum coin type key - const node = await deriveKeyFromPath({ - path: [mnemonicBip39Node, BIP44PurposeNodeToken, `bip32:60'`], - curve: 'secp256k1', + describe('BIP-32 specification test vectors', () => { + const vectors = fixtures.bip32; + + // We only test the BIP-32 vectors with deriveKeyFromPath, since not all + // paths are BIP-44 compatible. + describe('deriveKeyFromPath', () => { + it('derives the test vector keys', async () => { + for (const vector of vectors) { + const seed = hexStringToBytes(vector.hexSeed); + const node = await createBip39KeyFromSeed(seed, secp256k1); + + for (const keyObj of vector.keys) { + const { path, privateKey } = keyObj; + + let targetNode: SLIP10Node; + + // If the path is empty, use the master node + if (path.ours.string === '') { + targetNode = node; + } else { + targetNode = await deriveKeyFromPath({ + path: path.ours.tuple as HDPathTuple, + node, + }); + } + + expect(targetNode.privateKey).toStrictEqual(privateKey); + } + } + }); + }); + }); + + describe('ed25519', () => { + describe('SLIP-10', () => { + const vectors = fixtures.ed25519.slip10; + + describe('deriveKeyFromPath', () => { + it('derives the test vector keys', async () => { + for (const { hexSeed, keys } of vectors) { + const node = await createBip39KeyFromSeed( + hexStringToBytes(hexSeed), + ed25519, + ); + + for (const { path, privateKey, publicKey } of keys) { + let targetNode: SLIP10Node; + if (path.ours.string === '') { + targetNode = node; + } else { + targetNode = await deriveKeyFromPath({ + path: path.ours.tuple, + node, + }); + } + + expect(targetNode.privateKey).toBe(privateKey); + expect(targetNode.publicKey).toBe(publicKey); + } + } + }); }); + }); + + describe('ed25519-hd-key', () => { + const { sampleKeyIndices, hexSeed, privateKey, path } = + fixtures.ed25519['ed25519-hd-key']; + const seed = hexStringToBytes(hexSeed); + + describe('SLIP10Node', () => { + it('derives the same keys as the reference implementation', async () => { + // Ethereum coin type node + const parentNode = await createBip39KeyFromSeed(seed, ed25519); + const node = await parentNode.derive(path.ours.tuple); + + expect(node.privateKey).toStrictEqual(privateKey); - const numberOfAccounts = 5; - const ourAccounts = []; - for (let i = 0; i < numberOfAccounts; i++) { - ourAccounts.push( - await deriveKeyFromPath({ - path: getBIP44CoinTypeToAddressPathTuple({ address_index: i }), + for (const { + index, + privateKey: theirPrivateKey, + publicKey: theirPublicKey, + } of sampleKeyIndices) { + const childNode = await node.derive([`slip10:${index}'`]); + + expect(childNode.privateKey).toStrictEqual(theirPrivateKey); + expect(childNode.publicKey).toStrictEqual(theirPublicKey); + } + }); + }); + + describe('deriveKeyFromPath', () => { + it('derives the same keys as the reference implementation', async () => { + // Ethereum coin type key + const node = await createBip39KeyFromSeed(seed, ed25519); + const childNode = await deriveKeyFromPath({ + path: [`slip10:44'`, `slip10:0'`, `slip10:0'`, `slip10:1'`], node, - }).then(({ address }) => address), - ); - } + }); - expect(addresses).toStrictEqual(ourAccounts); + for (const { + index, + privateKey: theirPrivateKey, + } of sampleKeyIndices) { + const { privateKey: ourPrivateKey } = await deriveKeyFromPath({ + path: [`slip10:${index}'`], + node: childNode, + }); + + expect(ourPrivateKey).toStrictEqual(theirPrivateKey); + } + }); + }); }); }); }); - describe('ethereumjs-wallet', () => { - const { sampleAddressIndices, hexSeed, privateKey, address, path } = - fixtures['ethereumjs-wallet']; - const seed = hexStringToBytes(hexSeed); + describe('using built-in cryptography functions', () => { + beforeEach(() => { + jest.spyOn(utils, 'isWebCryptoSupported').mockReturnValue(false); + }); - describe('BIP44Node', () => { - it('derives the same keys as the reference implementation', async () => { - const parentNode = await createBip39KeyFromSeed(seed, secp256k1); - const node = await parentNode.derive(path.ours.tuple); + describe('local', () => { + const { addresses, mnemonic } = fixtures.local; + const mnemonicBip39Node = `bip39:${mnemonic}` as const; - expect(node.privateKey).toStrictEqual(privateKey); - expect(node.address).toStrictEqual(address); + describe('BIP44Node', () => { + it('derives the expected keys', async () => { + // Ethereum coin type node + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + mnemonicBip39Node, + BIP44PurposeNodeToken, + `bip32:60'`, + ], + }); - for (const { index, address: theirAddress } of sampleAddressIndices) { - const ourAddress = await node - .derive([`bip32:${index}`]) - .then((childNode) => childNode.address); + for (let index = 0; index < addresses.length; index++) { + const expectedAddress = addresses[index]; + const childNode = await node.derive( + getBIP44CoinTypeToAddressPathTuple({ address_index: index }), + ); - expect(ourAddress).toStrictEqual(theirAddress); - } + expect(childNode.address).toStrictEqual(expectedAddress); + } + }); }); - it('derives the same keys as the reference implementation using public key derivation', async () => { - const parentNode = await createBip39KeyFromSeed(seed, secp256k1); - const node = await parentNode.derive(path.ours.tuple); - - expect(node.privateKey).toStrictEqual(privateKey); - expect(node.address).toStrictEqual(address); + describe('deriveKeyFromPath', () => { + it('derives the expected keys', async () => { + // Ethereum coin type key + const node = await deriveKeyFromPath({ + path: [mnemonicBip39Node, BIP44PurposeNodeToken, `bip32:60'`], + curve: 'secp256k1', + }); - for (const { index, address: theirAddress } of sampleAddressIndices) { - const ourAddress = await node - .neuter() - .derive([`bip32:${index}`]) - .then((childNode) => childNode.address); + for (let index = 0; index < addresses.length; index++) { + const expectedAddress = addresses[index]; + const { address } = await deriveKeyFromPath({ + path: getBIP44CoinTypeToAddressPathTuple({ + address_index: index, + }), + node, + }); - expect(ourAddress).toStrictEqual(theirAddress); - } + expect(address).toStrictEqual(expectedAddress); + } + }); }); }); - describe('deriveKeyFromPath', () => { - it('derives the same keys as the reference implementation', async () => { - const node = await createBip39KeyFromSeed(seed, secp256k1); - const childNode = await deriveKeyFromPath({ - path: path.ours.tuple, - node, + describe('eth-hd-keyring', () => { + const { mnemonic, addresses } = fixtures['eth-hd-keyring']; + const mnemonicBip39Node = `bip39:${mnemonic}` as const; + + describe('BIP44Node', () => { + it('derives the same keys as the reference implementation', async () => { + // Ethereum coin type node + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + mnemonicBip39Node, + BIP44PurposeNodeToken, + `bip32:60'`, + ], + }); + + const numberOfAccounts = 5; + for (let i = 0; i < numberOfAccounts; i++) { + const path = getBIP44CoinTypeToAddressPathTuple({ + address_index: i, + }); + const address = await node + .derive(path) + .then((childNode) => childNode.address); + + expect(address).toBe(addresses[i]); + } }); - expect(childNode.privateKey).toStrictEqual(privateKey); - expect(childNode.address).toStrictEqual(address); + it('derives the same keys as the reference implementation using public key derivation', async () => { + // Ethereum coin type node + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + mnemonicBip39Node, + BIP44PurposeNodeToken, + `bip32:60'`, + ], + }); + + const numberOfAccounts = 5; + for (let i = 0; i < numberOfAccounts; i++) { + const [account, change, index] = getBIP44CoinTypeToAddressPathTuple( + { + address_index: i, + }, + ); + const parentNode = await node.derive([account, change]); + + const address = await parentNode + .neuter() + .derive([index]) + .then((childNode) => childNode.address); + + expect(address).toBe(addresses[i]); + } + }); + }); - for (const { index, address: theirAddress } of sampleAddressIndices) { - const childChildNode = await deriveKeyFromPath({ - path: [`bip32:${index}`], - node: childNode, + describe('deriveKeyFromPath', () => { + it('derives the same keys as the reference implementation', async () => { + // Ethereum coin type key + const node = await deriveKeyFromPath({ + path: [mnemonicBip39Node, BIP44PurposeNodeToken, `bip32:60'`], + curve: 'secp256k1', }); - expect(childChildNode.address).toStrictEqual(theirAddress); - } + const numberOfAccounts = 5; + const ourAccounts = []; + for (let i = 0; i < numberOfAccounts; i++) { + ourAccounts.push( + await deriveKeyFromPath({ + path: getBIP44CoinTypeToAddressPathTuple({ address_index: i }), + node, + }).then(({ address }) => address), + ); + } + + expect(addresses).toStrictEqual(ourAccounts); + }); }); }); - }); - describe('BIP-32 specification test vectors', () => { - const vectors = fixtures.bip32; + describe('ethereumjs-wallet', () => { + const { sampleAddressIndices, hexSeed, privateKey, address, path } = + fixtures['ethereumjs-wallet']; + const seed = hexStringToBytes(hexSeed); - // We only test the BIP-32 vectors with deriveKeyFromPath, since not all - // paths are BIP-44 compatible. - describe('deriveKeyFromPath', () => { - it('derives the test vector keys', async () => { - for (const vector of vectors) { - const seed = hexStringToBytes(vector.hexSeed); - const node = await createBip39KeyFromSeed(seed, secp256k1); + describe('BIP44Node', () => { + it('derives the same keys as the reference implementation', async () => { + const parentNode = await createBip39KeyFromSeed(seed, secp256k1); + const node = await parentNode.derive(path.ours.tuple); - for (const keyObj of vector.keys) { - const { path, privateKey } = keyObj; + expect(node.privateKey).toStrictEqual(privateKey); + expect(node.address).toStrictEqual(address); - let targetNode: SLIP10Node; + for (const { index, address: theirAddress } of sampleAddressIndices) { + const ourAddress = await node + .derive([`bip32:${index}`]) + .then((childNode) => childNode.address); - // If the path is empty, use the master node - if (path.ours.string === '') { - targetNode = node; - } else { - targetNode = await deriveKeyFromPath({ - path: path.ours.tuple as HDPathTuple, - node, - }); - } + expect(ourAddress).toStrictEqual(theirAddress); + } + }); + + it('derives the same keys as the reference implementation using public key derivation', async () => { + const parentNode = await createBip39KeyFromSeed(seed, secp256k1); + const node = await parentNode.derive(path.ours.tuple); - expect(targetNode.privateKey).toStrictEqual(privateKey); + expect(node.privateKey).toStrictEqual(privateKey); + expect(node.address).toStrictEqual(address); + + for (const { index, address: theirAddress } of sampleAddressIndices) { + const ourAddress = await node + .neuter() + .derive([`bip32:${index}`]) + .then((childNode) => childNode.address); + + expect(ourAddress).toStrictEqual(theirAddress); } - } + }); + }); + + describe('deriveKeyFromPath', () => { + it('derives the same keys as the reference implementation', async () => { + const node = await createBip39KeyFromSeed(seed, secp256k1); + const childNode = await deriveKeyFromPath({ + path: path.ours.tuple, + node, + }); + + expect(childNode.privateKey).toStrictEqual(privateKey); + expect(childNode.address).toStrictEqual(address); + + for (const { index, address: theirAddress } of sampleAddressIndices) { + const childChildNode = await deriveKeyFromPath({ + path: [`bip32:${index}`], + node: childNode, + }); + + expect(childChildNode.address).toStrictEqual(theirAddress); + } + }); }); }); - }); - describe('ed25519', () => { - describe('SLIP-10', () => { - const vectors = fixtures.ed25519.slip10; + describe('BIP-32 specification test vectors', () => { + const vectors = fixtures.bip32; + // We only test the BIP-32 vectors with deriveKeyFromPath, since not all + // paths are BIP-44 compatible. describe('deriveKeyFromPath', () => { it('derives the test vector keys', async () => { - for (const { hexSeed, keys } of vectors) { - const node = await createBip39KeyFromSeed( - hexStringToBytes(hexSeed), - ed25519, - ); + for (const vector of vectors) { + const seed = hexStringToBytes(vector.hexSeed); + const node = await createBip39KeyFromSeed(seed, secp256k1); + + for (const keyObj of vector.keys) { + const { path, privateKey } = keyObj; - for (const { path, privateKey, publicKey } of keys) { let targetNode: SLIP10Node; + + // If the path is empty, use the master node if (path.ours.string === '') { targetNode = node; } else { targetNode = await deriveKeyFromPath({ - path: path.ours.tuple, + path: path.ours.tuple as HDPathTuple, node, }); } - expect(targetNode.privateKey).toBe(privateKey); - expect(targetNode.publicKey).toBe(publicKey); + expect(targetNode.privateKey).toStrictEqual(privateKey); } } }); }); }); - describe('ed25519-hd-key', () => { - const { sampleKeyIndices, hexSeed, privateKey, path } = - fixtures.ed25519['ed25519-hd-key']; - const seed = hexStringToBytes(hexSeed); + describe('ed25519', () => { + describe('SLIP-10', () => { + const vectors = fixtures.ed25519.slip10; + + describe('deriveKeyFromPath', () => { + it('derives the test vector keys', async () => { + for (const { hexSeed, keys } of vectors) { + const node = await createBip39KeyFromSeed( + hexStringToBytes(hexSeed), + ed25519, + ); + + for (const { path, privateKey, publicKey } of keys) { + let targetNode: SLIP10Node; + if (path.ours.string === '') { + targetNode = node; + } else { + targetNode = await deriveKeyFromPath({ + path: path.ours.tuple, + node, + }); + } + + expect(targetNode.privateKey).toBe(privateKey); + expect(targetNode.publicKey).toBe(publicKey); + } + } + }); + }); + }); - describe('SLIP10Node', () => { - it('derives the same keys as the reference implementation', async () => { - // Ethereum coin type node - const parentNode = await createBip39KeyFromSeed(seed, ed25519); - const node = await parentNode.derive(path.ours.tuple); + describe('ed25519-hd-key', () => { + const { sampleKeyIndices, hexSeed, privateKey, path } = + fixtures.ed25519['ed25519-hd-key']; + const seed = hexStringToBytes(hexSeed); - expect(node.privateKey).toStrictEqual(privateKey); + describe('SLIP10Node', () => { + it('derives the same keys as the reference implementation', async () => { + // Ethereum coin type node + const parentNode = await createBip39KeyFromSeed(seed, ed25519); + const node = await parentNode.derive(path.ours.tuple); - for (const { - index, - privateKey: theirPrivateKey, - publicKey: theirPublicKey, - } of sampleKeyIndices) { - const childNode = await node.derive([`slip10:${index}'`]); + expect(node.privateKey).toStrictEqual(privateKey); - expect(childNode.privateKey).toStrictEqual(theirPrivateKey); - expect(childNode.publicKey).toStrictEqual(theirPublicKey); - } - }); - }); + for (const { + index, + privateKey: theirPrivateKey, + publicKey: theirPublicKey, + } of sampleKeyIndices) { + const childNode = await node.derive([`slip10:${index}'`]); - describe('deriveKeyFromPath', () => { - it('derives the same keys as the reference implementation', async () => { - // Ethereum coin type key - const node = await createBip39KeyFromSeed(seed, ed25519); - const childNode = await deriveKeyFromPath({ - path: [`slip10:44'`, `slip10:0'`, `slip10:0'`, `slip10:1'`], - node, + expect(childNode.privateKey).toStrictEqual(theirPrivateKey); + expect(childNode.publicKey).toStrictEqual(theirPublicKey); + } }); + }); - for (const { - index, - privateKey: theirPrivateKey, - } of sampleKeyIndices) { - const { privateKey: ourPrivateKey } = await deriveKeyFromPath({ - path: [`slip10:${index}'`], - node: childNode, + describe('deriveKeyFromPath', () => { + it('derives the same keys as the reference implementation', async () => { + // Ethereum coin type key + const node = await createBip39KeyFromSeed(seed, ed25519); + const childNode = await deriveKeyFromPath({ + path: [`slip10:44'`, `slip10:0'`, `slip10:0'`, `slip10:1'`], + node, }); - expect(ourPrivateKey).toStrictEqual(theirPrivateKey); - } + for (const { + index, + privateKey: theirPrivateKey, + } of sampleKeyIndices) { + const { privateKey: ourPrivateKey } = await deriveKeyFromPath({ + path: [`slip10:${index}'`], + node: childNode, + }); + + expect(ourPrivateKey).toStrictEqual(theirPrivateKey); + } + }); }); }); }); diff --git a/test/vectors.test.ts b/test/vectors.test.ts index 5ab455b2..3bd63a97 100644 --- a/test/vectors.test.ts +++ b/test/vectors.test.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ import { hexToBytes } from '@metamask/utils'; +import { webcrypto } from 'crypto'; import type { SLIP10PathTuple } from '../src'; import { secp256k1 } from '../src'; @@ -9,8 +10,13 @@ import { createBip39KeyFromSeed, entropyToCip3MasterNode, } from '../src/derivers/bip39'; +import * as utils from '../src/utils'; import derivationVectors from './vectors/derivation.json'; +// Node.js <20 doesn't have `globalThis.crypto`, so we need to define it. +// TODO: Remove this once we drop support for Node.js <20. +Object.defineProperty(globalThis, 'crypto', { value: webcrypto }); + type Vector = (typeof derivationVectors.bip32.hardened)[0]; const masterNodeFromSeed = async (seed: Uint8Array, curve: Curve) => { @@ -114,80 +120,169 @@ function generateTests( } describe('vectors', () => { - describe('bip32', () => { - describe('hardened', () => { - for (const vector of derivationVectors.bip32.hardened) { - generateTests(vector); - } + describe('using web crypto API', () => { + beforeEach(() => { + jest.spyOn(utils, 'isWebCryptoSupported').mockReturnValue(true); }); - describe('unhardened', () => { - for (const vector of derivationVectors.bip32.unhardened) { - generateTests(vector, { - publicDerivation: true, + describe('bip32', () => { + describe('hardened', () => { + for (const vector of derivationVectors.bip32.hardened) { + generateTests(vector); + } + }); + + describe('unhardened', () => { + for (const vector of derivationVectors.bip32.unhardened) { + generateTests(vector, { + publicDerivation: true, + }); + } + }); + + describe('mixed', () => { + for (const vector of derivationVectors.bip32.mixed) { + generateTests(vector); + } + }); + }); + + describe('slip10', () => { + describe('hardened', () => { + describe('secp256k1', () => { + for (const vector of derivationVectors.slip10.hardened.secp256k1) { + generateTests(vector); + } }); - } + + describe('ed25519', () => { + for (const vector of derivationVectors.slip10.hardened.ed25519) { + generateTests(vector, { + curve: ed25519, + }); + } + }); + }); + + describe('unhardened', () => { + for (const vector of derivationVectors.slip10.unhardened) { + generateTests(vector, { + publicDerivation: true, + }); + } + }); + + describe('mixed', () => { + for (const vector of derivationVectors.slip10.mixed) { + generateTests(vector); + } + }); }); - describe('mixed', () => { - for (const vector of derivationVectors.bip32.mixed) { - generateTests(vector); - } + describe('cip3', () => { + describe('hardened', () => { + for (const vector of derivationVectors.cip3.hardened) { + generateTests(vector, { curve: ed25519Bip32 }); + } + }); + + describe('unhardened', () => { + for (const vector of derivationVectors.cip3.unhardened) { + generateTests(vector, { + publicDerivation: true, + curve: ed25519Bip32, + }); + } + }); + + describe('mixed', () => { + for (const vector of derivationVectors.cip3.mixed) { + generateTests(vector, { curve: ed25519Bip32 }); + } + }); }); }); - describe('slip10', () => { - describe('hardened', () => { - describe('secp256k1', () => { - for (const vector of derivationVectors.slip10.hardened.secp256k1) { + describe('using built-in cryptography functions', () => { + beforeEach(() => { + jest.spyOn(utils, 'isWebCryptoSupported').mockReturnValue(false); + }); + + describe('bip32', () => { + describe('hardened', () => { + for (const vector of derivationVectors.bip32.hardened) { generateTests(vector); } }); - describe('ed25519', () => { - for (const vector of derivationVectors.slip10.hardened.ed25519) { + describe('unhardened', () => { + for (const vector of derivationVectors.bip32.unhardened) { generateTests(vector, { - curve: ed25519, + publicDerivation: true, }); } }); + + describe('mixed', () => { + for (const vector of derivationVectors.bip32.mixed) { + generateTests(vector); + } + }); }); - describe('unhardened', () => { - for (const vector of derivationVectors.slip10.unhardened) { - generateTests(vector, { - publicDerivation: true, + describe('slip10', () => { + describe('hardened', () => { + describe('secp256k1', () => { + for (const vector of derivationVectors.slip10.hardened.secp256k1) { + generateTests(vector); + } }); - } - }); - describe('mixed', () => { - for (const vector of derivationVectors.slip10.mixed) { - generateTests(vector); - } - }); - }); + describe('ed25519', () => { + for (const vector of derivationVectors.slip10.hardened.ed25519) { + generateTests(vector, { + curve: ed25519, + }); + } + }); + }); - describe('cip3', () => { - describe('hardened', () => { - for (const vector of derivationVectors.cip3.hardened) { - generateTests(vector, { curve: ed25519Bip32 }); - } - }); + describe('unhardened', () => { + for (const vector of derivationVectors.slip10.unhardened) { + generateTests(vector, { + publicDerivation: true, + }); + } + }); - describe('unhardened', () => { - for (const vector of derivationVectors.cip3.unhardened) { - generateTests(vector, { - publicDerivation: true, - curve: ed25519Bip32, - }); - } + describe('mixed', () => { + for (const vector of derivationVectors.slip10.mixed) { + generateTests(vector); + } + }); }); - describe('mixed', () => { - for (const vector of derivationVectors.cip3.mixed) { - generateTests(vector, { curve: ed25519Bip32 }); - } + describe('cip3', () => { + describe('hardened', () => { + for (const vector of derivationVectors.cip3.hardened) { + generateTests(vector, { curve: ed25519Bip32 }); + } + }); + + describe('unhardened', () => { + for (const vector of derivationVectors.cip3.unhardened) { + generateTests(vector, { + publicDerivation: true, + curve: ed25519Bip32, + }); + } + }); + + describe('mixed', () => { + for (const vector of derivationVectors.cip3.mixed) { + generateTests(vector, { curve: ed25519Bip32 }); + } + }); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 65e0b9c9..968970f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, - "lib": ["ES2020"], + "lib": ["ES2020", "DOM"], "module": "CommonJS", "moduleResolution": "node", "noEmit": true,