Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix deriveKeyFromPath interface, refactor derivers #14

Merged
merged 4 commits into from
Feb 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/derivers/bip32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/derivers/bip39.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 5 additions & 1 deletion src/derivers/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
38 changes: 29 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Expand All @@ -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:<SPACE_DELIMITED_SEED_PHRASE>/".
* @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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {
privateKeyToEthAddress,
},
bip39: { deriveChildKey: bip39Derive, bip39MnemonicToMultipath },
} = require('../dist/derivers');
} = require('../dist/derivers').derivers;

const defaultEthereumPath = `m/44'/60'/0'/0`;

Expand Down Expand Up @@ -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) => {
Expand Down