const OUTPUT_SIGN = new StaticArray<u8>(SIGNATURE_BYTELENGTH)
const OUTPUT_VERIFY = new StaticArray<u8>(MAX_VERIFY_BLOCKS)
+/**
+ * Sets 32 bytes at a time in a buffer memory address specified by `p`.
+ *
+ * A host-to-module crossing costs roughly the same regardless of payload width,
+ * so batching 32 bytes is much more efficient than writing individual bytes.
+ */
+function set32Bytes (p: usize, v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
+ store<u32>(p, v0)
+ store<u32>(p + 4, v1)
+ store<u32>(p + 8, v2)
+ store<u32>(p + 12, v3)
+ store<u32>(p + 16, v4)
+ store<u32>(p + 20, v5)
+ store<u32>(p + 24, v6)
+ store<u32>(p + 28, v7)
+}
+
/** Clears all input and output buffers. */
export function clearMemory (): void {
INPUT_MSG.fill(0)
OUTPUT_VERIFY.fill(0)
}
-/** Sets a byte in the static message input buffer. */
-export function setInputMsgByte (index: i32, v0: i32): void {
- if (index < 0 || MAX_MESSAGE_BYTELENGTH <= index) {
+/**
+ * Sets 32 bytes in the static message input buffer. Index range check accounts
+ * for a full batch width. The host code must pack the input bytes in
+ * little-endian order since WASM is little-endian by definition.
+ */
+export function setInputMsg (index: i32, v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
+ if (index < 0 || MAX_MESSAGE_BYTELENGTH - 32 < index) {
raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_MSG, index)
}
- if (v0 < 0 || 0xff < v0) {
- raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_MSG, v0)
- }
- INPUT_MSG[index] = u8(v0)
+ const p: usize = changetype<usize>(INPUT_MSG) + index
+ set32Bytes(p, v0, v1, v2, v3, v4, v5, v6, v7)
}
-/** Sets 4 bytes in the static message input buffer. */
-export function setInputMsg32 (index: i32, v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
- if (index < 0 || MAX_MESSAGE_BYTELENGTH <= index) {
- raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_MSG, index)
- }
- const v = [v0, v1, v2, v3, v4, v5, v6, v7]
- for (let i = 0; i < 8; i++) {
- if (v[i] < 0 || 0xffffffff < v[i]) {
- raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_MSG, v[i])
- }
- INPUT_MSG[index + (i << 2)] = u8((v[i] >>> 24) & 255)
- INPUT_MSG[index + (i << 2) + 1] = u8((v[i] >>> 16) & 255)
- INPUT_MSG[index + (i << 2) + 2] = u8((v[i] >>> 8) & 255)
- INPUT_MSG[index + (i << 2) + 3] = u8(v[i] & 255)
- }
+/** Sets all 32 bytes in the static private key input buffer. */
+export function setInputPrv (v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
+ const p: usize = changetype<usize>(INPUT_PRV)
+ set32Bytes(p, v0, v1, v2, v3, v4, v5, v6, v7)
}
-/** Sets a byte in the static private key input buffer (32 bytes). */
-export function setInputPrvByte (index: i32, value: i32): void {
- if (index < 0 || KEY_BYTELENGTH <= index) {
- raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_PRV, index)
- }
- if (value < 0 || 0xff < value) {
- raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_PRV, value)
- }
- INPUT_PRV[index] = u8(value)
+/** Sets all 32 bytes of the static public key input buffer. */
+export function setInputPub (v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
+ const p: usize = changetype<usize>(INPUT_PUB)
+ set32Bytes(p, v0, v1, v2, v3, v4, v5, v6, v7)
}
-/** Sets a byte in the static public key input buffer (32 bytes). */
-export function setInputPubByte (index: i32, value: i32): void {
- if (index < 0 || KEY_BYTELENGTH <= index) {
- raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_PUB, index)
- }
- if (value < 0 || 0xff < value) {
- raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_PUB, value)
- }
- INPUT_PUB[index] = u8(value)
-}
-
-/** Sets a byte in the static signature input buffer (64 bytes). */
-export function setInputSigByte (index: i32, value: i32): void {
- if (index < 0 || SIGNATURE_BYTELENGTH <= index) {
- raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_SIG, index)
- }
- if (value < 0 || 0xff < value) {
- raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_SIG, value)
- }
- INPUT_SIG[index] = u8(value)
+/** Sets all 64 bytes of the static signature input buffer. */
+export function setInputSig (v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32, v8: u32, v9: u32, v10: u32, v11: u32, v12: u32, v13: u32, v14: u32, v15: u32): void {
+ const p: usize = changetype<usize>(INPUT_SIG)
+ set32Bytes(p, v0, v1, v2, v3, v4, v5, v6, v7)
+ set32Bytes(p + 32, v8, v9, v10, v11, v12, v13, v14, v15)
}
/** Gets a byte from the derive() static output buffer (32 bytes). */
//! SPDX-License-Identifier: GPL-3.0-or-later
import { allocate, byteLength, bytesToHex, copy, fill, isBytes, normalize } from './primordials'
-import { MemoryBuffers, Mutex, clearMemory, constants, derive as wasm_derive, } from './wasm'
+import { clearMemory, constants, getOutput, Mutex, setInput, derive as wasm_derive, } from './wasm'
const { KEY_BYTELENGTH, } = constants
const publicKey = allocate(KEY_BYTELENGTH)
Mutex.lock()
try {
- for (let i = 0; i < KEY_BYTELENGTH; i++) {
- MemoryBuffers.INPUT_PRV(i, privateKey[i])
- }
+ setInput('prv', privateKey)
wasm_derive()
for (let i = 0; i < KEY_BYTELENGTH; i++) {
- publicKey[i] = MemoryBuffers.OUTPUT_DERIVE(i)
+ publicKey[i] = getOutput('derive', i)
}
if (out != null) {
copy(out, publicKey)
//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
+export class Nano25519MutexError extends RangeError { }
export class Nano25519TypeError extends TypeError { }
export class Nano25519WasmError extends Error { }
//! SPDX-License-Identifier: GPL-3.0-or-later
import { allocate, byteLength, bytesToHex, copy, fill, isBytes, normalize } from './primordials'
-import { MemoryBuffers, Mutex, clearMemory, constants, sign as wasm_sign } from './wasm'
+import { Mutex, clearMemory, constants, getOutput, setInput, sign as wasm_sign } from './wasm'
const { KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, SIGNATURE_BYTELENGTH, } = constants
const signature = allocate(SIGNATURE_BYTELENGTH)
Mutex.lock()
try {
- for (let i = 0; i < message.byteLength; i++) {
- MemoryBuffers.INPUT_MSG(i, message[i])
- }
- for (let i = 0; i < KEY_BYTELENGTH; i++) {
- MemoryBuffers.INPUT_PRV(i, privateKey[i])
- }
- for (let i = 0; i < KEY_BYTELENGTH; i++) {
- MemoryBuffers.INPUT_PUB(i, publicKey[i])
- }
+ setInput('msg', message)
+ setInput('prv', privateKey)
+ setInput('pub', publicKey)
wasm_sign(message.byteLength)
for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) {
- signature[i] = MemoryBuffers.OUTPUT_SIGN(i)
+ signature[i] = getOutput('sign', i)
}
if (out != null) {
copy(out, signature)
//! SPDX-License-Identifier: GPL-3.0-or-later
import { isArray, normalize } from './primordials'
-import { MemoryBuffers, Mutex, clearMemory, constants, verify as wasm_verify, verify_blocks as wasm_verify_blocks, } from './wasm'
+import { Mutex, clearMemory, constants, getOutput, setInput, verify as wasm_verify, verify_blocks as wasm_verify_blocks, } from './wasm'
const { BLOCKHASH_BYTELENGTH, KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, MAX_VERIFY_BLOCKS, SIGNATURE_BYTELENGTH, } = constants
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 += 32) {
- const u32: number[] = []
- for (let j = 0; j < 32; j += 4) {
- u32[j >> 2] = ((message[i + j] ?? 0) << 24) | ((message[i + j + 1] ?? 0) << 16) | ((message[i + j + 2] ?? 0) << 8) | (message[i + j + 3] ?? 0)
- }
- MemoryBuffers.INPUT_MSG_32(i, u32[0], u32[1], u32[2], u32[3], u32[4], u32[5], u32[6], u32[7])
- }
- for (let i = 0; i < KEY_BYTELENGTH; i++) {
- MemoryBuffers.INPUT_PUB(i, publicKey[i])
- }
- for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) {
- MemoryBuffers.INPUT_SIG(i, signature[i])
- }
+ setInput('msg', message)
+ setInput('pub', publicKey)
+ setInput('sig', signature)
wasm_verify(message.byteLength)
- const verified = MemoryBuffers.OUTPUT_VERIFY(0)
+ const verified = getOutput('verify', 0)
return verified === 1
} finally {
clearMemory()
Mutex.lock()
try {
const publicKey = normalize('public key', KEY_BYTELENGTH, KEY_BYTELENGTH, pub)
- for (let i = 0; i < KEY_BYTELENGTH; i++) {
- MemoryBuffers.INPUT_PUB(i, publicKey[i])
- }
- for (let b = 0, p = 0; b < count; b++) {
+ setInput('pub', publicKey)
+ const message = new Uint8Array(count * (SIGNATURE_BYTELENGTH + BLOCKHASH_BYTELENGTH))
+ for (let b = 0; b < count; b++) {
const block = blocks[b]
if (block == null || typeof block !== 'object') {
throw new TypeError('Invalid block', { cause: block })
}
const signature = normalize('signature', SIGNATURE_BYTELENGTH, SIGNATURE_BYTELENGTH, block.signature)
const hash = normalize('hash', BLOCKHASH_BYTELENGTH, BLOCKHASH_BYTELENGTH, block.hash)
- for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) {
- MemoryBuffers.INPUT_MSG(p + i, signature[i])
- }
- p += SIGNATURE_BYTELENGTH
- for (let i = 0; i < BLOCKHASH_BYTELENGTH; i++) {
- MemoryBuffers.INPUT_MSG(p + i, hash[i])
- }
- p += BLOCKHASH_BYTELENGTH
+ const offset = b * 96
+ message.set(signature, offset)
+ message.set(hash, offset + SIGNATURE_BYTELENGTH)
}
+ setInput('msg', message)
wasm_verify_blocks(count)
for (let i = 0; i < count; i++) {
- verified[i] = MemoryBuffers.OUTPUT_VERIFY(i) === 1
+ verified[i] = getOutput('verify', i) === 1
}
return verified
} finally {
//@ts-expect-error
import nano25519_wasm from '../../build/nano25519.wasm'
-import { Nano25519TypeError, abort, errorCodes } from './errors'
-import { copy } from './primordials'
+import { Nano25519MutexError, Nano25519TypeError, abort, errorCodes, } from './errors'
+import { copy, } from './primordials'
type Exports = WebAssembly.Instance['exports'] & {
clearMemory: () => void
sign: (mlen: number) => void
verify: (mlen: number) => void
verify_blocks: (count: number) => void
- setInputMsgByte: (index: number, value: number) => void
- setInputMsg32: (index: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) => void
- setInputPrvByte: (index: number, value: number) => void
- setInputPubByte: (index: number, value: number) => void
- setInputSigByte: (index: number, value: number) => void
+ setInputMsg: (index: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) => void
+ setInputPrv: (v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) => void
+ setInputPub: (v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) => void
+ setInputSig: (v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number, v11: number, v12: number, v13: number, v14: number, v15: number) => void
getOutputDeriveByte: (index: number) => number
getOutputSignByte: (index: number) => number
getOutputVerifyByte: (index: number) => number
const MAX_VERIFY_BLOCKS_BYTELENGTH = exports.MAX_VERIFY_BLOCKS_BYTELENGTH.value
const MAX_MESSAGE_BYTELENGTH = exports.MAX_MESSAGE_BYTELENGTH.value
+/**
+ * Copies the bytes of a message for signing, or signature verification, into
+ * the static message input buffer of the WASM module. Bytes are packed in
+ * little-endian order to align with the WASM spec. The entire input is written
+ * in one call rather than looped over an intermediate array because allocating
+ * costs more than the copy itself. Each byte must be protected with a null
+ * coalescing operator in order to fall back on a zero value since the message
+ * length can vary and not be a multiple of 32 bytes.
+ */
+function setInputMsg (message: Uint8Array): void {
+ const mlen = message.byteLength
+ for (let i = 0; i < mlen; i += 32) {
+ exports.setInputMsg(i,
+ (message[i] ?? 0) | ((message[i + 1] ?? 0) << 8) | ((message[i + 2] ?? 0) << 16) | ((message[i + 3] ?? 0) << 24),
+ (message[i + 4] ?? 0) | ((message[i + 5] ?? 0) << 8) | ((message[i + 6] ?? 0) << 16) | ((message[i + 7] ?? 0) << 24),
+ (message[i + 8] ?? 0) | ((message[i + 9] ?? 0) << 8) | ((message[i + 10] ?? 0) << 16) | ((message[i + 11] ?? 0) << 24),
+ (message[i + 12] ?? 0) | ((message[i + 13] ?? 0) << 8) | ((message[i + 14] ?? 0) << 16) | ((message[i + 15] ?? 0) << 24),
+ (message[i + 16] ?? 0) | ((message[i + 17] ?? 0) << 8) | ((message[i + 18] ?? 0) << 16) | ((message[i + 19] ?? 0) << 24),
+ (message[i + 20] ?? 0) | ((message[i + 21] ?? 0) << 8) | ((message[i + 22] ?? 0) << 16) | ((message[i + 23] ?? 0) << 24),
+ (message[i + 24] ?? 0) | ((message[i + 25] ?? 0) << 8) | ((message[i + 26] ?? 0) << 16) | ((message[i + 27] ?? 0) << 24),
+ (message[i + 28] ?? 0) | ((message[i + 29] ?? 0) << 8) | ((message[i + 30] ?? 0) << 16) | ((message[i + 31] ?? 0) << 24)
+ )
+ }
+}
+
+/**
+ * Copies the bytes of a private key into the static private key input buffer of
+ * the WASM module. Bytes are packed in little-endian order to align with the
+ * WASM spec. The entire input is written in one call rather than looped over an
+ * intermediate array because allocating costs more than the copy itself.
+ */
+function setInputPrv (prv: Uint8Array): void {
+ exports.setInputPrv(
+ prv[0] | (prv[1] << 8) | (prv[2] << 16) | (prv[3] << 24),
+ prv[4] | (prv[5] << 8) | (prv[6] << 16) | (prv[7] << 24),
+ prv[8] | (prv[9] << 8) | (prv[10] << 16) | (prv[11] << 24),
+ prv[12] | (prv[13] << 8) | (prv[14] << 16) | (prv[15] << 24),
+ prv[16] | (prv[17] << 8) | (prv[18] << 16) | (prv[19] << 24),
+ prv[20] | (prv[21] << 8) | (prv[22] << 16) | (prv[23] << 24),
+ prv[24] | (prv[25] << 8) | (prv[26] << 16) | (prv[27] << 24),
+ prv[28] | (prv[29] << 8) | (prv[30] << 16) | (prv[31] << 24)
+ )
+}
+
+/**
+ * Copies the bytes of a public key into the static public key input buffer of
+ * the WASM module. Bytes are packed in little-endian order to align with the
+ * WASM spec. The entire input is written in one call rather than looped over an
+ * intermediate array because allocating costs more than the copy itself.
+ */
+function setInputPub (pub: Uint8Array): void {
+ exports.setInputPub(
+ pub[0] | (pub[1] << 8) | (pub[2] << 16) | (pub[3] << 24),
+ pub[4] | (pub[5] << 8) | (pub[6] << 16) | (pub[7] << 24),
+ pub[8] | (pub[9] << 8) | (pub[10] << 16) | (pub[11] << 24),
+ pub[12] | (pub[13] << 8) | (pub[14] << 16) | (pub[15] << 24),
+ pub[16] | (pub[17] << 8) | (pub[18] << 16) | (pub[19] << 24),
+ pub[20] | (pub[21] << 8) | (pub[22] << 16) | (pub[23] << 24),
+ pub[24] | (pub[25] << 8) | (pub[26] << 16) | (pub[27] << 24),
+ pub[28] | (pub[29] << 8) | (pub[30] << 16) | (pub[31] << 24)
+ )
+}
+
+/**
+ * Copies the bytes of a detached signature into the static signature input
+ * buffer of the WASM module. Bytes are packed in little-endian order to align
+ * with the WASM spec. The entire input is written in one call rather than looped over an
+ * intermediate array because allocating costs more than the copy itself.
+ */
+function setInputSig (sig: Uint8Array): void {
+ exports.setInputSig(
+ sig[0] | (sig[1] << 8) | (sig[2] << 16) | (sig[3] << 24),
+ sig[4] | (sig[5] << 8) | (sig[6] << 16) | (sig[7] << 24),
+ sig[8] | (sig[9] << 8) | (sig[10] << 16) | (sig[11] << 24),
+ sig[12] | (sig[13] << 8) | (sig[14] << 16) | (sig[15] << 24),
+ sig[16] | (sig[17] << 8) | (sig[18] << 16) | (sig[19] << 24),
+ sig[20] | (sig[21] << 8) | (sig[22] << 16) | (sig[23] << 24),
+ sig[24] | (sig[25] << 8) | (sig[26] << 16) | (sig[27] << 24),
+ sig[28] | (sig[29] << 8) | (sig[30] << 16) | (sig[31] << 24),
+ sig[32] | (sig[33] << 8) | (sig[34] << 16) | (sig[35] << 24),
+ sig[36] | (sig[37] << 8) | (sig[38] << 16) | (sig[39] << 24),
+ sig[40] | (sig[41] << 8) | (sig[42] << 16) | (sig[43] << 24),
+ sig[44] | (sig[45] << 8) | (sig[46] << 16) | (sig[47] << 24),
+ sig[48] | (sig[49] << 8) | (sig[50] << 16) | (sig[51] << 24),
+ sig[52] | (sig[53] << 8) | (sig[54] << 16) | (sig[55] << 24),
+ sig[56] | (sig[57] << 8) | (sig[58] << 16) | (sig[59] << 24),
+ sig[60] | (sig[61] << 8) | (sig[62] << 16) | (sig[63] << 24)
+ )
+}
+
+/**
+ * Tells the WASM module to clear its own I/O buffers internally, and then
+ * releases the mutex lock.
+ */
+export function clearMemory (): void {
+ exports.clearMemory()
+ locked = false
+}
+
export const constants = {
BLOCKHASH_BYTELENGTH,
KEY_BYTELENGTH,
MAX_MESSAGE_BYTELENGTH
}
-export class MemoryBuffers {
- static #check (): void {
- if (!locked) throw new Nano25519TypeError('Mutex not acquired')
- }
- static get INPUT_MSG () {
- this.#check()
- return exports.setInputMsgByte
- }
- static get INPUT_MSG_32 () {
- this.#check()
- return exports.setInputMsg32
- }
- static get INPUT_PRV () {
- this.#check()
- return exports.setInputPrvByte
- }
- static get INPUT_PUB () {
- this.#check()
- return exports.setInputPubByte
- }
- static get INPUT_SIG () {
- this.#check()
- return exports.setInputSigByte
- }
- static get OUTPUT_DERIVE () {
- this.#check()
- return exports.getOutputDeriveByte
- }
- static get OUTPUT_SIGN () {
- this.#check()
- return exports.getOutputSignByte
- }
- static get OUTPUT_VERIFY () {
- this.#check()
- return exports.getOutputVerifyByte
+/**
+ * Copies the bytes of a specified input parameter into the associated static
+ * input buffer of the WASM module. Called internal functions must pack input
+ * bytes in little-endian order to align with the WASM spec.
+ *
+ * A mutex lock must first be acquired by called `Mutex.lock()`. The lock is
+ * only released when `clearMemory()` is called in order to prevent re-entry
+ * while WASM memory holds sensitive data.
+ */
+export function setInput (name: 'msg' | 'prv' | 'pub' | 'sig', input: Uint8Array): void {
+ if (!locked) throw new Nano25519MutexError('Mutex not acquired')
+ switch (name) {
+ case 'msg': return setInputMsg(input)
+ case 'prv': return setInputPrv(input)
+ case 'pub': return setInputPub(input)
+ case 'sig': return setInputSig(input)
+ default: throw new Nano25519TypeError('Invalid input name', { cause: name })
}
}
-export function clearMemory (): void {
- exports.clearMemory()
- locked = false
+/**
+ * Copies the bytes of a specified function result from the associated static
+ * output buffer of the WASM module.
+ *
+ * A mutex lock must first be acquired by called `Mutex.lock()`. The lock is
+ * only released when `clearMemory()` is called in order to prevent re-entry
+ * while WASM memory holds sensitive data.
+ */
+export function getOutput (name: 'derive' | 'sign' | 'verify', index: number): number {
+ if (!locked) throw new Nano25519MutexError('Mutex not acquired')
+ switch (name) {
+ case 'derive': return exports.getOutputDeriveByte(index)
+ case 'sign': return exports.getOutputSignByte(index)
+ case 'verify': return exports.getOutputVerifyByte(index)
+ default: throw new Nano25519TypeError('Invalid output name', { cause: name })
+ }
}
export const { derive, sign, verify, verify_blocks } = exports