From 1620afb8ee91f555ea2c05be92303016f5a95f84 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sun, 23 Nov 2025 02:36:49 -0800 Subject: [PATCH] Add support for SLIP-0010 'Bitcoin seed' using secp256k1. --- package-lock.json | 19 ++++++++- package.json | 1 + src/lib/crypto/bip44.ts | 86 +++++++++++++++++++++++++++++++---------- 3 files changed, 85 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3cfb296..9e4b822 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ledgerhq/hw-transport-web-ble": "^6.29.12", "@ledgerhq/hw-transport-webhid": "^6.30.8", "@ledgerhq/hw-transport-webusb": "^6.29.12", + "@noble/secp256k1": "^3.0.0", "nano-pow": "^5.1.8" }, "devDependencies": { @@ -569,6 +570,15 @@ "integrity": "sha512-4+qRW2Pc8V+btL0QEmdB2X+uyx0kOWMWE1/LWsq5sZy3Q5tpi4eItJS6mB0XL3wGW59RQ+8bchNQQ1OW/va8Og==", "license": "Apache-2.0" }, + "node_modules/@noble/secp256k1": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz", + "integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@puppeteer/browsers": { "version": "2.10.11", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.11.tgz", @@ -997,6 +1007,14 @@ "node": ">= 14" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1548823", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1548823.tgz", + "integrity": "sha512-VPuIGAx/GgqzNwyzfss5dFD/PDZMnaFaeEIb/3GeX0aKO5K5igm1BB7nYy0WXxhL9LA7vhI1XXbGi09t6cjGIA==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1808,7 +1826,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 386bb59..fd33b1c 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@ledgerhq/hw-transport-web-ble": "^6.29.12", "@ledgerhq/hw-transport-webhid": "^6.30.8", "@ledgerhq/hw-transport-webusb": "^6.29.12", + "@noble/secp256k1": "^3.0.0", "nano-pow": "^5.1.8" }, "devDependencies": { diff --git a/src/lib/crypto/bip44.ts b/src/lib/crypto/bip44.ts index b0989bd..1007f17 100644 --- a/src/lib/crypto/bip44.ts +++ b/src/lib/crypto/bip44.ts @@ -1,31 +1,42 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +import { getPublicKey } from '@noble/secp256k1' + type ExtendedKey = { privateKey: ArrayBuffer chainCode: ArrayBuffer + publicKey?: ArrayBuffer } +type Curve = 'Bitcoin seed' | 'ed25519 seed' + export class Bip44 { static get BIP44_PURPOSE (): 44 { return 44 } static get HARDENED_OFFSET (): 0x80000000 { return 0x80000000 } - static get SLIP10_ED25519 (): 'ed25519 seed' { return 'ed25519 seed' } + static get SECP256K1_N (): bigint { + return 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n + } /** * Derives a private child key for a coin by following the specified BIP-32 and - * BIP-44 derivation path. Purpose is always 44'. Only hardened child keys are - * defined. + * 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-044 + * @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 (seed: ArrayBuffer, coin: number, account: number, change?: number, address?: number): Promise { + 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 length`) + 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)}`) @@ -39,12 +50,12 @@ export class Bip44 { if (address !== undefined && (!Number.isSafeInteger(address) || address < 0 || address > 0x7fffffff)) { throw new RangeError(`Invalid address index 0x${account.toString(16)}`) } - return this.slip10(this.SLIP10_ED25519, seed) - .then(masterKey => this.CKDpriv(masterKey, this.BIP44_PURPOSE)) - .then(purposeKey => this.CKDpriv(purposeKey, coin)) - .then(coinKey => this.CKDpriv(coinKey, account)) - .then(accountKey => this.CKDpriv(accountKey, change)) - .then(chainKey => this.CKDpriv(chainKey, address)) + 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) } @@ -59,21 +70,44 @@ export class Bip44 { }) } - static CKDpriv ({ privateKey, chainCode }: ExtendedKey, index?: number): Promise { + static CKDpriv (curve: Curve, { privateKey, chainCode }: ExtendedKey, index?: number): Promise { + console.log(index) + console.log([...(new Uint8Array(privateKey))].map(v => v.toString(16).padStart(2, '0')).join('')) + console.log([...(new Uint8Array(chainCode))].map(v => v.toString(16).padStart(2, '0')).join('')) if (index === undefined) { return Promise.resolve({ privateKey, chainCode }) } - index += this.HARDENED_OFFSET + const pk = new Uint8Array(privateKey) const key = new Uint8Array(chainCode) const data = new Uint8Array(37) - data.set([0]) - data.set(this.ser256(privateKey), 1) - data.set(this.ser32(index), 33) + 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(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) - return ({ privateKey: IL, chainCode: IR }) + if (curve === 'ed25519 seed') { + return ({ privateKey: IL, chainCode: IR }) + } else { + const ILparsed = this.parse256(new Uint8Array(IL)) + if (ILparsed >= this.SECP256K1_N) { + throw new Error('Invalid child key is greater than the order of the curve') + } + const pkParsed = this.parse256(pk) + const childKey = (ILparsed + pkParsed) % this.SECP256K1_N + if (childKey === 0n) { + throw new Error('Invalid child key is zero') + } + return ({ privateKey: this.ser256(childKey).buffer, chainCode: IR }) + } }) } @@ -83,8 +117,20 @@ export class Bip44 { return new Uint8Array(view.buffer) } - static ser256 (integer: ArrayBuffer): Uint8Array { - return new Uint8Array(integer) + static ser256 (integer: bigint): Uint8Array { + let bytes = new Uint8Array(32) + for (let i = bytes.byteLength - 1; i >= 0; i--) { + bytes[i] = Number((integer >> BigInt(i * 8)) & 0xffn) + } + return bytes + } + + static parse256 (integer: Uint8Array): bigint { + let result = 0n + for (let i = 0; i < integer.byteLength; i++) { + result = (result << 8n) | BigInt(integer[i]) + } + return result } static hmac (key: Uint8Array, data: Uint8Array): Promise { -- 2.47.3