]> zoso.dev Git - libnemo.git/commitdiff
Organize ckd workers by splitting into separate files according to algorithm.
authorChris Duncan <chris@zoso.dev>
Thu, 21 Nov 2024 05:04:42 +0000 (21:04 -0800)
committerChris Duncan <chris@zoso.dev>
Thu, 21 Nov 2024 05:04:42 +0000 (21:04 -0800)
src/lib/workers/ckdBip44.ts [new file with mode: 0644]
src/lib/workers/ckdBlake2b.ts [moved from src/lib/ckd.ts with 62% similarity]

diff --git a/src/lib/workers/ckdBip44.ts b/src/lib/workers/ckdBip44.ts
new file mode 100644 (file)
index 0000000..a1f0175
--- /dev/null
@@ -0,0 +1,115 @@
+// 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))
+}
similarity index 62%
rename from src/lib/ckd.ts
rename to src/lib/workers/ckdBlake2b.ts
index 85665327d7961bbd65686ce3014d52983fa240b8..e807fd36bc262038c0856c6d5febc4edd05d295c 100644 (file)
@@ -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<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()