/**
* @fileoverview BIP32 child key derivation
* @version 1.0.0
* @author yfbsei
* @license ISC
*/
import { createHmac, createHash } from 'node:crypto';
import { secp256k1 } from '@noble/curves/secp256k1';
import BN from 'bn.js';
import { b58encode, b58decode } from '../../encoding/base58.js';
import rmd160 from '../../core/crypto/hash/ripemd160.js';
import {
CRYPTO_CONSTANTS,
BIP32_CONSTANTS,
NETWORK_VERSIONS,
validateAndGetNetwork
} from '../../core/constants.js';
/**
* Offset for hardened key derivation (2^31)
* @constant {number}
*/
const HARDENED_OFFSET = 0x80000000;
/**
* Validate a Base58Check-encoded extended key
* @param {string} key - Extended key to validate
* @param {string|null} [expectedPrefix=null] - Expected key prefix
* @returns {boolean} True if valid
* @throws {Error} If key is invalid
*/
function validateExtendedKey(key, expectedPrefix = null) {
if (!key || typeof key !== 'string') {
throw new Error('Extended key must be a non-empty string');
}
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]+$/;
if (!base58Regex.test(key)) {
throw new Error('Extended key contains invalid Base58 characters');
}
if (key.length !== 111) {
throw new Error(`Invalid extended key length: expected 111, got ${key.length}`);
}
if (expectedPrefix && !key.startsWith(expectedPrefix)) {
throw new Error(`Extended key has wrong prefix: expected ${expectedPrefix}`);
}
return true;
}
/**
* Validate a BIP32 derivation path
* @param {string} path - Derivation path (e.g., "m/44'/0'/0'/0/0")
* @returns {boolean} True if valid
* @throws {Error} If path format is invalid
*/
function validateDerivationPath(path) {
if (!path || typeof path !== 'string') {
throw new Error('Derivation path must be a non-empty string');
}
const pathRegex = /^m(\/\d+'?)*$/;
if (!pathRegex.test(path)) {
throw new Error(`Invalid derivation path format: ${path}`);
}
return true;
}
/**
* Validate a derived child key against curve parameters
* @param {BN} childKeyBN - Child key as big number
* @returns {Buffer} 32-byte formatted key
* @throws {Error} If key is zero or >= curve order
*/
function validateChildKey(childKeyBN) {
const curveOrder = new BN(CRYPTO_CONSTANTS.SECP256K1_ORDER, 'hex');
if (childKeyBN.isZero()) {
throw new Error('Invalid child key: key is zero');
}
if (childKeyBN.gte(curveOrder)) {
throw new Error('Invalid child key: key >= curve order');
}
const formattedKey = childKeyBN.toBuffer('be', 32);
if (formattedKey.length !== 32) {
throw new Error(`Key serialization failed: expected 32 bytes, got ${formattedKey.length}`);
}
return formattedKey;
}
/**
* Decode a Base58Check-encoded extended key
* @param {string} extendedKey - Extended key (xprv/xpub/tprv/tpub)
* @returns {Object} Decoded key information
* @throws {Error} If key format is invalid
*/
function decodeExtendedKey(extendedKey) {
validateExtendedKey(extendedKey);
const decoded = b58decode(extendedKey);
if (decoded.length !== BIP32_CONSTANTS.EXTENDED_KEY_LENGTH) {
throw new Error(`Invalid decoded key length: ${decoded.length}`);
}
const version = decoded.slice(0, 4);
const depth = decoded.readUInt8(4);
const parentFingerprint = decoded.slice(5, 9);
const childIndex = decoded.readUInt32BE(9);
const chainCode = decoded.slice(13, 45);
const keyData = decoded.slice(45, 78);
let isPrivate = false;
let network = 'main';
if (version.equals(NETWORK_VERSIONS.main.versions.EXTENDED_PRIVATE_KEY)) {
isPrivate = true;
network = 'main';
} else if (version.equals(NETWORK_VERSIONS.main.versions.EXTENDED_PUBLIC_KEY)) {
isPrivate = false;
network = 'main';
} else if (version.equals(NETWORK_VERSIONS.test.versions.EXTENDED_PRIVATE_KEY)) {
isPrivate = true;
network = 'test';
} else if (version.equals(NETWORK_VERSIONS.test.versions.EXTENDED_PUBLIC_KEY)) {
isPrivate = false;
network = 'test';
} else {
throw new Error('Unknown extended key version');
}
let privateKey = null;
let publicKey = null;
if (isPrivate) {
if (keyData[0] !== 0x00) {
throw new Error('Invalid private key padding');
}
privateKey = keyData.slice(1);
publicKey = Buffer.from(secp256k1.getPublicKey(privateKey, true));
} else {
publicKey = keyData;
}
return {
version,
depth,
parentFingerprint,
childIndex,
chainCode,
privateKey,
publicKey,
isPrivate,
network
};
}
/**
* Encode key information to a Base58Check extended key
* @param {Object} keyInfo - Key information object
* @returns {string} Base58Check-encoded extended key
*/
function encodeExtendedKey(keyInfo) {
const networkConfig = NETWORK_VERSIONS[keyInfo.network];
const buffer = Buffer.alloc(BIP32_CONSTANTS.EXTENDED_KEY_LENGTH);
let offset = 0;
const versionBytes = keyInfo.isPrivate
? networkConfig.versions.EXTENDED_PRIVATE_KEY
: networkConfig.versions.EXTENDED_PUBLIC_KEY;
versionBytes.copy(buffer, offset);
offset += 4;
buffer.writeUInt8(keyInfo.depth, offset);
offset += 1;
keyInfo.parentFingerprint.copy(buffer, offset);
offset += 4;
buffer.writeUInt32BE(keyInfo.childIndex, offset);
offset += 4;
keyInfo.chainCode.copy(buffer, offset);
offset += 32;
if (keyInfo.isPrivate) {
buffer.writeUInt8(0x00, offset);
offset += 1;
keyInfo.privateKey.copy(buffer, offset);
} else {
keyInfo.publicKey.copy(buffer, offset);
}
return b58encode(buffer);
}
/**
* Get the fingerprint of a public key (first 4 bytes of HASH160)
* @param {Buffer} publicKey - Compressed public key
* @returns {Buffer} 4-byte fingerprint
*/
function getFingerprint(publicKey) {
const sha256Hash = createHash('sha256').update(publicKey).digest();
const hash160 = rmd160(sha256Hash);
return hash160.slice(0, 4);
}
/**
* Derive a child key from a parent key
* @param {Object} parentKeyInfo - Parent key information
* @param {number} index - Child index (0-2147483647)
* @param {boolean} [hardened=false] - Whether to use hardened derivation
* @returns {Object} Child key information
* @throws {Error} If hardened derivation attempted on public key
*/
function deriveChildKey(parentKeyInfo, index, hardened = false) {
const actualIndex = hardened ? index + HARDENED_OFFSET : index;
if (hardened && !parentKeyInfo.isPrivate) {
throw new Error('Cannot derive hardened child from public key');
}
let data;
if (hardened) {
data = Buffer.concat([
Buffer.from([0x00]),
parentKeyInfo.privateKey,
Buffer.alloc(4)
]);
data.writeUInt32BE(actualIndex, 33);
} else {
data = Buffer.concat([
parentKeyInfo.publicKey,
Buffer.alloc(4)
]);
data.writeUInt32BE(actualIndex, 33);
}
const hmac = createHmac('sha512', parentKeyInfo.chainCode);
const hmacResult = hmac.update(data).digest();
const derivedKeyMaterial = hmacResult.slice(0, 32);
const childChainCode = hmacResult.slice(32, 64);
let childPrivateKey = null;
let childPublicKey = null;
if (parentKeyInfo.isPrivate) {
const parentKeyBN = new BN(parentKeyInfo.privateKey);
const derivedBN = new BN(derivedKeyMaterial);
const curveOrder = new BN(CRYPTO_CONSTANTS.SECP256K1_ORDER, 'hex');
const childKeyBN = parentKeyBN.add(derivedBN).mod(curveOrder);
childPrivateKey = validateChildKey(childKeyBN);
childPublicKey = Buffer.from(secp256k1.getPublicKey(childPrivateKey, true));
} else {
const parentPoint = secp256k1.ProjectivePoint.fromHex(parentKeyInfo.publicKey);
const derivedPoint = secp256k1.ProjectivePoint.fromPrivateKey(derivedKeyMaterial);
const childPoint = parentPoint.add(derivedPoint);
childPublicKey = Buffer.from(childPoint.toRawBytes(true));
}
const parentFingerprint = getFingerprint(parentKeyInfo.publicKey);
return {
version: parentKeyInfo.version,
depth: parentKeyInfo.depth + 1,
parentFingerprint,
childIndex: actualIndex,
chainCode: childChainCode,
privateKey: childPrivateKey,
publicKey: childPublicKey,
isPrivate: parentKeyInfo.isPrivate,
network: parentKeyInfo.network
};
}
/**
* Derive a child key at a given path from an extended key
* @param {string} path - BIP32 derivation path (e.g., "m/44'/0'/0'/0/0")
* @param {string} extendedKey - Parent extended key (xprv/xpub)
* @returns {Object} Derived key information
* @throws {Error} If path or key is invalid
*/
function derive(path, extendedKey) {
validateDerivationPath(path);
validateExtendedKey(extendedKey);
let currentKey = decodeExtendedKey(extendedKey);
const segments = path.split('/').slice(1);
for (const segment of segments) {
const hardened = segment.endsWith("'");
const indexStr = hardened ? segment.slice(0, -1) : segment;
const index = parseInt(indexStr, 10);
if (isNaN(index) || index < 0) {
throw new Error(`Invalid path segment: ${segment}`);
}
if (currentKey.depth >= BIP32_CONSTANTS.MAX_DERIVATION_DEPTH) {
throw new Error(`Maximum derivation depth exceeded: ${currentKey.depth}`);
}
currentKey = deriveChildKey(currentKey, index, hardened);
}
const extendedPrivateKey = currentKey.isPrivate ? encodeExtendedKey(currentKey) : null;
const publicKeyInfo = { ...currentKey, isPrivate: false, privateKey: null };
const extendedPublicKey = encodeExtendedKey(publicKeyInfo);
return {
extendedPrivateKey,
extendedPublicKey,
privateKey: currentKey.privateKey,
publicKey: currentKey.publicKey,
chainCode: currentKey.chainCode,
depth: currentKey.depth,
index: currentKey.childIndex,
network: currentKey.network
};
}
export {
derive,
deriveChildKey,
decodeExtendedKey,
encodeExtendedKey,
validateDerivationPath,
validateExtendedKey,
getFingerprint,
HARDENED_OFFSET
};
export default { derive };