From bae262ce1f84e1fdf547623d63c0a2e9a5763561 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 20 Nov 2024 21:04:42 -0800 Subject: [PATCH] Organize ckd workers by splitting into separate files according to algorithm. --- src/lib/workers/ckdBip44.ts | 115 ++++++++++++++++++++++ src/lib/{ckd.ts => workers/ckdBlake2b.ts} | 31 +----- 2 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 src/lib/workers/ckdBip44.ts rename src/lib/{ckd.ts => workers/ckdBlake2b.ts} (62%) diff --git a/src/lib/workers/ckdBip44.ts b/src/lib/workers/ckdBip44.ts new file mode 100644 index 0000000..a1f0175 --- /dev/null +++ b/src/lib/workers/ckdBip44.ts @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// 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} +*/ +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 { + 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 { + 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 { + 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 { + 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 { + 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)) +} diff --git a/src/lib/ckd.ts b/src/lib/workers/ckdBlake2b.ts similarity index 62% rename from src/lib/ckd.ts rename to src/lib/workers/ckdBlake2b.ts index 8566532..e807fd3 100644 --- a/src/lib/ckd.ts +++ b/src/lib/workers/ckdBlake2b.ts @@ -2,7 +2,6 @@ // 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 @@ -15,40 +14,18 @@ if (addEventListener == null || postMessage == null) { } /** -* Derives BIP-44 Nano account private keys. -* -* @param {number} index - Index of the account -* @returns {Promise} +* 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} -*/ -async function ckdBip44 (seed: string, index: number): Promise { - return nanoCKD(seed, index) -} - /** * Derives BLAKE2b account private keys. * * @param {number} index - Index of the account -* @returns {Promise} +* @returns {Promise} */ async function ckdBlake2b (seed: string, index: number): Promise { const indexHex = index.toString(16).padStart(8, '0').toUpperCase() -- 2.34.1