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<ArrayBuffer>} Private child key for the account
- */
- 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 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<ArrayBuffer>} Private child key for the account
+ */
+export function 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}`)
}
-
- static slip10 (curve: string, S: ArrayBuffer): Promise<ExtendedKey> {
- 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<ExtendedKey> {
- 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<ArrayBuffer> {
- 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<ExtendedKey> {
+ 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<ArrayBuffer> {
- 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<ExtendedKey> {
+ 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<ArrayBuffer>): 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<ArrayBuffer> {
+ const view = new DataView(new ArrayBuffer(4))
+ view.setUint32(0, integer, false)
+ return new Uint8Array(view.buffer)
+}
+
+function ser256 (integer: bigint): Uint8Array<ArrayBuffer> {
+ 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<ArrayBuffer>, data: Uint8Array<ArrayBuffer>): Promise<ArrayBuffer> {
- 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<ArrayBuffer>): bigint {
+ let result = 0n
+ for (let i = 0; i < integer.byteLength; i++) {
+ result = (result << 8n) | BigInt(integer[i])
}
+ return result
+}
+
+function hmac (key: Uint8Array<ArrayBuffer>, data: Uint8Array<ArrayBuffer>): Promise<ArrayBuffer> {
+ return crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign'])
+ .then(pk => {
+ return crypto.subtle.sign('HMAC', pk, data)
+ })
}