]> git.codecow.com Git - nano25519.git/commitdiff
Deprecate async module and focus on organizing buffers for batch prep.
authorChris Duncan <chris@codecow.com>
Tue, 25 Aug 2026 00:38:02 +0000 (17:38 -0700)
committerChris Duncan <chris@codecow.com>
Tue, 25 Aug 2026 00:38:02 +0000 (17:38 -0700)
17 files changed:
esbuild/config.mjs
esbuild/inject/worker.mjs [deleted file]
src/assembly/crypto_verify.ts
src/assembly/index.ts
src/async.ts [deleted file]
src/index.ts
src/lib/derive.ts [new file with mode: 0644]
src/lib/host.ts [deleted file]
src/lib/index.ts [new file with mode: 0644]
src/lib/nano25519.ts [deleted file]
src/lib/sign.ts [new file with mode: 0644]
src/lib/verify.ts [new file with mode: 0644]
src/lib/wasm.ts [new file with mode: 0644]
src/lib/worker.ts [deleted file]
src/sync.ts [deleted file]
test/node.mjs
tsconfig.json

index a0b77d76dfcb89cb90622dd684e28199917f8690..d3806b0af50ed05452d37b1b496fa3725292381b 100644 (file)
@@ -1,7 +1,6 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! 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 (file)
index 5d5602a..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
-//! 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)
index e29ed40cc5ffa64798486a715cfa72ac779c7aba..bab9018fadf9a6ccc415b8a24f60b4762f998a61 100644 (file)
@@ -21,7 +21,39 @@ const S = new StaticArray<u8>(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<u8>, M: StaticArray<u8>, mlen: i32, pub: StaticArray<u8>): i32 {
+export function crypto_verify_strict (s: StaticArray<u8>, M: StaticArray<u8>, mlen: i32, pub: StaticArray<u8>): 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<usize>(S), changetype<usize>(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<u8>, M: StaticArray<u8>, mlen: i32, pub: StaticArray<u8>): i32 {
 
        // fail if public key `k` is non-canonical (`p = 2²⁵⁵-19 ≤ k`)
        if (!ge_is_canonical(pub)) return -1
index 343ad1722e727666488592a3b3e9bb7e0c7c2f9b..4e4e2c55f71cabeac1bea4d6a66a5824c0ec971c 100644 (file)
@@ -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<u8>(INPUT_BUFFER_BYTES)
-const MESSAGE_BUFFER = new StaticArray<u8>(MESSAGE_BUFFER_BYTES)
-const OUTPUT_BUFFER = new StaticArray<u8>(OUTPUT_BUFFER_BYTES)
-
-/** Returns the pointer to the static input buffer (96 bytes). */
-export function getInputPointer (): usize {
-       return changetype<usize>(INPUT_BUFFER)
-}
+const MESSAGE_BUFFER = new StaticArray<u8>(MESSAGE_BUFFER_BYTELENGTH)
+const OUTPUT_BUFFER = new StaticArray<u8>(OUTPUT_BUFFER_BYTELENGTH)
+const PRV_BUFFER = new StaticArray<u8>(PRV_BUFFER_BYTELENGTH)
+const PUB_BUFFER = new StaticArray<u8>(PUB_BUFFER_BYTELENGTH)
+const SIGNATURE_BUFFER = new StaticArray<u8>(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<usize>(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<usize>(OUTPUT_BUFFER)
 }
 
-const derive_prv = new StaticArray<u8>(PRIVATEKEY_BYTES)
-const derive_pub = new StaticArray<u8>(PUBLICKEY_BYTES)
+/** Returns the pointer to the static private key input buffer (32 bytes). */
+export function getPrivateKeyPointer (): usize {
+       return changetype<usize>(PRV_BUFFER)
+}
+
+/** Returns the pointer to the static public key input buffer (32 bytes). */
+export function getPublicKeyPointer (): usize {
+       return changetype<usize>(PUB_BUFFER)
+}
+
+/** Returns the pointer to the static signature input buffer (64 bytes). */
+export function getSignaturePointer (): usize {
+       return changetype<usize>(SIGNATURE_BUFFER)
+}
+
+const derive_prv = new StaticArray<u8>(KEY_BYTELENGTH)
+const derive_pub = new StaticArray<u8>(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<usize>(derive_prv), changetype<usize>(INPUT_BUFFER), PRIVATEKEY_BYTES)
-       INPUT_BUFFER.fill(0)
+       memory.copy(changetype<usize>(derive_prv), changetype<usize>(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<usize>(OUTPUT_BUFFER), changetype<usize>(derive_pub), PUBLICKEY_BYTES)
+       memory.copy(changetype<usize>(OUTPUT_BUFFER), changetype<usize>(derive_pub), KEY_BYTELENGTH)
 
        // Clear local result
        derive_pub.fill(0)
 }
 
-const sign_prv = new StaticArray<u8>(PRIVATEKEY_BYTES)
-const sign_pub = new StaticArray<u8>(PUBLICKEY_BYTES)
-const sign_sig = new StaticArray<u8>(SIGNATURE_BYTES)
+const sign_prv = new StaticArray<u8>(KEY_BYTELENGTH)
+const sign_pub = new StaticArray<u8>(KEY_BYTELENGTH)
+const sign_sig = new StaticArray<u8>(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<u8>(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<usize>(sign_prv), changetype<usize>(INPUT_BUFFER), PRIVATEKEY_BYTES)
-       memory.copy(changetype<usize>(sign_pub), changetype<usize>(INPUT_BUFFER) + PRIVATEKEY_BYTES, PUBLICKEY_BYTES)
-       INPUT_BUFFER.fill(0)
+       // Copy input buffers to local parameters, then clear input buffer
+       memory.copy(changetype<usize>(sign_prv), changetype<usize>(PRV_BUFFER), KEY_BYTELENGTH)
+       PRV_BUFFER.fill(0)
+       memory.copy(changetype<usize>(sign_pub), changetype<usize>(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<usize>(OUTPUT_BUFFER), changetype<usize>(sign_sig), SIGNATURE_BYTES)
+       memory.copy(changetype<usize>(OUTPUT_BUFFER), changetype<usize>(sign_sig), SIGNATURE_BYTELENGTH)
 
        // Clear local result
        sign_sig.fill(0)
 }
 
-const verify_pub = new StaticArray<u8>(PUBLICKEY_BYTES)
-const verify_sig = new StaticArray<u8>(SIGNATURE_BYTES)
+const verify_pub = new StaticArray<u8>(KEY_BYTELENGTH)
+const verify_sig = new StaticArray<u8>(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<u8>(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<usize>(verify_pub), changetype<usize>(PUB_BUFFER), KEY_BYTELENGTH)
+       PUB_BUFFER.fill(0)
+       memory.copy(changetype<usize>(verify_sig), changetype<usize>(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<u8>(KEY_BYTELENGTH)
+const verify_blocks_sig = new StaticArray<u8>(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<usize>(verify_sig), changetype<usize>(INPUT_BUFFER), SIGNATURE_BYTES)
-       memory.copy(changetype<usize>(verify_pub), changetype<usize>(INPUT_BUFFER) + SIGNATURE_BYTES, PUBLICKEY_BYTES)
-       INPUT_BUFFER.fill(0)
+       memory.copy(changetype<usize>(verify_sig), changetype<usize>(PUB_BUFFER), SIGNATURE_BYTELENGTH)
+       PUB_BUFFER.fill(0)
+       memory.copy(changetype<usize>(verify_pub), changetype<usize>(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 (file)
index fe8bdac..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
-//! SPDX-License-Identifier: GPL-3.0-or-later
-
-import { run } from './lib/host'
-
-/**
- * Asynchronous Nano public key derivation using WebAssembly.
- * @param {Uint8Array<ArrayBuffer>} k - 64-character private key hex string
- * @returns Promise for 64-character public key hex string
- */
-export async function deriveAsync (privateKey: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>
-/**
- * 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<string>
-export async function deriveAsync (privateKey: string | Uint8Array<ArrayBuffer>): Promise<string | Uint8Array<ArrayBuffer>> {
-       return run({ action: 'derive', privateKey })
-}
-
-/**
- * Asynchronous signing using WebAssembly. To sign Nano blocks, the message
- * should be a 64-character block hash.
- * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string up to 65536 characters
- * @param {Uint8Array<ArrayBuffer>} k - 128-character secret key (prv + pub) hex string
- * @returns Promise for 128-character detached signature hex string
- */
-export async function signAsync (message: Uint8Array<ArrayBuffer>, secretKey: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>
-/**
- * Asynchronous signing using WebAssembly. To sign Nano blocks, the message
- * should be a 64-character block hash.
- * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string up to 65536 characters
- * @param {Uint8Array<ArrayBuffer>} 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<string>
-export async function signAsync (message: string | Uint8Array<ArrayBuffer>, secretKey: string | Uint8Array<ArrayBuffer>): Promise<string | Uint8Array<ArrayBuffer>> {
-       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<ArrayBuffer>} s - 128-character detached signature hex string
- * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string to up 65536 characters
- * @param {Uint8Array<ArrayBuffer>} 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<ArrayBuffer>, message: Uint8Array<ArrayBuffer>, publicKey: Uint8Array<ArrayBuffer>): Promise<boolean>
-/**
- * Asynchronous signature verification using WebAssembly. To verify Nano block
- * signatures, the message should be a 64-character block hash.
- * @param {Uint8Array<ArrayBuffer>} s - 128-character detached signature hex string
- * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string to up 65536 characters
- * @param {Uint8Array<ArrayBuffer>} 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<boolean>
-export async function verifyAsync (signature: string | Uint8Array<ArrayBuffer>, message: string | Uint8Array<ArrayBuffer>, publicKey: string | Uint8Array<ArrayBuffer>): Promise<boolean> {
-       return run({ action: 'verify', signature, message, publicKey })
-}
index 4cc42f047efd848883fe8164a7de0e72205aa6e3..aa6039d828678f1e45ae22bb8658d21f7b508bb9 100644 (file)
@@ -1,5 +1,105 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! 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<ArrayBuffer>} prv - 32-byte private key
+ * @returns 32-byte public key
+ */
+export function derive (prv: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>
+/**
+ * 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<ArrayBuffer>} prv - 32-byte private key
+ * @param {Uint8Array<ArrayBuffer>} out - buffer to receive 32-byte public key
+ */
+export function derive (prv: Uint8Array<ArrayBuffer>, out: Uint8Array<ArrayBuffer>): void
+export function derive (prv: string | Uint8Array<ArrayBuffer>, out?: Uint8Array<ArrayBuffer>): string | Uint8Array<ArrayBuffer> | void {
+       return nano25519.derive(prv, out)
+}
+
+/**
+ * Signing using WebAssembly. To sign Nano blocks, the message should be a
+ * 32-byte block hash.
+ * @param {Uint8Array<ArrayBuffer>} msg - Variable-byte-length message up to 32 KiB
+ * @param {Uint8Array<ArrayBuffer>} prv - 32-byte private key
+ * @param {Uint8Array<ArrayBuffer>} pub - 32-byte public key
+ * @returns 64-byte detached signature
+ */
+export function sign (msg: Uint8Array<ArrayBuffer>, prv: Uint8Array<ArrayBuffer>, pub: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>
+/**
+ * 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<ArrayBuffer>} msg - Variable-byte-length message up to 32 KiB
+ * @param {Uint8Array<ArrayBuffer>} prv - 32-byte private key
+ * @param {Uint8Array<ArrayBuffer>} pub - 32-byte public key
+ * @param {Uint8Array<ArrayBuffer>} out - buffer to receive 64-byte detached signature
+ */
+export function sign (msg: Uint8Array<ArrayBuffer>, prv: Uint8Array<ArrayBuffer>, pub: Uint8Array<ArrayBuffer>, out: Uint8Array<ArrayBuffer>): void
+export function sign (msg: string | Uint8Array<ArrayBuffer>, prv: string | Uint8Array<ArrayBuffer>, pub: string | Uint8Array<ArrayBuffer>, out?: Uint8Array<ArrayBuffer>): string | Uint8Array<ArrayBuffer> | void {
+       return nano25519.sign(msg, prv, pub, out)
+}
+
+/**
+ * Signature verification using WebAssembly. To verify Nano block signatures,
+ * use `verify_blocks()` instead.
+ * @param {Uint8Array<ArrayBuffer>} sig - 64-byte detached signature
+ * @param {Uint8Array<ArrayBuffer>} msg - Variable-byte-length message up to 32 KiB
+ * @param {Uint8Array<ArrayBuffer>} pub - 32-byte public key
+ * @returns true if signature matches block hash and public key, else false
+ */
+export function verify (sign: Uint8Array<ArrayBuffer>, msg: Uint8Array<ArrayBuffer>, pub: Uint8Array<ArrayBuffer>): 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<ArrayBuffer>, msg: string | Uint8Array<ArrayBuffer>, pub: string | Uint8Array<ArrayBuffer>): boolean {
+       return nano25519.verify(sig, msg, pub)
+}
+
+/**
+ * Nano block signature verification using WebAssembly.
+ * @param {Uint8Array<ArrayBuffer>} sig - Buffer of up to 1024 64-byte detached signature (64 KiB)
+ * @param {Uint8Array<ArrayBuffer>} msg - Buffer of up to 1024 32-byte block hashes (32 KiB)
+ * @param {Uint8Array<ArrayBuffer>} pub - 32-byte public key
+ * @returns true if signature matches block hash and public key, else false
+ */
+export function verify_blocks (sign: Uint8Array<ArrayBuffer>, msg: Uint8Array<ArrayBuffer>, pub: Uint8Array<ArrayBuffer>): 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<ArrayBuffer>, msg: string | Uint8Array<ArrayBuffer>, pub: string | Uint8Array<ArrayBuffer>): boolean {
+       return nano25519.verify_blocks(sig, msg, pub)
+}
+
diff --git a/src/lib/derive.ts b/src/lib/derive.ts
new file mode 100644 (file)
index 0000000..28e28f6
--- /dev/null
@@ -0,0 +1,39 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! 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<ArrayBuffer> | void {
+       if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === KEY_LEN)) {
+               throw new TypeError('Derive output buffer must be 32-byte Uint8Array<ArrayBuffer>')
+       }
+       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 (file)
index 3548262..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
-//! 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<unknown> | undefined
-let tasks: Map<string, Parameters<ConstructorParameters<PromiseConstructor>[0]>> = new Map()
-let worker: Worker | NodeWorker
-let url: string
-
-function isBytes (a: unknown): a is Uint8Array<ArrayBuffer> {
-       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<string, any> & 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<string, unknown> }): 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<unknown> {
-       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<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
-       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<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
-export async function run (data: Record<'action', 'sign'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
-export async function run (data: Record<'action', 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean>
-export async function run (data: Record<'action', 'derive' | 'sign' | 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean | string | Uint8Array<ArrayBuffer>> {
-       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 (file)
index 0000000..7b00dca
--- /dev/null
@@ -0,0 +1,7 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! 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 (file)
index a5e1b21..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
-//! 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<ArrayBuffer> = 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<ArrayBuffer> | void {
-       if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === 32)) {
-               throw new TypeError('Derive output buffer must be 32-byte Uint8Array<ArrayBuffer>')
-       }
-       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<ArrayBuffer> | void {
-       if (typeof s !== 'undefined' && !(isBytes(s) && s.byteLength === 64)) {
-               throw new TypeError('Sign output buffer must be 64-byte Uint8Array<ArrayBuffer>')
-       }
-       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<ArrayBuffer> {
-       return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer
-}
-
-function normalize (name: string, byteLengthMin: number, byteLengthMax: number, value: unknown): Uint8Array<ArrayBuffer> {
-       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<ArrayBuffer>
-}
diff --git a/src/lib/sign.ts b/src/lib/sign.ts
new file mode 100644 (file)
index 0000000..c3c3361
--- /dev/null
@@ -0,0 +1,48 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! 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<ArrayBuffer> | void {
+       if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === 64)) {
+               throw new TypeError('Sign output buffer must be 64-byte Uint8Array<ArrayBuffer>')
+       }
+       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 (file)
index 0000000..0643820
--- /dev/null
@@ -0,0 +1,48 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! 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 (file)
index 0000000..56b0ee0
--- /dev/null
@@ -0,0 +1,116 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! 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<ArrayBuffer> = 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<ArrayBuffer> {
+       return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer
+}
+
+export function normalize (name: string, byteLengthMin: number, byteLengthMax: number, value: unknown): Uint8Array<ArrayBuffer> {
+       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 (file)
index 3f94e36..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
-//! 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<ArrayBuffer>
-       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 (file)
index cae33f0..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
-//! 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<ArrayBuffer>} k - 32-byte private key
- * @returns 32-byte public key
- */
-export function derive (k: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>
-/**
- * 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<ArrayBuffer>} prv - 32-byte private key
- * @param {Uint8Array<ArrayBuffer>} pub - buffer to receive 32-byte public key
- */
-export function derive (k: Uint8Array<ArrayBuffer>, out: Uint8Array<ArrayBuffer>): void
-export function derive (k: string | Uint8Array<ArrayBuffer>, out?: Uint8Array<ArrayBuffer>): string | Uint8Array<ArrayBuffer> | 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<ArrayBuffer>} m - Variable-byte-length message up to 32 KiB
- * @param {Uint8Array<ArrayBuffer>} k - 64-byte secret key (prv + pub)
- * @returns 64-byte detached signature
- */
-export function sign (m: Uint8Array<ArrayBuffer>, k: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>
-/**
- * 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<ArrayBuffer>} m - Variable-byte-length message up to 32 KiB
- * @param {Uint8Array<ArrayBuffer>} k - 64-byte secret key (prv + pub)
- * @param {Uint8Array<ArrayBuffer>} out - buffer to receive 64-byte detached signature
- */
-export function sign (m: Uint8Array<ArrayBuffer>, k: Uint8Array<ArrayBuffer>, out: Uint8Array<ArrayBuffer>): void
-export function sign (m: string | Uint8Array<ArrayBuffer>, k: string | Uint8Array<ArrayBuffer>, out?: Uint8Array<ArrayBuffer>): string | Uint8Array<ArrayBuffer> | 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<ArrayBuffer>} s - 64-byte detached signature
- * @param {Uint8Array<ArrayBuffer>} m - Variable-byte-length message up to 32 KiB
- * @param {Uint8Array<ArrayBuffer>} k - 32-byte public key
- * @returns true if signature matches block hash and public key, else false
- */
-export function verify (s: Uint8Array<ArrayBuffer>, m: Uint8Array<ArrayBuffer>, k: Uint8Array<ArrayBuffer>): boolean
-export function verify (s: string | Uint8Array<ArrayBuffer>, m: string | Uint8Array<ArrayBuffer>, k: string | Uint8Array<ArrayBuffer>): boolean {
-       return nano25519.verify(s, m, k)
-}
index 66f9bed39e751afca10758300e402e17f275a2f2..7ba55a7d7859f74949967ac40a6dccad3d889db7 100644 (file)
@@ -1,9 +1,7 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! 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()
index 96d42b2b7b49656246cb8add33a23d66d1af75aa..d0f1ef7f93436c7d2cb4c77788c5a5c1b8d021f4 100644 (file)
@@ -22,9 +22,7 @@
                ]
        },
        "include": [
-               "src/index.ts",
-               "src/async.ts",
-               "src/sync.ts"
+               "src/index.ts"
        ],
        "exclude": [
                "assembly",