From baf9953a637618394218a395579e97448c05fb7c Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 14 May 2026 12:02:40 -0700 Subject: [PATCH] Change bip44 class of all static methods to flat module. --- src/lib/crypto/bip44.ts | 222 ++++++++++++++++++++-------------------- 1 file changed, 110 insertions(+), 112 deletions(-) diff --git a/src/lib/crypto/bip44.ts b/src/lib/crypto/bip44.ts index 38d748b..aea3ad8 100644 --- a/src/lib/crypto/bip44.ts +++ b/src/lib/crypto/bip44.ts @@ -11,127 +11,125 @@ type ExtendedKey = { type Curve = 'Bitcoin seed' | 'ed25519 seed' -export class Bip44 { - static get BIP44_PURPOSE (): 44 { return 44 } - static get HARDENED_OFFSET (): 0x80000000 { return 0x80000000 } +const BIP44_PURPOSE: 44 = 44 +const HARDENED_OFFSET: 0x80000000 = 0x80000000 - /** - * Derives a private child key for a coin by following the specified BIP-32 and - * BIP-44 derivation path. Purpose is always 44'. For ed25519, only hardened - * child keys are supported. - * - * @param {string} curve - 'Bitcoin seed' or 'ed25519 seed' - * @param {ArrayBuffer} seed - Hexadecimal seed derived from mnemonic phrase - * @param {number} coin - Number registered to a specific coin in SLIP-0044 - * @param {number} account - Account number between 0 and 2^31-1 - * @param {number} [change] - Used for change transactions, 0 for external and 1 for internal - * @param {number} [address] - Sequentially increasing index of addresses to use for each account - * @returns {Promise} Private child key for the account - */ - static ckd (curve: Curve, seed: ArrayBuffer, coin: number, account: number, change?: number, address?: number): Promise { - if (curve !== 'Bitcoin seed' && curve !== 'ed25519 seed') { - throw new TypeError(`Unsupported curve ${curve}`) - } - if (seed.byteLength < 16 || seed.byteLength > 64) { - throw new RangeError(`Invalid seed byte length ${seed.byteLength}`) - } - if (!Number.isSafeInteger(coin) || coin < 0 || coin > 0x7fffffff) { - throw new RangeError(`Invalid coin 0x${coin.toString(16)}`) - } - if (!Number.isSafeInteger(account) || account < 0 || account > 0x7fffffff) { - throw new RangeError(`Invalid account index 0x${account.toString(16)}`) - } - if (change !== undefined && (!Number.isSafeInteger(change) || change < 0 || change > 1)) { - throw new RangeError(`Invalid change index 0x${account.toString(16)}`) - } - if (address !== undefined && (!Number.isSafeInteger(address) || address < 0 || address > 0x7fffffff)) { - throw new RangeError(`Invalid address index 0x${account.toString(16)}`) - } - return this.slip10(curve, seed) - .then(masterKey => this.CKDpriv(curve, masterKey, this.BIP44_PURPOSE + this.HARDENED_OFFSET)) - .then(purposeKey => this.CKDpriv(curve, purposeKey, coin + this.HARDENED_OFFSET)) - .then(coinKey => this.CKDpriv(curve, coinKey, account + this.HARDENED_OFFSET)) - .then(accountKey => this.CKDpriv(curve, accountKey, change)) - .then(chainKey => this.CKDpriv(curve, chainKey, address)) - .then(addressKey => addressKey.privateKey) +/** + * Derives a private child key for a coin by following the specified BIP-32 and + * BIP-44 derivation path. Purpose is always 44'. For ed25519, only hardened + * child keys are supported. + * + * @param {string} curve - 'Bitcoin seed' or 'ed25519 seed' + * @param {ArrayBuffer} seed - Hexadecimal seed derived from mnemonic phrase + * @param {number} coin - Number registered to a specific coin in SLIP-0044 + * @param {number} account - Account number between 0 and 2^31-1 + * @param {number} [change] - Used for change transactions, 0 for external and 1 for internal + * @param {number} [address] - Sequentially increasing index of addresses to use for each account + * @returns {Promise} Private child key for the account + */ +export function ckd (curve: Curve, seed: ArrayBuffer, coin: number, account: number, change?: number, address?: number): Promise { + if (curve !== 'Bitcoin seed' && curve !== 'ed25519 seed') { + throw new TypeError(`Unsupported curve ${curve}`) } - - static slip10 (curve: string, S: ArrayBuffer): Promise { - const key = new Uint8Array(new TextEncoder().encode(curve)) - const data = new Uint8Array(S) - return this.hmac(key, data) - .then(I => { - const IL = I.slice(0, I.byteLength / 2) - const IR = I.slice(I.byteLength / 2) - return ({ privateKey: IL, chainCode: IR }) - }) + if (seed.byteLength < 16 || seed.byteLength > 64) { + throw new RangeError(`Invalid seed byte length ${seed.byteLength}`) } - - static CKDpriv (curve: Curve, { privateKey, chainCode }: ExtendedKey, index?: number): Promise { - if (index === undefined) { - return Promise.resolve({ privateKey, chainCode }) - } - const pk = new Uint8Array(privateKey) - const key = new Uint8Array(chainCode) - const data = new Uint8Array(37) - if (index >= this.HARDENED_OFFSET) { - data.set([0]) - data.set(pk, 1) - data.set(this.ser32(index), 33) - } else if (curve === 'ed25519 seed') { - throw new RangeError('Only hardened child keys are supported for ed25519') - } else { - data.set(secp256k1_getPublicKey(pk)) - data.set(this.ser32(index), 33) - } - return this.hmac(key, data) - .then(I => { - const IL = I.slice(0, I.byteLength / 2) - const IR = I.slice(I.byteLength / 2) - if (curve === 'ed25519 seed') { - return ({ privateKey: IL, chainCode: IR }) - } else { - const ILparsed = this.parse256(new Uint8Array(IL)) - if (ILparsed >= Point.CURVE().n) { - throw new Error('Invalid child key is greater than the order of the curve') - } - const pkParsed = this.parse256(pk) - const childKey = (ILparsed + pkParsed) % Point.CURVE().n - if (childKey === 0n) { - throw new Error('Invalid child key is zero') - } - return ({ privateKey: this.ser256(childKey).buffer, chainCode: IR }) - } - }) + if (!Number.isSafeInteger(coin) || coin < 0 || coin > 0x7fffffff) { + throw new RangeError(`Invalid coin 0x${coin.toString(16)}`) } - - static ser32 (integer: number): Uint8Array { - const view = new DataView(new ArrayBuffer(4)) - view.setUint32(0, integer, false) - return new Uint8Array(view.buffer) + if (!Number.isSafeInteger(account) || account < 0 || account > 0x7fffffff) { + throw new RangeError(`Invalid account index 0x${account.toString(16)}`) } + if (change !== undefined && (!Number.isSafeInteger(change) || change < 0 || change > 1)) { + throw new RangeError(`Invalid change index 0x${account.toString(16)}`) + } + if (address !== undefined && (!Number.isSafeInteger(address) || address < 0 || address > 0x7fffffff)) { + throw new RangeError(`Invalid address index 0x${account.toString(16)}`) + } + return slip10(curve, seed) + .then(masterKey => CKDpriv(curve, masterKey, BIP44_PURPOSE + HARDENED_OFFSET)) + .then(purposeKey => CKDpriv(curve, purposeKey, coin + HARDENED_OFFSET)) + .then(coinKey => CKDpriv(curve, coinKey, account + HARDENED_OFFSET)) + .then(accountKey => CKDpriv(curve, accountKey, change)) + .then(chainKey => CKDpriv(curve, chainKey, address)) + .then(addressKey => addressKey.privateKey) +} + +function slip10 (curve: string, S: ArrayBuffer): Promise { + const key = new Uint8Array(new TextEncoder().encode(curve)) + const data = new Uint8Array(S) + return hmac(key, data) + .then(I => { + const IL = I.slice(0, I.byteLength / 2) + const IR = I.slice(I.byteLength / 2) + return ({ privateKey: IL, chainCode: IR }) + }) +} - static ser256 (integer: bigint): Uint8Array { - let bytes = new Uint8Array(32) - for (let i = bytes.byteLength - 1; i >= 0; i--) { - bytes[i] = Number(integer & 0xffn) - integer >>= 8n - } - return bytes +function CKDpriv (curve: Curve, { privateKey, chainCode }: ExtendedKey, index?: number): Promise { + if (index === undefined) { + return Promise.resolve({ privateKey, chainCode }) } + const pk = new Uint8Array(privateKey) + const key = new Uint8Array(chainCode) + const data = new Uint8Array(37) + if (index >= HARDENED_OFFSET) { + data.set([0]) + data.set(pk, 1) + data.set(ser32(index), 33) + } else if (curve === 'ed25519 seed') { + throw new RangeError('Only hardened child keys are supported for ed25519') + } else { + data.set(secp256k1_getPublicKey(pk)) + data.set(ser32(index), 33) + } + return hmac(key, data) + .then(I => { + const IL = I.slice(0, I.byteLength / 2) + const IR = I.slice(I.byteLength / 2) + if (curve === 'ed25519 seed') { + return ({ privateKey: IL, chainCode: IR }) + } else { + const ILparsed = parse256(new Uint8Array(IL)) + if (ILparsed >= Point.CURVE().n) { + throw new Error('Invalid child key is greater than the order of the curve') + } + const pkParsed = parse256(pk) + const childKey = (ILparsed + pkParsed) % Point.CURVE().n + if (childKey === 0n) { + throw new Error('Invalid child key is zero') + } + return ({ privateKey: ser256(childKey).buffer, chainCode: IR }) + } + }) +} - static parse256 (integer: Uint8Array): bigint { - let result = 0n - for (let i = 0; i < integer.byteLength; i++) { - result = (result << 8n) | BigInt(integer[i]) - } - return result +function ser32 (integer: number): Uint8Array { + const view = new DataView(new ArrayBuffer(4)) + view.setUint32(0, integer, false) + return new Uint8Array(view.buffer) +} + +function ser256 (integer: bigint): Uint8Array { + let bytes = new Uint8Array(32) + for (let i = bytes.byteLength - 1; i >= 0; i--) { + bytes[i] = Number(integer & 0xffn) + integer >>= 8n } + return bytes +} - static hmac (key: Uint8Array, data: Uint8Array): Promise { - return crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign']) - .then(pk => { - return crypto.subtle.sign('HMAC', pk, data) - }) +function parse256 (integer: Uint8Array): bigint { + let result = 0n + for (let i = 0; i < integer.byteLength; i++) { + result = (result << 8n) | BigInt(integer[i]) } + return result +} + +function hmac (key: Uint8Array, data: Uint8Array): Promise { + return crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign']) + .then(pk => { + return crypto.subtle.sign('HMAC', pk, data) + }) } -- 2.47.3