From 16c6cf3a5da12278548fac3e98f9a4ffedc4a70b Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 24 Aug 2026 17:38:02 -0700 Subject: [PATCH] Deprecate async module and focus on organizing buffers for batch prep. --- esbuild/config.mjs | 7 -- esbuild/inject/worker.mjs | 34 ------ src/assembly/crypto_verify.ts | 34 +++++- src/assembly/index.ts | 143 +++++++++++++++++------- src/async.ts | 62 ----------- src/index.ts | 104 +++++++++++++++++- src/lib/derive.ts | 39 +++++++ src/lib/host.ts | 168 ----------------------------- src/lib/index.ts | 7 ++ src/lib/nano25519.ts | 198 ---------------------------------- src/lib/sign.ts | 48 +++++++++ src/lib/verify.ts | 48 +++++++++ src/lib/wasm.ts | 116 ++++++++++++++++++++ src/lib/worker.ts | 99 ----------------- src/sync.ts | 80 -------------- test/node.mjs | 79 +------------- tsconfig.json | 4 +- 17 files changed, 498 insertions(+), 772 deletions(-) delete mode 100644 esbuild/inject/worker.mjs delete mode 100644 src/async.ts create mode 100644 src/lib/derive.ts delete mode 100644 src/lib/host.ts create mode 100644 src/lib/index.ts delete mode 100644 src/lib/nano25519.ts create mode 100644 src/lib/sign.ts create mode 100644 src/lib/verify.ts create mode 100644 src/lib/wasm.ts delete mode 100644 src/lib/worker.ts delete mode 100644 src/sync.ts diff --git a/esbuild/config.mjs b/esbuild/config.mjs index a0b77d7..d3806b0 100644 --- a/esbuild/config.mjs +++ b/esbuild/config.mjs @@ -1,7 +1,6 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { browserWorker, nodeWorker } from './inject/worker.mjs' /** * @type import('esbuild').BuildOptions */ @@ -28,9 +27,6 @@ export const browserOptions = { { in: 'src/index.ts', out: 'browser' } ], dropLabels: ['NODE'], - define: { - 'NANO25519_WORKER': browserWorker - }, } /** @@ -47,7 +43,4 @@ export const nodeOptions = { entryNames: '[name]', external: ['node:worker_threads'], dropLabels: ['BROWSER'], - define: { - 'NANO25519_WORKER': nodeWorker - }, } diff --git a/esbuild/inject/worker.mjs b/esbuild/inject/worker.mjs deleted file mode 100644 index 5d5602a..0000000 --- a/esbuild/inject/worker.mjs +++ /dev/null @@ -1,34 +0,0 @@ -//! SPDX-FileCopyrightText: 2025 Chris Duncan -//! SPDX-License-Identifier: GPL-3.0-or-later - -import { build } from 'esbuild' - -/** - * @type {import('esbuild').BuildOptions} - */ -const workerOptions = { - bundle: true, - loader: { - '.wasm': 'binary', - }, - entryPoints: ['./src/lib/worker.ts'], - format: 'esm', - legalComments: 'inline', - write: false, - drop: ['console', 'debugger'], - minify: true, -} - -export const browserWorker = JSON.stringify((await build({ - ...workerOptions, - platform: 'browser', - target: 'es2022', - dropLabels: ['NODE'], -})).outputFiles?.[0].text) - -export const nodeWorker = JSON.stringify((await build({ - ...workerOptions, - platform: 'node', - target: 'node22', - dropLabels: ['BROWSER'], -})).outputFiles?.[0].text) diff --git a/src/assembly/crypto_verify.ts b/src/assembly/crypto_verify.ts index e29ed40..bab9018 100644 --- a/src/assembly/crypto_verify.ts +++ b/src/assembly/crypto_verify.ts @@ -21,7 +21,39 @@ const S = new StaticArray(32) * Verify signature `s` was made by signing message `M` using public key `pub`. * @returns -1 if signature fails to verify, else return 0 if signature is good */ -export function crypto_verify (s: StaticArray, M: StaticArray, mlen: i32, pub: StaticArray): i32 { +export function crypto_verify_strict (s: StaticArray, M: StaticArray, mlen: i32, pub: StaticArray): i32 { + + // fail if public key `k` is non-canonical (`p = 2²⁵⁵-19 ≤ k`) + if (!ge_is_canonical(pub)) return -1 + + // fail if private scalar `S` is non-canonical (`L ≤ S`) + memory.copy(changetype(S), changetype(s) + 32, 32) + if (!sc_is_canonical(S)) return -1 + + if (ge_frombytes_negate_vartime(A, pub) != 0) return -1 + if (ge_has_small_order(A) != 0) return -1 + + if (ge_frombytes(expected_r, s) != 0) return -1 + if (ge_has_small_order(expected_r) != 0) return -1 + + // signature is nonce point R and scalar S (R || S) + // data to hash is nonce point R, public key A, and message M + // from parameter arguments: R = s[0,32], A = pk, M = m + // R, S, A, and M are all 32-byte values in this implementation + blake2b.init().update(s, 32).update(pub, 32).update(M, mlen).digest(h) + sc_reduce(h) + + ge_double_scalarmult_vartime_to_p3(sb_ah, h, A, S) + ge_sub_p3(check, expected_r, sb_ah) + + return ge_has_small_order(check) - 1 +} + +/** + * Verify signature `s` was made by signing message `M` using public key `pub`. + * @returns -1 if signature fails to verify, else return 0 if signature is good + */ +export function crypto_verify_relaxed (s: StaticArray, M: StaticArray, mlen: i32, pub: StaticArray): i32 { // fail if public key `k` is non-canonical (`p = 2²⁵⁵-19 ≤ k`) if (!ge_is_canonical(pub)) return -1 diff --git a/src/assembly/index.ts b/src/assembly/index.ts index 343ad17..4e4e2c5 100644 --- a/src/assembly/index.ts +++ b/src/assembly/index.ts @@ -3,37 +3,52 @@ import { crypto_derive } from './crypto_derive' import { crypto_sign } from './crypto_sign' -import { crypto_verify } from './crypto_verify' +import { crypto_verify_relaxed, crypto_verify_strict } from './crypto_verify' -const PRIVATEKEY_BYTES: i32 = 32 -const PUBLICKEY_BYTES: i32 = 32 -const SIGNATURE_BYTES: i32 = 64 +export const BLOCKHASH_BYTELENGTH: i32 = 32 +export const KEY_BYTELENGTH: i32 = 32 +export const SIGNATURE_BYTELENGTH: i32 = 64 + +export const MESSAGE_BUFFER_BYTELENGTH: i32 = BLOCKHASH_BYTELENGTH << 10 +export const PRV_BUFFER_BYTELENGTH: i32 = KEY_BYTELENGTH +export const PUB_BUFFER_BYTELENGTH: i32 = KEY_BYTELENGTH +export const OUTPUT_BUFFER_BYTELENGTH: i32 = 1 << 10 +export const SIGNATURE_BUFFER_BYTELENGTH: i32 = SIGNATURE_BYTELENGTH << 10 // Static I/O buffers -export const INPUT_BUFFER_BYTES: i32 = SIGNATURE_BYTES + PUBLICKEY_BYTES -export const MESSAGE_BUFFER_BYTES: i32 = 32768 -export const OUTPUT_BUFFER_BYTES: i32 = SIGNATURE_BYTES -const INPUT_BUFFER = new StaticArray(INPUT_BUFFER_BYTES) -const MESSAGE_BUFFER = new StaticArray(MESSAGE_BUFFER_BYTES) -const OUTPUT_BUFFER = new StaticArray(OUTPUT_BUFFER_BYTES) - -/** Returns the pointer to the static input buffer (96 bytes). */ -export function getInputPointer (): usize { - return changetype(INPUT_BUFFER) -} +const MESSAGE_BUFFER = new StaticArray(MESSAGE_BUFFER_BYTELENGTH) +const OUTPUT_BUFFER = new StaticArray(OUTPUT_BUFFER_BYTELENGTH) +const PRV_BUFFER = new StaticArray(PRV_BUFFER_BYTELENGTH) +const PUB_BUFFER = new StaticArray(PUB_BUFFER_BYTELENGTH) +const SIGNATURE_BUFFER = new StaticArray(SIGNATURE_BUFFER_BYTELENGTH) -/** Returns the pointer to the static message buffer (32 KiB). */ +/** Returns the pointer to the static message input buffer (32 KiB). */ export function getMessagePointer (): usize { return changetype(MESSAGE_BUFFER) } -/** Returns the pointer to the static output buffer (64 bytes). */ +/** Returns the pointer to the static output buffer (1024 bytes). */ export function getOutputPointer (): usize { return changetype(OUTPUT_BUFFER) } -const derive_prv = new StaticArray(PRIVATEKEY_BYTES) -const derive_pub = new StaticArray(PUBLICKEY_BYTES) +/** Returns the pointer to the static private key input buffer (32 bytes). */ +export function getPrivateKeyPointer (): usize { + return changetype(PRV_BUFFER) +} + +/** Returns the pointer to the static public key input buffer (32 bytes). */ +export function getPublicKeyPointer (): usize { + return changetype(PUB_BUFFER) +} + +/** Returns the pointer to the static signature input buffer (64 bytes). */ +export function getSignaturePointer (): usize { + return changetype(SIGNATURE_BUFFER) +} + +const derive_prv = new StaticArray(KEY_BYTELENGTH) +const derive_pub = new StaticArray(KEY_BYTELENGTH) /** * Derive a 32-byte Nano public key from a 32-byte private key. Parameters are * read as bytes from the input buffer in the following order: @@ -48,8 +63,8 @@ export function derive (): void { derive_pub.fill(0) // Copy input buffer to local parameterss, then clear input buffer - memory.copy(changetype(derive_prv), changetype(INPUT_BUFFER), PRIVATEKEY_BYTES) - INPUT_BUFFER.fill(0) + memory.copy(changetype(derive_prv), changetype(PRV_BUFFER), KEY_BYTELENGTH) + PRV_BUFFER.fill(0) // Derive, then clear local input crypto_derive(derive_pub, derive_prv) @@ -57,15 +72,15 @@ export function derive (): void { // Clear output buffer of prior data, then copy local result to output buffer OUTPUT_BUFFER.fill(0) - memory.copy(changetype(OUTPUT_BUFFER), changetype(derive_pub), PUBLICKEY_BYTES) + memory.copy(changetype(OUTPUT_BUFFER), changetype(derive_pub), KEY_BYTELENGTH) // Clear local result derive_pub.fill(0) } -const sign_prv = new StaticArray(PRIVATEKEY_BYTES) -const sign_pub = new StaticArray(PUBLICKEY_BYTES) -const sign_sig = new StaticArray(SIGNATURE_BYTES) +const sign_prv = new StaticArray(KEY_BYTELENGTH) +const sign_pub = new StaticArray(KEY_BYTELENGTH) +const sign_sig = new StaticArray(SIGNATURE_BYTELENGTH) /** * Sign up to 32 KiB of data using a 64-byte secret key. Parameters are read as * bytes from the input buffer in the following order: @@ -78,8 +93,9 @@ const sign_sig = new StaticArray(SIGNATURE_BYTES) * @param {i32} mlen Byte length of message to be signed, up to 32768 */ export function sign (mlen: i32): void { - if (mlen < 0 || mlen > MESSAGE_BUFFER_BYTES) { - INPUT_BUFFER.fill(0) + if (mlen < 0 || mlen > MESSAGE_BUFFER_BYTELENGTH) { + PRV_BUFFER.fill(0) + PUB_BUFFER.fill(0) MESSAGE_BUFFER.fill(0) throw new Error('invalid message length') } @@ -89,10 +105,11 @@ export function sign (mlen: i32): void { sign_pub.fill(0) sign_sig.fill(0) - // Copy input buffer to local parameters, then clear input buffer - memory.copy(changetype(sign_prv), changetype(INPUT_BUFFER), PRIVATEKEY_BYTES) - memory.copy(changetype(sign_pub), changetype(INPUT_BUFFER) + PRIVATEKEY_BYTES, PUBLICKEY_BYTES) - INPUT_BUFFER.fill(0) + // Copy input buffers to local parameters, then clear input buffer + memory.copy(changetype(sign_prv), changetype(PRV_BUFFER), KEY_BYTELENGTH) + PRV_BUFFER.fill(0) + memory.copy(changetype(sign_pub), changetype(PUB_BUFFER), KEY_BYTELENGTH) + PUB_BUFFER.fill(0) // Sign message from buffer, then clear local input crypto_sign(sign_sig, MESSAGE_BUFFER, mlen, sign_prv, sign_pub) @@ -102,14 +119,14 @@ export function sign (mlen: i32): void { // Clear output buffer of prior data, then copy local result to output buffer OUTPUT_BUFFER.fill(0) - memory.copy(changetype(OUTPUT_BUFFER), changetype(sign_sig), SIGNATURE_BYTES) + memory.copy(changetype(OUTPUT_BUFFER), changetype(sign_sig), SIGNATURE_BYTELENGTH) // Clear local result sign_sig.fill(0) } -const verify_pub = new StaticArray(PUBLICKEY_BYTES) -const verify_sig = new StaticArray(SIGNATURE_BYTES) +const verify_pub = new StaticArray(KEY_BYTELENGTH) +const verify_sig = new StaticArray(SIGNATURE_BYTELENGTH) /** * Verify a 64-byte detached signature for a variable-length message against a * 32-byte public key. Parameters are read as bytes from the input buffer in the @@ -123,8 +140,53 @@ const verify_sig = new StaticArray(SIGNATURE_BYTES) * @returns {boolean} True if message was signed by public key's private key */ export function verify (mlen: i32): i32 { - if (mlen < 0 || mlen > MESSAGE_BUFFER_BYTES) { - INPUT_BUFFER.fill(0) + if (mlen < 0 || mlen > MESSAGE_BUFFER_BYTELENGTH) { + PRV_BUFFER.fill(0) + PUB_BUFFER.fill(0) + MESSAGE_BUFFER.fill(0) + throw new Error('invalid message length') + } + + // Clear local buffers + verify_pub.fill(0) + verify_sig.fill(0) + + // Copy input buffer to local parameters, then clear input buffer + memory.copy(changetype(verify_pub), changetype(PUB_BUFFER), KEY_BYTELENGTH) + PUB_BUFFER.fill(0) + memory.copy(changetype(verify_sig), changetype(SIGNATURE_BUFFER), SIGNATURE_BYTELENGTH) + SIGNATURE_BUFFER.fill(0) + + // Verify message from buffer, then clear local input + const verified = crypto_verify_strict(verify_sig, MESSAGE_BUFFER, mlen, verify_pub) + MESSAGE_BUFFER.fill(0) + verify_pub.fill(0) + verify_sig.fill(0) + + // Output buffer not used, so just return + return verified +} + + +const verify_blocks_pub = new StaticArray(KEY_BYTELENGTH) +const verify_blocks_sig = new StaticArray(SIGNATURE_BYTELENGTH) +/** + * Verify a 64-byte detached signature for a 32-byte Nano block hash against a + * 32-byte public key. Parameters are read as bytes from the input buffer in the + * following order: + * + * - `[0,63]: signature` + * - `[64,95]: public key` + * + * The message buffer can hold up 1024 block hashes to verify many signatures + * for a single public key. + * @param {u64} mlen Byte length of message that was signed, up to 32768 + * @returns {boolean} True if message was signed by public key's private key + */ +export function verify_blocks (mlen: i32): i32 { + if (mlen < 0 || mlen > MESSAGE_BUFFER_BYTELENGTH) { + PRV_BUFFER.fill(0) + PUB_BUFFER.fill(0) MESSAGE_BUFFER.fill(0) throw new Error('invalid message length') } @@ -134,12 +196,13 @@ export function verify (mlen: i32): i32 { verify_sig.fill(0) // Copy input buffer to local parameters, then clear input buffer - memory.copy(changetype(verify_sig), changetype(INPUT_BUFFER), SIGNATURE_BYTES) - memory.copy(changetype(verify_pub), changetype(INPUT_BUFFER) + SIGNATURE_BYTES, PUBLICKEY_BYTES) - INPUT_BUFFER.fill(0) + memory.copy(changetype(verify_sig), changetype(PUB_BUFFER), SIGNATURE_BYTELENGTH) + PUB_BUFFER.fill(0) + memory.copy(changetype(verify_pub), changetype(SIGNATURE_BUFFER), KEY_BYTELENGTH) + SIGNATURE_BUFFER.fill(0) // Verify message from buffer, then clear local input - const verified = crypto_verify(verify_sig, MESSAGE_BUFFER, mlen, verify_pub) + const verified = crypto_verify_relaxed(verify_sig, MESSAGE_BUFFER, mlen, verify_pub) MESSAGE_BUFFER.fill(0) verify_pub.fill(0) verify_sig.fill(0) diff --git a/src/async.ts b/src/async.ts deleted file mode 100644 index fe8bdac..0000000 --- a/src/async.ts +++ /dev/null @@ -1,62 +0,0 @@ -//! SPDX-FileCopyrightText: 2026 Chris Duncan -//! SPDX-License-Identifier: GPL-3.0-or-later - -import { run } from './lib/host' - -/** - * Asynchronous Nano public key derivation using WebAssembly. - * @param {Uint8Array} k - 64-character private key hex string - * @returns Promise for 64-character public key hex string - */ -export async function deriveAsync (privateKey: Uint8Array): Promise> -/** - * Asynchronous Nano public key derivation using WebAssembly. - * @param {string} k - 64-character private key hex string - * @returns Promise for 64-character public key hex string - */ -export async function deriveAsync (privateKey: string): Promise -export async function deriveAsync (privateKey: string | Uint8Array): Promise> { - return run({ action: 'derive', privateKey }) -} - -/** - * Asynchronous signing using WebAssembly. To sign Nano blocks, the message - * should be a 64-character block hash. - * @param {Uint8Array} m - Variable-length message hex string up to 65536 characters - * @param {Uint8Array} k - 128-character secret key (prv + pub) hex string - * @returns Promise for 128-character detached signature hex string - */ -export async function signAsync (message: Uint8Array, secretKey: Uint8Array): Promise> -/** - * Asynchronous signing using WebAssembly. To sign Nano blocks, the message - * should be a 64-character block hash. - * @param {Uint8Array} m - Variable-length message hex string up to 65536 characters - * @param {Uint8Array} k - 128-character secret key (prv + pub) hex string - * @returns Promise for 128-character detached signature hex string - */ -export async function signAsync (message: string, secretKey: string): Promise -export async function signAsync (message: string | Uint8Array, secretKey: string | Uint8Array): Promise> { - return run({ action: 'sign', message, secretKey }) -} - -/** - * Asynchronous signature verification using WebAssembly. To verify Nano block - * signatures, the message should be a 64-character block hash. - * @param {Uint8Array} s - 128-character detached signature hex string - * @param {Uint8Array} m - Variable-length message hex string to up 65536 characters - * @param {Uint8Array} k - 64-character public key hex string - * @returns Promise resolving to true if signature matches block hash and public key, else false - */ -export async function verifyAsync (signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): Promise -/** - * Asynchronous signature verification using WebAssembly. To verify Nano block - * signatures, the message should be a 64-character block hash. - * @param {Uint8Array} s - 128-character detached signature hex string - * @param {Uint8Array} m - Variable-length message hex string to up 65536 characters - * @param {Uint8Array} k - 64-character public key hex string - * @returns Promise resolving to true if signature matches block hash and public key, else false - */ -export async function verifyAsync (signature: string, message: string, publicKey: string): Promise -export async function verifyAsync (signature: string | Uint8Array, message: string | Uint8Array, publicKey: string | Uint8Array): Promise { - return run({ action: 'verify', signature, message, publicKey }) -} diff --git a/src/index.ts b/src/index.ts index 4cc42f0..aa6039d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,105 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -export { deriveAsync, signAsync, verifyAsync } from './async' -export { derive, sign, verify } from './sync' +import * as nano25519 from './lib' + +/** + * Nano public key derivation using WebAssembly. + * @param {Uint8Array} prv - 32-byte private key + * @returns 32-byte public key + */ +export function derive (prv: Uint8Array): Uint8Array +/** + * Nano public key derivation using WebAssembly. + * @param {string} prv - 64-character hexadecimal private key + * @returns 64-character hexadecimal public key + */ +export function derive (prv: string): string +/** + * Nano public key derivation using WebAssembly. Instead of allocating an output + * buffer internally for the return value, public key bytes are written to the + * the user-supplied output buffer. + * @param {Uint8Array} prv - 32-byte private key + * @param {Uint8Array} out - buffer to receive 32-byte public key + */ +export function derive (prv: Uint8Array, out: Uint8Array): void +export function derive (prv: string | Uint8Array, out?: Uint8Array): string | Uint8Array | void { + return nano25519.derive(prv, out) +} + +/** + * Signing using WebAssembly. To sign Nano blocks, the message should be a + * 32-byte block hash. + * @param {Uint8Array} msg - Variable-byte-length message up to 32 KiB + * @param {Uint8Array} prv - 32-byte private key + * @param {Uint8Array} pub - 32-byte public key + * @returns 64-byte detached signature + */ +export function sign (msg: Uint8Array, prv: Uint8Array, pub: Uint8Array): Uint8Array +/** + * Signing using WebAssembly. To sign Nano blocks, the message should be a + * 64-character hexadecimal block hash. + * @param {string} msg - Variable-length hexadecimal message up to 32 KiB + * @param {string} prv - 64-character hexadecimal private key + * @param {string} pub - 64-character hexadecimal public key + * @returns 128-character hexadecimal detached signature + */ +export function sign (msg: string, prv: string, pub: string): string +/** + * Signing using WebAssembly. To sign Nano blocks, the message should be a + * 32-byte block hash. Instead of allocating an output buffer internally for the + * return value, signature bytes are written to the the user-supplied output + * buffer. + * @param {Uint8Array} msg - Variable-byte-length message up to 32 KiB + * @param {Uint8Array} prv - 32-byte private key + * @param {Uint8Array} pub - 32-byte public key + * @param {Uint8Array} out - buffer to receive 64-byte detached signature + */ +export function sign (msg: Uint8Array, prv: Uint8Array, pub: Uint8Array, out: Uint8Array): void +export function sign (msg: string | Uint8Array, prv: string | Uint8Array, pub: string | Uint8Array, out?: Uint8Array): string | Uint8Array | void { + return nano25519.sign(msg, prv, pub, out) +} + +/** + * Signature verification using WebAssembly. To verify Nano block signatures, + * use `verify_blocks()` instead. + * @param {Uint8Array} sig - 64-byte detached signature + * @param {Uint8Array} msg - Variable-byte-length message up to 32 KiB + * @param {Uint8Array} pub - 32-byte public key + * @returns true if signature matches block hash and public key, else false + */ +export function verify (sign: Uint8Array, msg: Uint8Array, pub: Uint8Array): boolean +/** + * Signature verification using WebAssembly. To verify Nano block signatures, + * the message should be a 64-character block hash. + * @param {string} sig - 128-character hexadecimal detached signature + * @param {string} msg - Variable-length message up to 32 KiB + * @param {string} pub - 64-character hexadecimal public key + * @returns true if signature matches block hash and public key, else false + */ +export function verify (sign: string, msg: string, pub: string): boolean +export function verify (sig: string | Uint8Array, msg: string | Uint8Array, pub: string | Uint8Array): boolean { + return nano25519.verify(sig, msg, pub) +} + +/** + * Nano block signature verification using WebAssembly. + * @param {Uint8Array} sig - Buffer of up to 1024 64-byte detached signature (64 KiB) + * @param {Uint8Array} msg - Buffer of up to 1024 32-byte block hashes (32 KiB) + * @param {Uint8Array} pub - 32-byte public key + * @returns true if signature matches block hash and public key, else false + */ +export function verify_blocks (sign: Uint8Array, msg: Uint8Array, pub: Uint8Array): boolean +/** + * Signature verification using WebAssembly. To verify Nano block signatures, + * the message should be a 64-character block hash. + * @param {string} sig - 128-character hexadecimal detached signature + * @param {string} msg - Variable-length message up to 32 KiB + * @param {string} pub - 64-character hexadecimal public key + * @returns true if signature matches block hash and public key, else false + */ +export function verify_blocks (sign: string, msg: string, pub: string): boolean +export function verify_blocks (sig: string | Uint8Array, msg: string | Uint8Array, pub: string | Uint8Array): boolean { + return nano25519.verify_blocks(sig, msg, pub) +} + diff --git a/src/lib/derive.ts b/src/lib/derive.ts new file mode 100644 index 0000000..28e28f6 --- /dev/null +++ b/src/lib/derive.ts @@ -0,0 +1,39 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { clear, exports, isBytes, KEY_LEN, normalize, OUT_PTR, PRV_PTR } from './wasm' + +export function derive (prv: unknown, out?: unknown): string | Uint8Array | void { + if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === KEY_LEN)) { + throw new TypeError('Derive output buffer must be 32-byte Uint8Array') + } + const privateKey = normalize('private key', KEY_LEN, KEY_LEN, prv) + const publicKey = new Uint8Array(KEY_LEN) + let buffer = new Uint8Array(exports.memory.buffer) + try { + for (let i = 0; i < KEY_LEN; i++) { + buffer[PRV_PTR + i] = privateKey[i] + } + exports.derive() + buffer = new Uint8Array(exports.memory.buffer) + for (let i = 0; i < KEY_LEN; i++) { + publicKey[i] = buffer[OUT_PTR + i] + } + if (typeof prv === 'string') { + let hex = '' + for (const byte of publicKey) { + hex += byte.toString(16).padStart(2, '0') + } + return hex + } else if (isBytes(prv) && out != null) { + out.set(publicKey) + publicKey.fill(0) + return + } else { + return publicKey + } + } finally { + clear(buffer) + privateKey.fill(0) + } +} diff --git a/src/lib/host.ts b/src/lib/host.ts deleted file mode 100644 index 3548262..0000000 --- a/src/lib/host.ts +++ /dev/null @@ -1,168 +0,0 @@ -//! SPDX-FileCopyrightText: 2026 Chris Duncan -//! SPDX-License-Identifier: GPL-3.0-or-later - -import { UUID } from 'node:crypto' -import { Worker as NodeWorker } from 'node:worker_threads' - -type Action = 'derive' | 'sign' | 'start' | 'verify' - -//@ts-expect-error -const nano25519_worker = NANO25519_WORKER - -/** - * Host code for asynchronous Web Worker - */ -let isWorkerReady: boolean = false -let starting: Promise | undefined -let tasks: Map[0]>> = new Map() -let worker: Worker | NodeWorker -let url: string - -function isBytes (a: unknown): a is Uint8Array { - return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer -} - -// Create worker module -function init (): void { - try { - BROWSER: { - if (url) URL.revokeObjectURL(url) - url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' })) - worker = new Worker(url, { type: 'module' }) - worker.onmessage = report - worker.onerror = reset - } - NODE: { - worker = new NodeWorker(nano25519_worker, { - eval: true, - stderr: false, - stdout: false - }) - worker.on('message', report) - worker.on('error', reset) - } - console.log(`nano25519/async initialized`) - isWorkerReady = true - } catch (e: any) { - isWorkerReady = false - throw new Nano25519WorkerError(e) - } -} - -// Helper for environment-specific messaging to worker -function post (id: UUID, data: Record & Record<'action', Action>, transfer?: ArrayBuffer[]): void { - data.id = id - BROWSER: worker.postMessage(data, transfer) - NODE: worker.postMessage({ data }, transfer) -} - -// Parse and validate worker message -function report (msg: { data: Record }): void { - const { data } = msg - if (!('id' in data) || typeof data.id !== 'string') return - - const executor = tasks.get(data.id) - if (executor == null) return - const [ok, err] = executor - tasks.delete(data.id) - - const { result } = data - console.log('received result from worker') - if (typeof result !== 'boolean' && typeof result !== 'string' && !isBytes(result)) { - return err(`expected boolean, string, or bytes; received ${result?.constructor?.name ?? typeof result} '${result}`) - } - return ok(result) -} - -// Reconstruct worker when errors occur -function reset (): void { - console.warn(`nano25519 encountered an error. Reinitializing...`) - starting = undefined - for (const [_, err] of tasks.values()) { - err(new Nano25519WorkerError('worker reset, try again')) - } - tasks.clear() - worker.terminate() - init() -} - -// Check that the worker is running and listening before sending messages -async function start (): Promise { - return starting ??= new Promise((resolve, reject): void => { - console.log('starting worker') - if (!isWorkerReady) init() - const id = crypto.randomUUID() - tasks.set(id, [resolve, reject]) - post(id, { action: 'start' }) - }) -} - -// Send command and relevant data to nano25519 worker -async function dispatch (data: Record<'action', Action> & Record>): Promise { - const id = crypto.randomUUID() - const transfer: ArrayBuffer[] = [] - for (let k of Object.keys(data)) { - if (isBytes(data[k])) { - data[k] = data[k].slice().buffer - transfer.push(data[k]) - } - } - console.log('sending data to worker') - return new Promise((resolve, reject) => { - tasks.set(id, [resolve, reject]) - post(id, data, transfer) - }) -} - -export async function run (data: Record<'action', 'derive'> & Record>): Promise> -export async function run (data: Record<'action', 'sign'> & Record>): Promise> -export async function run (data: Record<'action', 'verify'> & Record>): Promise -export async function run (data: Record<'action', 'derive' | 'sign' | 'verify'> & Record>): Promise> { - try { - const result = await start() - if (result === 'listening') { - console.log('worker listening') - } else if (typeof result === 'string') { - throw new Error(result) - } else { - throw new Error('unknown error') - } - } catch (e: any) { - starting = undefined - throw new Nano25519WorkerError(e) - } - - const result = await dispatch(data) - switch (data.action) { - case 'derive': { - if ((isBytes(result) && result.byteLength === 32) || (typeof result === 'string' && /^[0-9a-f]{64}$/i.test(result))) { - return result - } - break - } - case 'sign': { - if ((isBytes(result) && result.byteLength === 64) || (typeof result === 'string' && /^[0-9a-f]{128}$/i.test(result))) { - return result - } - break - } - case 'verify': { - if (typeof result === 'boolean') { - return result - } - break - } - } - throw new Nano25519ResultError(data.action, result) -} - -class Nano25519WorkerError extends Error { - constructor (cause?: unknown) { - super(`async process error`, { cause }) - } -} -class Nano25519ResultError extends Error { - constructor (action: string, cause?: unknown) { - super(`${action} result invalid`, { cause }) - } -} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..7b00dca --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,7 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +export { derive } from './derive' +export { sign } from './sign' +export { verify, verify_blocks } from './verify' + diff --git a/src/lib/nano25519.ts b/src/lib/nano25519.ts deleted file mode 100644 index a5e1b21..0000000 --- a/src/lib/nano25519.ts +++ /dev/null @@ -1,198 +0,0 @@ -//! SPDX-FileCopyrightText: 2026 Chris Duncan -//! SPDX-License-Identifier: GPL-3.0-or-later - -//@ts-expect-error -import nano25519_wasm from '../../build/nano25519.wasm' - -type Exports = { - exports: { - derive: () => void - sign: (mlen: number) => void - verify: (mlen: number) => number - getInputPointer: () => number - getMessagePointer: () => number - getOutputPointer: () => number - INPUT_BUFFER_BYTES: WebAssembly.Global - MESSAGE_BUFFER_BYTES: WebAssembly.Global - OUTPUT_BUFFER_BYTES: WebAssembly.Global - memory: WebAssembly.Memory - } -} - -const mLen = 32768 -const wasm: Uint8Array = Uint8Array.from(nano25519_wasm) -const module = new WebAssembly.Module(wasm) -const { exports } = new WebAssembly.Instance(module, { - env: { - abort: (msg: any, file: any, row: any, col: any) => { - const getString = (pointer: number): string | null => { - const end = pointer + new Uint32Array(exports.memory.buffer)[pointer - 4 >>> 2] >>> 1 - const buf = new Uint16Array(exports.memory.buffer) - let start = pointer >>> 1 - let string = '' - while (end - start > 1024) { - string += String.fromCharCode(...buf.subarray(start, start += 1024)) - } - return string + String.fromCharCode(...buf.subarray(start, end)) - } - // ~lib/builtins/abort(~lib/string/String | null?, ~lib/string/String | null?, u32?, u32?) => void - msg >>>= 0 - file >>>= 0 - row >>>= 0 - col >>>= 0 - const message = `Nano25519WasmError: ${getString(msg)}, ${getString(file)}, row ${row}, col ${col}` - throw new Error(message) - } - } -}) as Exports -const IN_LEN = exports.INPUT_BUFFER_BYTES.value -const MSG_LEN = exports.MESSAGE_BUFFER_BYTES.value -const OUT_LEN = exports.OUTPUT_BUFFER_BYTES.value -const IN_PTR = exports.getInputPointer() -const MSG_PTR = exports.getMessagePointer() -const OUT_PTR = exports.getOutputPointer() - -export function derive (k: unknown, out?: unknown): string | Uint8Array | void { - if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === 32)) { - throw new TypeError('Derive output buffer must be 32-byte Uint8Array') - } - const privateKey = new Uint8Array(32) - const publicKey = new Uint8Array(32) - let buffer = new Uint8Array(exports.memory.buffer) - try { - privateKey.set(normalize('private key', 32, 32, k)) - for (let i = 0; i < 32; i++) { - buffer[IN_PTR + i] = privateKey[i] - } - exports.derive() - buffer = new Uint8Array(exports.memory.buffer) - for (let i = 0; i < 32; i++) { - publicKey[i] = buffer[OUT_PTR + i] - } - if (typeof k === 'string') { - let hex = '' - for (const byte of publicKey) { - hex += byte.toString(16).padStart(2, '0') - } - return hex - } else if (isBytes(k) && out != null) { - out.set(publicKey) - publicKey.fill(0) - return - } else { - return publicKey - } - } finally { - clear(buffer) - privateKey.fill(0) - } -} - -export function sign (m: unknown, k: unknown, s?: unknown): string | Uint8Array | void { - if (typeof s !== 'undefined' && !(isBytes(s) && s.byteLength === 64)) { - throw new TypeError('Sign output buffer must be 64-byte Uint8Array') - } - const secretKey = new Uint8Array(64) - const signature = new Uint8Array(64) - let buffer = new Uint8Array(exports.memory.buffer) - try { - secretKey.set(normalize('secret key', 64, 64, k)) - for (let i = 0; i < 64; i++) { - buffer[IN_PTR + i] = secretKey[i] - } - const message = normalize('message', 0, mLen, m) - for (let i = 0; i < message.byteLength; i++) { - buffer[MSG_PTR + i] = message[i] - } - exports.sign(message.byteLength) - buffer = new Uint8Array(exports.memory.buffer) - for (let i = 0; i < 64; i++) { - signature[i] = buffer[OUT_PTR + i] - } - if (typeof k === 'string') { - let hex = '' - for (const byte of signature) { - hex += byte.toString(16).padStart(2, '0') - } - return hex - } else if (isBytes(k) && s != null) { - s.set(signature) - signature.fill(0) - return - } else { - return signature - } - } finally { - clear(buffer) - secretKey.fill(0) - } -} - -export function verify (s: unknown, m: unknown, k: unknown): boolean { - let buffer = new Uint8Array(exports.memory.buffer) - try { - const signature = normalize('signature', 64, 64, s) - const message = normalize('message', 0, mLen, m) - const publicKey = normalize('public key', 32, 32, k) - for (let i = 0; i < message.byteLength; i++) { - buffer[MSG_PTR + i] = message[i] - } - for (let i = 0; i < 64; i++) { - buffer[IN_PTR + i] = signature[i] - } - for (let i = 0; i < 32; i++) { - buffer[IN_PTR + 64 + i] = publicKey[i] - } - const v = exports.verify(message.byteLength) - return v === 0 - } finally { - clear(buffer) - } -} - -function clear (memory: Uint8Array): void { - memory.fill(0, IN_PTR, IN_LEN) - memory.fill(0, MSG_PTR, MSG_LEN) - memory.fill(0, OUT_PTR, OUT_LEN) -} - -function isBytes (a: unknown): a is Uint8Array { - return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer -} - -function normalize (name: string, byteLengthMin: number, byteLengthMax: number, value: unknown): Uint8Array { - if (typeof name !== 'string') { - throw new TypeError(`Invalid name ${name}`) - } - if (typeof byteLengthMin !== 'number') { - throw new TypeError(`Invalid minimum byte length for ${name}`) - } - if (typeof byteLengthMax !== 'number') { - throw new TypeError(`Invalid maximum byte length for ${name}`) - } - if (typeof value === 'string') { - if (/[^0-9a-f]/i.test(value)) { - throw new TypeError(`Invalid hexadecimal characters in ${name}`) - } - if (value.length & 1 || value.length < (byteLengthMin << 1) || value.length > (byteLengthMax << 1)) { - throw new TypeError(`Invalid hexadecimal length ${value.length} for ${name}`) - } - value = new Uint8Array(value.match(/[0-9a-f]{2}/gi)?.map(b => parseInt(b, 16)) || []) - } - if (value instanceof ArrayBuffer) { - value = new Uint8Array(value) - } - if (!(value instanceof Uint8Array)) { - throw new TypeError(`${name} must be Uint8Array`) - } - if (!('buffer' in value && value.buffer instanceof ArrayBuffer)) { - throw new TypeError(`${name} must be backed by an ArrayBuffer`) - } - if (value.byteLength < byteLengthMin) { - throw new TypeError(`${name} must be at least ${byteLengthMin} bytes`) - } - if (value.byteLength > byteLengthMax) { - throw new TypeError(`${name} must be no more than ${byteLengthMax} bytes`) - } - return value as Uint8Array -} diff --git a/src/lib/sign.ts b/src/lib/sign.ts new file mode 100644 index 0000000..c3c3361 --- /dev/null +++ b/src/lib/sign.ts @@ -0,0 +1,48 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { clear, exports, isBytes, KEY_LEN, MSG_BUF_LEN, MSG_PTR, normalize, OUT_PTR, PRV_PTR, PUB_PTR, SIG_LEN } from './wasm' + +export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown): string | Uint8Array | void { + if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === 64)) { + throw new TypeError('Sign output buffer must be 64-byte Uint8Array') + } + const message = normalize('message', 0, MSG_BUF_LEN, msg) + const privateKey = normalize('private key', KEY_LEN, KEY_LEN, prv) + const publicKey = normalize('public key', KEY_LEN, KEY_LEN, pub) + const signature = new Uint8Array(SIG_LEN) + let buffer = new Uint8Array(exports.memory.buffer) + try { + for (let i = 0; i < message.byteLength; i++) { + buffer[MSG_PTR + i] = message[i] + } + for (let i = 0; i < KEY_LEN; i++) { + buffer[PRV_PTR + i] = privateKey[i] + } + for (let i = 0; i < KEY_LEN; i++) { + buffer[PUB_PTR + i] = publicKey[i] + } + exports.sign(message.byteLength) + buffer = new Uint8Array(exports.memory.buffer) + for (let i = 0; i < SIG_LEN; i++) { + signature[i] = buffer[OUT_PTR + i] + } + if (typeof prv === 'string') { + let hex = '' + for (const byte of signature) { + hex += byte.toString(16).padStart(2, '0') + } + return hex + } else if (isBytes(prv) && out != null) { + out.set(signature) + signature.fill(0) + return + } else { + return signature + } + } finally { + clear(buffer) + privateKey.fill(0) + publicKey.fill(0) + } +} diff --git a/src/lib/verify.ts b/src/lib/verify.ts new file mode 100644 index 0000000..0643820 --- /dev/null +++ b/src/lib/verify.ts @@ -0,0 +1,48 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { clear, exports, KEY_LEN, MSG_BUF_LEN, MSG_PTR, normalize, PUB_PTR, SIG_LEN, SIG_PTR } from './wasm' + +export function verify (sig: unknown, msg: unknown, pub: unknown): boolean { + let buffer = new Uint8Array(exports.memory.buffer) + try { + const message = normalize('message', 0, MSG_BUF_LEN, msg) + const publicKey = normalize('public key', KEY_LEN, KEY_LEN, pub) + const signature = normalize('signature', SIG_LEN, SIG_LEN, sig) + for (let i = 0; i < message.byteLength; i++) { + buffer[MSG_PTR + i] = message[i] + } + for (let i = 0; i < KEY_LEN; i++) { + buffer[PUB_PTR + i] = publicKey[i] + } + for (let i = 0; i < SIG_LEN; i++) { + buffer[SIG_PTR + i] = signature[i] + } + const v = exports.verify(message.byteLength) + return v === 0 + } finally { + clear(buffer) + } +} + +export function verify_blocks (sig: unknown, msg: unknown, pub: unknown): boolean { + let buffer = new Uint8Array(exports.memory.buffer) + try { + const message = normalize('message', 0, MSG_BUF_LEN, msg) + const publicKey = normalize('public key', KEY_LEN, KEY_LEN, pub) + const signature = normalize('signature', SIG_LEN, SIG_LEN, sig) + for (let i = 0; i < message.byteLength; i++) { + buffer[MSG_PTR + i] = message[i] + } + for (let i = 0; i < KEY_LEN; i++) { + buffer[PUB_PTR + i] = publicKey[i] + } + for (let i = 0; i < SIG_LEN; i++) { + buffer[SIG_PTR + i] = signature[i] + } + const v = exports.verify(message.byteLength) + return v === 0 + } finally { + clear(buffer) + } +} diff --git a/src/lib/wasm.ts b/src/lib/wasm.ts new file mode 100644 index 0000000..56b0ee0 --- /dev/null +++ b/src/lib/wasm.ts @@ -0,0 +1,116 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +//@ts-expect-error +import nano25519_wasm from '../../build/nano25519.wasm' + +type Exports = { + exports: { + derive: () => void + sign: (mlen: number) => void + verify: (mlen: number) => number + getMessagePointer: () => number + getOutputPointer: () => number + getPrivateKeyPointer: () => number + getPublicKeyPointer: () => number + getSignaturePointer: () => number + BLOCKHASH_BYTELENGTH: WebAssembly.Global + KEY_BYTELENGTH: WebAssembly.Global + SIGNATURE_BYTELENGTH: WebAssembly.Global + MESSAGE_BUFFER_BYTELENGTH: WebAssembly.Global + OUTPUT_BUFFER_BYTELENGTH: WebAssembly.Global + PRV_BUFFER_BYTELENGTH: WebAssembly.Global + PUB_BUFFER_BYTELENGTH: WebAssembly.Global + SIGNATURE_BUFFER_BYTELENGTH: WebAssembly.Global + memory: WebAssembly.Memory + } +} + +const wasm: Uint8Array = Uint8Array.from(nano25519_wasm) +const module = new WebAssembly.Module(wasm) + +export const { exports } = new WebAssembly.Instance(module, { + env: { + abort: (msg: any, file: any, row: any, col: any): never => { + const getString = (pointer: number): string | null => { + const end = pointer + new Uint32Array(exports.memory.buffer)[pointer - 4 >>> 2] >>> 1 + const buf = new Uint16Array(exports.memory.buffer) + let start = pointer >>> 1 + let string = '' + while (end - start > 1024) { + string += String.fromCharCode(...buf.subarray(start, start += 1024)) + } + return string + String.fromCharCode(...buf.subarray(start, end)) + } + // ~lib/builtins/abort(~lib/string/String | null?, ~lib/string/String | null?, u32?, u32?) => void + msg >>>= 0 + file >>>= 0 + row >>>= 0 + col >>>= 0 + const message = `Nano25519WasmError: ${getString(msg)}, ${getString(file)}, row ${row}, col ${col}` + throw new Error(message) + } + } +}) as Exports + +export const BLK_LEN = exports.BLOCKHASH_BYTELENGTH.value +export const KEY_LEN = exports.KEY_BYTELENGTH.value +export const SIG_LEN = exports.SIGNATURE_BYTELENGTH.value + +export const MSG_BUF_LEN = exports.MESSAGE_BUFFER_BYTELENGTH.value +export const OUT_BUF_LEN = exports.OUTPUT_BUFFER_BYTELENGTH.value +export const PRV_BUF_LEN = exports.PRV_BUFFER_BYTELENGTH.value +export const PUB_BUF_LEN = exports.PUB_BUFFER_BYTELENGTH.value +export const SIG_BUF_LEN = exports.SIGNATURE_BUFFER_BYTELENGTH.value + +export const MSG_PTR = exports.getMessagePointer() +export const OUT_PTR = exports.getOutputPointer() +export const PRV_PTR = exports.getPrivateKeyPointer() +export const PUB_PTR = exports.getPublicKeyPointer() +export const SIG_PTR = exports.getSignaturePointer() + +export function clear (memory: Uint8Array): void { + memory.fill(0, MSG_PTR, MSG_BUF_LEN) + memory.fill(0, OUT_PTR, OUT_BUF_LEN) + memory.fill(0, PRV_PTR, KEY_LEN) + memory.fill(0, PUB_PTR, KEY_LEN) + memory.fill(0, SIG_PTR, SIG_LEN) +} + +export function isBytes (a: unknown): a is Uint8Array { + return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer +} + +export function normalize (name: string, byteLengthMin: number, byteLengthMax: number, value: unknown): Uint8Array { + if (typeof name !== 'string') { + throw new TypeError(`Invalid name ${name}`) + } + if (typeof byteLengthMin !== 'number') { + throw new TypeError(`Invalid minimum byte length for ${name}`) + } + if (typeof byteLengthMax !== 'number') { + throw new TypeError(`Invalid maximum byte length for ${name}`) + } + + if (typeof value === 'string') { + if (/[^0-9a-f]/i.test(value)) { + throw new TypeError(`Invalid hexadecimal characters in ${name}`) + } + if (value.length & 1 || value.length < (byteLengthMin << 1) || value.length > (byteLengthMax << 1)) { + throw new TypeError(`Invalid hexadecimal length ${value.length} for ${name}`) + } + return new Uint8Array(value.match(/[0-9a-f]{2}/gi)?.map(b => parseInt(b, 16)) || []) + } + + if (!(value instanceof ArrayBuffer || value instanceof Uint8Array)) { + throw new TypeError(`${name} must be Uint8Array or ArrayBuffer`) + } + const bytes = new Uint8Array(value.slice()) + if (bytes.byteLength < byteLengthMin) { + throw new TypeError(`${name} must be at least ${byteLengthMin} bytes`) + } + if (bytes.byteLength > byteLengthMax) { + throw new TypeError(`${name} must be no more than ${byteLengthMax} bytes`) + } + return bytes +} diff --git a/src/lib/worker.ts b/src/lib/worker.ts deleted file mode 100644 index 3f94e36..0000000 --- a/src/lib/worker.ts +++ /dev/null @@ -1,99 +0,0 @@ -//! SPDX-FileCopyrightText: 2026 Chris Duncan -//! SPDX-License-Identifier: GPL-3.0-or-later - -import { UUID } from 'node:crypto' -import { MessagePort as NodeMessagePort } from 'node:worker_threads' -import { derive, sign, verify } from './nano25519' - -type Action = 'derive' | 'sign' | 'start' | 'verify' - -type Data = { - id: UUID - action: string - message?: string | ArrayBuffer - privateKey?: string - publicKey?: string - secretKey?: string - signature?: string -} - -let host: NodeMessagePort | null = null - -/** - * Parses inbound data when nano25519 is started as a Web Worker. Only called - * by functions in `async` module. - * @param {object} message.data - Worker commands and related data - */ -function listener (message: unknown): void { - NODE: if (host == null) return queueMicrotask(() => listener(message)) - if (message == null - || typeof message !== 'object' - || !('data' in message) - || message.data == null - || typeof message.data !== 'object' - || !('id' in message.data) - || typeof message.data.id !== 'string' - || !('action' in message.data) - || typeof message.data.action !== 'string' - ) return - let result: undefined | boolean | string | Uint8Array - let id: undefined | UUID - try { - const data: Data = message.data as object & { id: UUID, action: Action } - id = data.id - - switch (data.action) { - case 'start': { - result = 'listening' - break - } - case 'derive': { - const { privateKey } = data - const publicKey = derive(privateKey) - if (publicKey == null) { - throw new TypeError('Invalid public key from WASM derive()') - } - result = publicKey - break - } - case 'sign': { - const { message, secretKey } = data - const signature = sign(message, secretKey) - if (signature == null) { - throw new TypeError('Invalid signature from WASM sign()') - } - result = signature - break - } - case 'verify': { - const { message, publicKey, signature } = data - const verification = verify(signature, message, publicKey) - if (verification == null) { - throw new TypeError('Invalid verification from WASM verify()') - } - result = verification - break - } - default: { - throw new TypeError(`Invalid action '${data.action}'`) - } - } - } catch (err: any) { - result = JSON.stringify(err?.message ?? err ?? 'unknown error in nano25519 worker listener') - } finally { - const data = { id, result } - BROWSER: postMessage(data) - NODE: host?.postMessage({ data }) - } -} - -BROWSER: addEventListener('message', listener) -NODE: { - if (host == null) { - import('node:worker_threads') - .then(({ parentPort }): void => { - host = parentPort - host?.on('message', listener) - }) - } -} diff --git a/src/sync.ts b/src/sync.ts deleted file mode 100644 index cae33f0..0000000 --- a/src/sync.ts +++ /dev/null @@ -1,80 +0,0 @@ -//! SPDX-FileCopyrightText: 2026 Chris Duncan -//! SPDX-License-Identifier: GPL-3.0-or-later - -import * as nano25519 from './lib/nano25519' - -/** - * Nano public key derivation using WebAssembly. - * @param {string} k - 64-character hexadecimal private key - * @returns 64-character hexadecimal public key - */ -export function derive (k: string): string -/** - * Nano public key derivation using WebAssembly. - * @param {Uint8Array} k - 32-byte private key - * @returns 32-byte public key - */ -export function derive (k: Uint8Array): Uint8Array -/** - * Nano public key derivation using WebAssembly. Instead of allocating an output - * buffer internally for the return value, public key bytes are written to the - * the user-supplied output buffer. - * @param {Uint8Array} prv - 32-byte private key - * @param {Uint8Array} pub - buffer to receive 32-byte public key - */ -export function derive (k: Uint8Array, out: Uint8Array): void -export function derive (k: string | Uint8Array, out?: Uint8Array): string | Uint8Array | void { - return nano25519.derive(k, out) -} - -/** - * Signing using WebAssembly. To sign Nano blocks, the message should be a - * 64-character hexadecimal block hash. - * @param {string} m - Variable-length hexadecimal message up to 32 KiB - * @param {string} k - 128-character hexadecimal secret key (prv + pub) - * @returns 128-character hexadecimal detached signature - */ -export function sign (m: string, k: string): string -/** - * Signing using WebAssembly. To sign Nano blocks, the message should be a - * 32-byte block hash. - * @param {Uint8Array} m - Variable-byte-length message up to 32 KiB - * @param {Uint8Array} k - 64-byte secret key (prv + pub) - * @returns 64-byte detached signature - */ -export function sign (m: Uint8Array, k: Uint8Array): Uint8Array -/** - * Signing using WebAssembly. To sign Nano blocks, the message should be a - * 32-byte block hash. Instead of allocating an output buffer internally for the - * return value, signature bytes are written to the the user-supplied output - * buffer. - * @param {Uint8Array} m - Variable-byte-length message up to 32 KiB - * @param {Uint8Array} k - 64-byte secret key (prv + pub) - * @param {Uint8Array} out - buffer to receive 64-byte detached signature - */ -export function sign (m: Uint8Array, k: Uint8Array, out: Uint8Array): void -export function sign (m: string | Uint8Array, k: string | Uint8Array, out?: Uint8Array): string | Uint8Array | void { - return nano25519.sign(m, k, out) -} - -/** - * Signature verification using WebAssembly. To verify Nano block signatures, - * the message should be a 64-character block hash. - * @param {string} s - 128-character hexadecimal detached signature - * @param {string} m - Variable-length message up to 32 KiB - * @param {string} k - 64-character hexadecimal public key - * @returns true if signature matches block hash and public key, else false - */ -export function verify (s: string, m: string, k: string): boolean -/** - * Signature verification using WebAssembly. To verify Nano block signatures, - * the message should be a 32-byte block hash. - * @param {Uint8Array} s - 64-byte detached signature - * @param {Uint8Array} m - Variable-byte-length message up to 32 KiB - * @param {Uint8Array} k - 32-byte public key - * @returns true if signature matches block hash and public key, else false - */ -export function verify (s: Uint8Array, m: Uint8Array, k: Uint8Array): boolean -export function verify (s: string | Uint8Array, m: string | Uint8Array, k: string | Uint8Array): boolean { - return nano25519.verify(s, m, k) -} diff --git a/test/node.mjs b/test/node.mjs index 66f9bed..7ba55a7 100644 --- a/test/node.mjs +++ b/test/node.mjs @@ -1,9 +1,7 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import * as Nano25519 from 'nano25519' -import { deriveAsync, signAsync, verifyAsync } from 'nano25519/async' -import { derive, sign, verify } from 'nano25519/sync' +import { derive, sign, verify } from 'nano25519' import { NANO_ORG_VECTOR, PROBLEM_VECTOR, PYTHON_ED25519_BLAKE2B_VECTORS } from './vectors.mjs' /** @@ -216,81 +214,6 @@ check(`sign offset outbuf wrong length`, test) passes += +test failures += +!test -// Check combined imports -result = await Nano25519.deriveAsync(NANO_ORG_VECTOR.privateKeyBytes) -test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase() -check(`async derive from ${NANO_ORG_VECTOR.privateKey}`, test) -passes += +test -failures += +!test - -result = await Nano25519.signAsync(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes) -test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase() -check(`async sign message ${NANO_ORG_VECTOR.blockHash}`, test) -passes += +test -failures += +!test - -result = await Nano25519.verifyAsync(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes) -test = result === true -check(`async verify signature ${NANO_ORG_VECTOR.signature}`, test) -passes += +test -failures += +!test - -result = Nano25519.derive(NANO_ORG_VECTOR.privateKeyBytes) -test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase() -check(`derive from ${NANO_ORG_VECTOR.privateKey}`, test) -passes += +test -failures += +!test - -result = Nano25519.sign(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes) -test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase() -check(`sign message ${NANO_ORG_VECTOR.blockHash}`, test) -passes += +test -failures += +!test - -result = Nano25519.verify(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes) -test = result === true -check(`verify signature ${NANO_ORG_VECTOR.signature}`, test) -passes += +test -failures += +!test - -// Check async imports with string inputs -result = await deriveAsync(NANO_ORG_VECTOR.privateKey) -test = result.toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase() -check(`async derive from private key string ${NANO_ORG_VECTOR.privateKey}`, test) -passes += +test -failures += +!test - -result = await signAsync(NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.secretKey) -test = result.toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase() -check(`async sign message string ${NANO_ORG_VECTOR.blockHash}`, test) -passes += +test -failures += +!test - -result = await verifyAsync(NANO_ORG_VECTOR.signature, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey) -test = result === true -check(`async verify signature string ${NANO_ORG_VECTOR.signature}`, test) -passes += +test -failures += +!test - -// Check async imports with byte inputs -result = await deriveAsync(NANO_ORG_VECTOR.privateKeyBytes) -test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase() -check(`async derive from private key bytes ${NANO_ORG_VECTOR.privateKey}`, test) -passes += +test -failures += +!test - -result = await signAsync(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes) -test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase() -check(`async sign message bytes ${NANO_ORG_VECTOR.blockHash}`, test) -passes += +test -failures += +!test - -result = await verifyAsync(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes) -test = result === true -check(`async verify signature bytes ${NANO_ORG_VECTOR.signature}`, test) -passes += +test -failures += +!test - // Check sync imports with string inputs result = derive(NANO_ORG_VECTOR.privateKey) test = result.toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase() diff --git a/tsconfig.json b/tsconfig.json index 96d42b2..d0f1ef7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,9 +22,7 @@ ] }, "include": [ - "src/index.ts", - "src/async.ts", - "src/sync.ts" + "src/index.ts" ], "exclude": [ "assembly", -- 2.52.0