@ledgerhq/hw-app-btc
Version:
Ledger Hardware Wallet Bitcoin Application API
125 lines • 5.3 kB
JavaScript
;
/**
* @file bip32.ts
* @description BIP32 Path Handling for Bitcoin Wallets
*
* This file provides utility functions to handle BIP32 paths,
* which are commonly used in hierarchical deterministic (HD) wallets.
* It includes functions to convert BIP32 paths to and from different formats,
* extract components from extended public keys (xpubs), manipulate path elements,
* and derive child public keys locally (for non-hardened derivation).
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.pathElementsToBuffer = pathElementsToBuffer;
exports.bip32asBuffer = bip32asBuffer;
exports.pathArrayToString = pathArrayToString;
exports.pathStringToArray = pathStringToArray;
exports.pubkeyFromXpub = pubkeyFromXpub;
exports.getXpubComponents = getXpubComponents;
exports.hardenedPathOf = hardenedPathOf;
exports.deriveChildPublicKey = deriveChildPublicKey;
const bip32_path_1 = __importDefault(require("bip32-path"));
const bs58check_1 = __importDefault(require("bs58check"));
const secp256k1_1 = require("@noble/curves/secp256k1");
const hmac_1 = require("@noble/hashes/hmac");
const sha2_1 = require("@noble/hashes/sha2");
function pathElementsToBuffer(paths) {
const buffer = Buffer.alloc(1 + paths.length * 4);
buffer[0] = paths.length;
paths.forEach((element, index) => {
buffer.writeUInt32BE(element, 1 + 4 * index);
});
return buffer;
}
function bip32asBuffer(path) {
const pathElements = !path ? [] : pathStringToArray(path);
return pathElementsToBuffer(pathElements);
}
function pathArrayToString(pathElements) {
// Limitation: bippath can't handle and empty path. It shouldn't affect us
// right now, but might in the future.
// TODO: Fix support for empty path.
return bip32_path_1.default.fromPathArray(pathElements).toString();
}
function pathStringToArray(path) {
return bip32_path_1.default.fromString(path).toPathArray();
}
function pubkeyFromXpub(xpub) {
const xpubBuf = bs58check_1.default.decode(xpub);
return xpubBuf.subarray(-33);
}
function getXpubComponents(xpub) {
const xpubBuf = bs58check_1.default.decode(xpub);
return {
chaincode: xpubBuf.subarray(13, 13 + 32),
pubkey: xpubBuf.subarray(-33),
version: xpubBuf.readUInt32BE(0),
};
}
function hardenedPathOf(pathElements) {
for (let i = pathElements.length - 1; i >= 0; i--) {
if (pathElements[i] >= 0x80000000) {
return pathElements.slice(0, i + 1);
}
}
return [];
}
/**
* Derives a child public key from a parent public key using BIP32 non-hardened derivation.
* This allows deriving child keys locally without device interaction.
*
* @param parentPubkey - The parent compressed public key (33 bytes)
* @param parentChaincode - The parent chaincode (32 bytes)
* @param index - The child index (must be non-hardened, i.e., < 0x80000000)
* @returns The derived child public key and chaincode
* @throws Error if attempting hardened derivation or invalid inputs
*/
function deriveChildPublicKey(parentPubkey, parentChaincode, index) {
// Validate parentPubkey is a compressed public key (33 bytes)
if (parentPubkey.length !== 33) {
throw new Error(`Invalid parent pubkey length: expected 33 bytes, got ${parentPubkey.length}`);
}
// Validate parentChaincode is 32 bytes
if (parentChaincode.length !== 32) {
throw new Error(`Invalid parent chaincode length: expected 32 bytes, got ${parentChaincode.length}`);
}
// Validate index is a non-negative integer
if (!Number.isInteger(index) || index < 0) {
throw new Error(`Invalid index: must be a non-negative integer, got ${index}`);
}
// Hardened derivation not possible from public key
if (index >= 0x80000000) {
throw new Error("Cannot derive hardened child from public key");
}
// I = HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i))
const data = Buffer.alloc(parentPubkey.length + 4);
parentPubkey.copy(data, 0);
data.writeUInt32BE(index, parentPubkey.length);
const I = (0, hmac_1.hmac)(sha2_1.sha512, parentChaincode, data);
const IL = I.subarray(0, 32);
const IR = I.subarray(32);
const tweak = BigInt(`0x${Buffer.from(IL).toString("hex")}`);
const curveOrder = secp256k1_1.secp256k1.Point.CURVE().n;
// BIP32 CKDpub invalid child cases:
// - parse256(IL) >= n
// - parse256(IL) == 0
if (tweak === 0n || tweak >= curveOrder) {
throw new Error(`Invalid child derivation at index ${index}`);
}
// Ki = point(parse256(IL)) + Kpar
const parentPoint = secp256k1_1.secp256k1.Point.fromHex(parentPubkey);
const tweakScalar = secp256k1_1.secp256k1.Point.Fn.fromBytes(IL);
const tweakPoint = secp256k1_1.secp256k1.Point.BASE.multiply(tweakScalar);
const childPoint = parentPoint.add(tweakPoint);
if (childPoint.equals(secp256k1_1.secp256k1.Point.ZERO)) {
throw new Error(`Invalid child derivation at index ${index}`);
}
return {
pubkey: Buffer.from(childPoint.toBytes(true)),
chaincode: Buffer.from(IR),
};
}
//# sourceMappingURL=bip32.js.map