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

Add cryptographic functions with option to specify custom functions #197

Merged
merged 11 commits into from
Nov 25, 2024
93 changes: 93 additions & 0 deletions src/BIP44CoinTypeNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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', () => {
Expand Down
103 changes: 72 additions & 31 deletions src/BIP44CoinTypeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
FrederikBolding marked this conversation as resolved.
Show resolved Hide resolved
) {
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);
}
Expand All @@ -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(
Expand Down Expand Up @@ -324,23 +342,29 @@ 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<BIP44Node> {
const path = getBIP44CoinTypeToAddressPathTuple({
account,
change,
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);
}
Expand Down Expand Up @@ -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<CoinTypeToAddressIndices, 'address_index'>,
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);
Expand All @@ -409,16 +436,19 @@ export async function getBIP44AddressKeyDeriver(
address_index: number,
isHardened = false,
): Promise<BIP44Node> => {
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);
};
Expand All @@ -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<BIP44CoinTypeNode> {
if (node instanceof BIP44CoinTypeNode) {
validateCoinTypeNodeDepth(node.depth);
Expand All @@ -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,
Expand All @@ -463,5 +500,9 @@ async function getNode(
return coinTypeNode;
}

return BIP44CoinTypeNode.fromJSON(node, node.coin_type);
return BIP44CoinTypeNode.fromJSON(
node,
node.coin_type,
cryptographicFunctions,
);
}
Loading
Loading