From 5082870cd4b6c9618d8b8fa08399da4c57f53756 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sat, 29 Aug 2026 01:46:09 -0700 Subject: [PATCH] Implement mutex pattern to prevent async wrappers or similar from breaking shared buffer layout. --- src/lib/derive.ts | 10 +++++----- src/lib/sign.ts | 14 +++++++------- src/lib/verify.ts | 19 ++++++++++--------- src/lib/wasm.ts | 31 +++++++++++++++++++++++-------- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/lib/derive.ts b/src/lib/derive.ts index 5a831f8..30f01ad 100644 --- a/src/lib/derive.ts +++ b/src/lib/derive.ts @@ -1,10 +1,9 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { clear, constants, exports, isBytes, normalize, pointers, } from './wasm' +import { Mutex, Pointers, clear, constants, exports, isBytes, normalize } from './wasm' const { KEY_BYTELENGTH, } = constants -const { INPUT_PRV, OUTPUT_DERIVE, } = pointers export function derive (prv: unknown, out?: unknown): string | Uint8Array | void { if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === KEY_BYTELENGTH)) { @@ -14,13 +13,14 @@ export function derive (prv: unknown, out?: unknown): string | Uint8Array //! SPDX-License-Identifier: GPL-3.0-or-later -import { clear, constants, exports, isBytes, normalize, pointers, } from './wasm' +import { Mutex, Pointers, clear, constants, exports, isBytes, normalize } from './wasm' const { KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, SIGNATURE_BYTELENGTH, } = constants -const { INPUT_MSG, INPUT_PRV, INPUT_PUB, OUTPUT_SIGN, } = pointers export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown): string | Uint8Array | void { if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === SIGNATURE_BYTELENGTH)) { @@ -16,19 +15,20 @@ export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown): const signature = new Uint8Array(SIGNATURE_BYTELENGTH) let buffer = new Uint8Array(exports.memory.buffer) try { + Mutex.lock() for (let i = 0; i < message.byteLength; i++) { - buffer[INPUT_MSG + i] = message[i] + buffer[Pointers.INPUT_MSG + i] = message[i] } for (let i = 0; i < KEY_BYTELENGTH; i++) { - buffer[INPUT_PRV + i] = privateKey[i] + buffer[Pointers.INPUT_PRV + i] = privateKey[i] } for (let i = 0; i < KEY_BYTELENGTH; i++) { - buffer[INPUT_PUB + i] = publicKey[i] + buffer[Pointers.INPUT_PUB + i] = publicKey[i] } exports.sign(message.byteLength) buffer = new Uint8Array(exports.memory.buffer) for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) { - signature[i] = buffer[OUTPUT_SIGN + i] + signature[i] = buffer[Pointers.OUTPUT_SIGN + i] } if (typeof prv === 'string') { let hex = '' @@ -44,8 +44,8 @@ export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown): return signature } } finally { - clear(buffer) privateKey.fill(0) publicKey.fill(0) + clear(buffer) } } diff --git a/src/lib/verify.ts b/src/lib/verify.ts index b622c5f..2b2ae1e 100644 --- a/src/lib/verify.ts +++ b/src/lib/verify.ts @@ -1,29 +1,29 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { clear, constants, exports, normalize, pointers, } from './wasm' +import { Mutex, Pointers, clear, constants, exports, normalize, } from './wasm' const { BLOCKHASH_BYTELENGTH, KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, MAX_VERIFY_BLOCKS, SIGNATURE_BYTELENGTH, } = constants -const { INPUT_MSG, INPUT_PUB, INPUT_SIG, OUTPUT_VERIFY, } = pointers export function verify (sig: unknown, msg: unknown, pub: unknown): boolean { let buffer = new Uint8Array(exports.memory.buffer) try { + Mutex.lock() const message = normalize('message', 0, MAX_MESSAGE_BYTELENGTH, msg) const publicKey = normalize('public key', KEY_BYTELENGTH, KEY_BYTELENGTH, pub) const signature = normalize('signature', SIGNATURE_BYTELENGTH, SIGNATURE_BYTELENGTH, sig) for (let i = 0; i < message.byteLength; i++) { - buffer[INPUT_MSG + i] = message[i] + buffer[Pointers.INPUT_MSG + i] = message[i] } for (let i = 0; i < KEY_BYTELENGTH; i++) { - buffer[INPUT_PUB + i] = publicKey[i] + buffer[Pointers.INPUT_PUB + i] = publicKey[i] } for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) { - buffer[INPUT_SIG + i] = signature[i] + buffer[Pointers.INPUT_SIG + i] = signature[i] } exports.verify(message.byteLength) buffer = new Uint8Array(exports.memory.buffer) - const verified = buffer[OUTPUT_VERIFY] + const verified = buffer[Pointers.OUTPUT_VERIFY] return verified === 0 } finally { clear(buffer) @@ -42,11 +42,12 @@ export function verify_blocks (pub: unknown, data: unknown): boolean[] { const verified = new Uint8Array(count) let buffer = new Uint8Array(exports.memory.buffer) try { + Mutex.lock() const publicKey = normalize('public key', KEY_BYTELENGTH, KEY_BYTELENGTH, pub) for (let i = 0; i < KEY_BYTELENGTH; i++) { - buffer[INPUT_PUB + i] = publicKey[i] + buffer[Pointers.INPUT_PUB + i] = publicKey[i] } - for (let b = 0, p = INPUT_MSG; b < count; b++) { + for (let b = 0, p = Pointers.INPUT_MSG; b < count; b++) { const block = blocks[b] if (block == null || typeof block !== 'object') { throw new TypeError('Invalid block', { cause: block }) @@ -71,7 +72,7 @@ export function verify_blocks (pub: unknown, data: unknown): boolean[] { exports.verify_blocks(count) buffer = new Uint8Array(exports.memory.buffer) for (let i = 0; i < count; i++) { - verified[i] = buffer[OUTPUT_VERIFY + i] + verified[i] = buffer[Pointers.OUTPUT_VERIFY + i] } return [...verified].map(v => v === 0) } finally { diff --git a/src/lib/wasm.ts b/src/lib/wasm.ts index 37bdbb4..6640700 100644 --- a/src/lib/wasm.ts +++ b/src/lib/wasm.ts @@ -27,6 +27,16 @@ type Exports = { } } +export class Nano25519TypeError extends TypeError { } + +let locked = false +export const Mutex = { + lock (): void { + if (locked) throw new Nano25519TypeError('Failed to acquire mutex') + locked = true + } +} + const wasm: Uint8Array = Uint8Array.from(nano25519_wasm) const module = new WebAssembly.Module(wasm) @@ -79,14 +89,18 @@ const OUTPUT_DERIVE = exports.ptrOutputDerive() const OUTPUT_SIGN = exports.ptrOutputSign() const OUTPUT_VERIFY = exports.ptrOutputVerify() -export const pointers = { - INPUT_MSG, - INPUT_PRV, - INPUT_PUB, - INPUT_SIG, - OUTPUT_DERIVE, - OUTPUT_SIGN, - OUTPUT_VERIFY, +export class Pointers { + static #get (ptr: number): number { + if (locked) return ptr + throw new Nano25519TypeError('Mutex not acquired') + } + static get INPUT_MSG (): number { return this.#get(INPUT_MSG) } + static get INPUT_PRV (): number { return this.#get(INPUT_PRV) } + static get INPUT_PUB (): number { return this.#get(INPUT_PUB) } + static get INPUT_SIG (): number { return this.#get(INPUT_SIG) } + static get OUTPUT_DERIVE (): number { return this.#get(OUTPUT_DERIVE) } + static get OUTPUT_SIGN (): number { return this.#get(OUTPUT_SIGN) } + static get OUTPUT_VERIFY (): number { return this.#get(OUTPUT_VERIFY) } } export function clear (memory: Uint8Array): void { @@ -97,6 +111,7 @@ export function clear (memory: Uint8Array): void { memory.fill(0, OUTPUT_DERIVE, OUTPUT_DERIVE + KEY_BYTELENGTH) memory.fill(0, OUTPUT_SIGN, OUTPUT_SIGN + SIGNATURE_BYTELENGTH) memory.fill(255, OUTPUT_VERIFY, OUTPUT_VERIFY + MAX_VERIFY_BLOCKS) + locked = false } export function isBytes (a: unknown): a is Uint8Array { -- 2.52.0