"@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": {
"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",
"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",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! 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<ArrayBuffer>} Private child key for the account
*/
- static ckd (seed: ArrayBuffer, coin: number, account: number, change?: number, address?: number): Promise<ArrayBuffer> {
+ static ckd (curve: Curve, seed: ArrayBuffer, coin: number, account: number, change?: number, address?: number): Promise<ArrayBuffer> {
+ 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)}`)
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)
}
})
}
- static CKDpriv ({ privateKey, chainCode }: ExtendedKey, index?: number): Promise<ExtendedKey> {
+ static CKDpriv (curve: Curve, { privateKey, chainCode }: ExtendedKey, index?: number): Promise<ExtendedKey> {
+ 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 })
+ }
})
}
return new Uint8Array(view.buffer)
}
- static ser256 (integer: ArrayBuffer): Uint8Array<ArrayBuffer> {
- return new Uint8Array(integer)
+ static ser256 (integer: bigint): Uint8Array<ArrayBuffer> {
+ 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<ArrayBuffer>): bigint {
+ let result = 0n
+ for (let i = 0; i < integer.byteLength; i++) {
+ result = (result << 8n) | BigInt(integer[i])
+ }
+ return result
}
static hmac (key: Uint8Array<ArrayBuffer>, data: Uint8Array<ArrayBuffer>): Promise<ArrayBuffer> {