//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { Nano25519RangeError, Nano25519TypeError } from "./errors"
-
/**
* Builtins captured once, when this module is evaluated, so that nothing on a
* call path resolves a global or a prototype method that hostile code could
/** Type alias for legibility. */
export type Bytes = Uint8Array<ArrayBuffer>
-export type Words = Uint32Array<ArrayBuffer>
export type View = DataView<ArrayBuffer>
export const isArray = Array.isArray
-export const isLE = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1
/** `TypedArray` constructors, bound before anything can replace the globals. */
const Bytes = Uint8Array
-const Words = Uint32Array
const TypedArray = Object.getPrototypeOf(Bytes.prototype)
const View = DataView
return getTag.call(a) === 'Uint8Array' && isArrayBuffer(getBuffer.call(a))
}
-/** Captured version of `DataView` reusing input offset and length. */
-export function view (bytes: Bytes): View {
- return new View(getBuffer.call(bytes) as ArrayBuffer, getByteOffset.call(bytes), getByteLength.call(bytes))
-}
-
/**
- * Captured version of `Uint32Array`. Enables reading multiple bytes in a single
+ * Captured version of `DataView` reusing input offset and length.
+ * Enables reading multiple bytes in a single
* load which is more efficient than reading and combining individual bytes
* with bitshifts. The caller is responsible for managing endianness.
*
* Every boundary-crossing buffer comes from here or `normalize()`, so the
* offset is always 0 in practice; the offset check is a guard against mishap.
*/
-export function words (bytes: Bytes): Words {
+export function view (bytes: Bytes): View {
if (!isBytes(bytes)) {
- throw new Nano25519TypeError('Error creating Uint32Array', { cause: bytes })
+ throw new TypeError('Error creating DataView', { cause: bytes })
}
const offset = getByteOffset.call(bytes)
if (offset !== 0) {
- throw new Nano25519RangeError('Unexpected byte offset', { cause: offset })
+ throw new RangeError('Unexpected byte offset', { cause: offset })
}
- return new Words(getBuffer.call(bytes) as ArrayBuffer, 0, getByteLength.call(bytes) >> 2)
+ return new View(getBuffer.call(bytes) as ArrayBuffer, offset, getByteLength.call(bytes))
}
const LOWER = '0123456789abcdef'
* remaining bytes, fewer than 32, go through `setInputMsgBytes`, which handles
* the zero padding.
*/
-function setInputMsgBigEndian (m: View, i: number, p: number): void {
- p <<= 2
- exports.setInputMsg(i,
- m.getUint32(p, true),
- m.getUint32(p + 1, true),
- m.getUint32(p + 2, true),
- m.getUint32(p + 3, true),
- m.getUint32(p + 4, true),
- m.getUint32(p + 5, true),
- m.getUint32(p + 6, true),
- m.getUint32(p + 7, true)
- )
-}
-
-/**
- * Copies the bytes of a message for signing, or signature verification, into
- * the static message input buffer of the WASM module. The entire input is
- * written in one pass rather than looped over an intermediate array because
- * allocating costs more than the copy itself.
- *
- * Whole 32-byte chunks are read a word at a time through a `Uint32Array` view.
- * A `Uint32Array` is laid out little-endian on every platform that can run
- * WASM, so a word read is already in the order the module's `store<u32>` wants
- * and needs no shifting — measured over 64 KiB, that takes the loop from ~45 µs
- * to ~13 µs, which is the cost of the 2048 boundary crossings alone. The
- * remaining bytes, fewer than 32, go through `setInputMsgBytes`, which handles
- * the zero padding.
- *
- * Widening the crossing to 64 bytes per call was measured at ~12 µs, under the
- * ~2% noise floor for a whole `sign` or `verify`, so the ABI stays at 32.
- */
-function setInputMsgLittleEndian (m: Words, i: number, p: number): void {
- exports.setInputMsg(i,
- m[p],
- m[p + 1],
- m[p + 2],
- m[p + 3],
- m[p + 4],
- m[p + 5],
- m[p + 6],
- m[p + 7]
- )
-}
-
function setInputMsg (message: Bytes): void {
const mlen = byteLength(message)
const mlentrunc = mlen & ~31
- const m = setInputMsgView(message)
- for (let i = 0, p = 0; i < mlentrunc; i += 32, p += 8) {
- setInputMsgEndian(m as (Words & View), i, p)
+ const m = view(message)
+ for (let i = 0; i < mlentrunc; i += 32) {
+ exports.setInputMsg(i,
+ m.getUint32(i, true),
+ m.getUint32(i + 4, true),
+ m.getUint32(i + 8, true),
+ m.getUint32(i + 12, true),
+ m.getUint32(i + 16, true),
+ m.getUint32(i + 20, true),
+ m.getUint32(i + 24, true),
+ m.getUint32(i + 28, true)
+ )
}
if (mlentrunc < mlen) {
setInputMsgEnd(message, mlentrunc)
}
}
-/**
- * Selected once, at load, so the hot loop carries no per-chunk branch. The
- * word path is wrong on a big-endian platform and there is no such WASM host,
- * but the check costs one probe at load and removes the silent-wrong-answer
- * failure mode entirely.
- */
-const setInputMsgEndian = isLE ? setInputMsgLittleEndian : setInputMsgBigEndian
-const setInputMsgView = isLE ? words : view
-
/**
* 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