diff --git a/src/derivers/bip32.ts b/src/derivers/bip32.ts index 05f009fc..c3f839fb 100755 --- a/src/derivers/bip32.ts +++ b/src/derivers/bip32.ts @@ -29,11 +29,12 @@ export function bip32PathToMultipath(bip32Path: string): string { return multipath; } -export function deriveChildKey(parentKey: Buffer, pathPart: string): Buffer { +export function deriveChildKey(pathPart: string, parentKey: Buffer): Buffer { const isHardened = pathPart.includes(`'`); const indexPart = pathPart.split(`'`)[0]; const childIndex = parseInt(indexPart, 10); assert(childIndex < HARDENED_OFFSET, 'Invalid index'); + assert(Boolean(parentKey), 'Must provide parentKey'); assert(parentKey.length === 64, 'Parent key invalid length'); const parentPrivateKey = parentKey.slice(0, 32); diff --git a/src/derivers/bip39.ts b/src/derivers/bip39.ts index 064738d4..7db180c6 100755 --- a/src/derivers/bip39.ts +++ b/src/derivers/bip39.ts @@ -8,7 +8,7 @@ export function bip39MnemonicToMultipath(mnemonic: string): string { } // this creates a child key using bip39, ignoring the parent key -export function deriveChildKey(_parentKey: unknown, pathPart: string): Buffer { +export function deriveChildKey(pathPart: string, _parentKey?: never): Buffer { const mnemonic = pathPart; const seedBuffer = bip39.mnemonicToSeed(mnemonic); const entropy = crypto.createHmac('sha512', ROOT_BASE_SECRET).update(seedBuffer).digest(); diff --git a/src/derivers/index.ts b/src/derivers/index.ts index ff4b4daf..9cffa1e6 100644 --- a/src/derivers/index.ts +++ b/src/derivers/index.ts @@ -1,7 +1,11 @@ import * as bip32 from './bip32'; import * as bip39 from './bip39'; -export = { +export interface Deriver { + deriveChildKey: (pathPart: string, parentKey?: Buffer) => Buffer; +} + +export const derivers = { bip32, bip39, }; diff --git a/src/index.ts b/src/index.ts index 02d1e06a..fe6c3448 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,11 @@ import bip39 from 'bip39'; -import derivers from './derivers'; +import { derivers, Deriver } from './derivers'; +/** + * Converts the given BIP-39 mnemonic to a cryptographic seed. + * @param mnemonic - The BIP-39 mnemonic. + * @returns The cryptographic seed corresponding to the given mnemonic. + */ export function mnemonicToSeed(mnemonic: string): Buffer { return bip39.mnemonicToSeed(mnemonic); } @@ -18,11 +23,26 @@ export function mnemonicToSeed(mnemonic: string): Buffer { */ /** - * @param {string} pathSegment - A full or leaf HD path segment. If full, - * optionally preceded by "bip39:/". - * @param {Buffer} [parentKey] - The parent key of the given path segment. + * Takes a full or partial HD path string and returns the key corresponding to + * the given path, with the following constraints: + * + * - If the path starts with a BIP-32 segment, a parent key must be provided. + * - If the path starts with a BIP-39 segment, a parent key may NOT be provided. + * - The path cannot exceed 5 BIP-32 segments in length, optionally preceded by + * a single BIP-39 segment. + * + * WARNING: It is the consumer's responsibility to ensure that the path is valid + * relative to its parent key. + * + * @param pathSegment - A full or partial HD path, e.g.: + * bip39:SEED_PHRASE/bip32:44'/bip32:60'/bip32:0'/bip32:0/bip32:0 + * + * BIP-39 seed phrases must be lowercase, space-delimited, and 12-24 words long. + * + * @param parentKey - The parent key of the given path segment, if any. + * @returns The derived key. */ -export function deriveKeyFromPath(pathSegment: string, parentKey: Buffer) { +export function deriveKeyFromPath(pathSegment: string, parentKey?: Buffer): Buffer { validateDeriveKeyParams(pathSegment, parentKey); let key = parentKey; @@ -33,13 +53,13 @@ export function deriveKeyFromPath(pathSegment: string, parentKey: Buffer) { if (!(hasDeriver(pathType))) { throw new Error(`Unknown derivation type "${pathType}"`); } - const deriver = derivers[pathType]; - const childKey = deriver.deriveChildKey(key, pathValue); + const deriver = derivers[pathType] as Deriver; + const childKey = deriver.deriveChildKey(pathValue, key); // continue deriving from child key key = childKey; }); - return key; + return key as Buffer; } function hasDeriver(pathType: string): pathType is keyof typeof derivers { @@ -67,7 +87,7 @@ const BIP_39_PATH_REGEX = /^bip39:([a-z]+){1}( [a-z]+){11,23}$/u; */ const MULTI_PATH_REGEX = /^(bip39:([a-z]+){1}( [a-z]+){11,23}\/)?(bip32:\d+'?\/){0,4}(bip32:\d+'?)$/u; -function validateDeriveKeyParams(pathSegment: string, parentKey: Buffer) { +function validateDeriveKeyParams(pathSegment: string, parentKey?: Buffer) { // The path segment must be one of the following: // - A lone BIP-32 path segment // - A lone BIP-39 path segment diff --git a/test/index.js b/test/index.js index a96701de..d27e554f 100755 --- a/test/index.js +++ b/test/index.js @@ -8,7 +8,7 @@ const { privateKeyToEthAddress, }, bip39: { deriveChildKey: bip39Derive, bip39MnemonicToMultipath }, -} = require('../dist/derivers'); +} = require('../dist/derivers').derivers; const defaultEthereumPath = `m/44'/60'/0'/0`; @@ -121,15 +121,15 @@ test('bip32Derive', (t) => { // generate parent key let parentKey = null; parentKey = bip39Derive( - parentKey, `romance hurry grit huge rifle ordinary loud toss sound congress upset twist`, + parentKey, ); - parentKey = bip32Derive(parentKey, `44'`); - parentKey = bip32Derive(parentKey, `60'`); - parentKey = bip32Derive(parentKey, `0'`); - parentKey = bip32Derive(parentKey, `0`); + parentKey = bip32Derive(`44'`, parentKey); + parentKey = bip32Derive(`60'`, parentKey); + parentKey = bip32Derive(`0'`, parentKey); + parentKey = bip32Derive(`0`, parentKey); const keys = expectedAddresses.map((_, index) => { - return bip32Derive(parentKey, `${index}`); + return bip32Derive(`${index}`, parentKey); }); // validate addresses keys.forEach((key, index) => {