--- /dev/null
+// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
+// SPDX-License-Identifier: GPL-3.0-or-later
+import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } from '../constants.js'
+import { bytes, dec, hex, utf8 } from '../convert.js'
+
+type ExtendedKey = {
+ privateKey: string
+ chainCode: string
+}
+
+let addEventListener = globalThis.addEventListener
+let postMessage = globalThis.postMessage
+if (addEventListener == null || postMessage == null) {
+ const { isMainThread, parentPort } = await import('node:worker_threads')
+ if (!isMainThread && parentPort) {
+ addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort)
+ postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort)
+ }
+}
+
+/**
+* Derives BIP-44 Nano account private keys.
+*
+* @param {number} index - Index of the account
+* @returns {Promise<Account>}
+*/
+addEventListener('message', (message) => {
+ const { seed, index } = message.data ?? message
+ ckd(seed, index).then(postMessage)
+})
+
+/**
+* Derives a private child key following the BIP-32 and BIP-44 derivation path
+* registered to the Nano block lattice. Only hardened child keys are defined.
+*
+* @param {string} seed - Hexadecimal seed derived from mnemonic phrase
+* @param {number} index - Account number between 0 and 2^31-1
+* @returns Private child key for the account
+*/
+async function ckd (seed: string, index: number): Promise<string> {
+ if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) {
+ throw new RangeError(`Invalid child key index 0x${index.toString(16)}`)
+ }
+ const masterKey = await slip10(SLIP10_ED25519, seed)
+ const purposeKey = await CKDpriv(masterKey, BIP44_PURPOSE + HARDENED_OFFSET)
+ const coinKey = await CKDpriv(purposeKey, BIP44_COIN_NANO + HARDENED_OFFSET)
+ const accountKey = await CKDpriv(coinKey, index + HARDENED_OFFSET)
+ return accountKey.privateKey
+}
+
+/**
+* Derives a private child key following the BIP-32 and BIP-44 derivation path
+* registered to the Nano block lattice. Only hardened child keys are defined.
+*
+* @param {string} seed - Hexadecimal seed derived from mnemonic phrase
+* @param {number} index - Account number between 0 and 2^31-1
+* @returns Private child key for the account
+*/
+export async function nanoCKD (seed: string, index: number): Promise<string> {
+ if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) {
+ throw new RangeError(`Invalid child key index 0x${index.toString(16)}`)
+ }
+ const masterKey = await slip10(SLIP10_ED25519, seed)
+ const purposeKey = await CKDpriv(masterKey, BIP44_PURPOSE + HARDENED_OFFSET)
+ const coinKey = await CKDpriv(purposeKey, BIP44_COIN_NANO + HARDENED_OFFSET)
+ const accountKey = await CKDpriv(coinKey, index + HARDENED_OFFSET)
+ return accountKey.privateKey
+}
+
+async function slip10 (curve: string, S: string): Promise<ExtendedKey> {
+ const key = utf8.toBytes(curve)
+ const data = hex.toBytes(S)
+ const I = await hmac(key, data)
+ const IL = I.slice(0, I.length / 2)
+ const IR = I.slice(I.length / 2)
+ return ({ privateKey: IL, chainCode: IR })
+}
+
+async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise<ExtendedKey> {
+ const key = hex.toBytes(chainCode)
+ const data = hex.toBytes(`00${bytes.toHex(ser256(privateKey))}${bytes.toHex(ser32(index))}`)
+ const I = await hmac(key, data)
+ const IL = I.slice(0, I.length / 2)
+ const IR = I.slice(I.length / 2)
+ return ({ privateKey: IL, chainCode: IR })
+}
+
+function ser32 (integer: number): Uint8Array {
+ if (typeof integer !== 'number') {
+ throw new TypeError(`Expected a number, received ${typeof integer}`)
+ }
+ const bits = dec.toBin(integer)
+ if (bits.length > 32) {
+ throw new RangeError(`Expected 32-bit integer, received ${bits.length}-bit value: ${integer}`)
+ }
+ return dec.toBytes(integer, 4)
+}
+
+function ser256 (integer: string): Uint8Array {
+ if (typeof integer !== 'string') {
+ throw new TypeError(`Expected string, received ${typeof integer}`)
+ }
+ const bits = hex.toBin(integer)
+ if (bits.length > 256) {
+ throw new RangeError(`Expected 256-bit integer, received ${bits.length}-bit value: ${integer}`)
+ }
+ return hex.toBytes(integer, 32)
+}
+
+async function hmac (key: Uint8Array, data: Uint8Array): Promise<string> {
+ const { subtle } = globalThis.crypto
+ const pk = await subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign'])
+ const signature = await subtle.sign('HMAC', pk, data)
+ return bytes.toHex(new Uint8Array(signature))
+}
// SPDX-License-Identifier: GPL-3.0-or-later
import blake2b from 'blake2b'
-import { nanoCKD } from './bip32-key-derivation.js'
let addEventListener = globalThis.addEventListener
let postMessage = globalThis.postMessage
}
/**
-* Derives BIP-44 Nano account private keys.
-*
-* @param {number} index - Index of the account
-* @returns {Promise<Account>}
+* Listens for messages from a calling function.
*/
addEventListener('message', (message) => {
- const { type, seed, index } = message.data ?? message
- switch (type) {
- case 'bip44': {
- ckdBip44(seed, index).then(postMessage)
- break
- }
- case 'blake2b': {
- ckdBlake2b(seed, index).then(postMessage)
- break
- }
- }
+ const { seed, index } = message.data ?? message
+ ckdBlake2b(seed, index).then(key => postMessage({ index, key }))
})
-/**
-* Derives BIP-44 Nano account private keys.
-*
-* @param {number} index - Index of the account
-* @returns {Promise<Account>}
-*/
-async function ckdBip44 (seed: string, index: number): Promise<string> {
- return nanoCKD(seed, index)
-}
-
/**
* Derives BLAKE2b account private keys.
*
* @param {number} index - Index of the account
-* @returns {Promise<Account>}
+* @returns {Promise<string>}
*/
async function ckdBlake2b (seed: string, index: number): Promise<string> {
const indexHex = index.toString(16).padStart(8, '0').toUpperCase()