target: 'node22',
entryPoints: [
'src/*',
- 'types/src/*',
],
entryNames: '[name]',
}
"scripts": {
"build": "npm run clean && npm run compile && node ./esbuild/dev.mjs",
"build:prod": "npm run clean && npm run compile && node ./esbuild/prod.mjs",
- "clean": "rm -rf {build,dist,types}",
+ "clean": "rm -rf {build,dist}",
"compile": "asc ./src/assembly/index.ts && tsc",
"prepublishOnly": "npm run test:prod",
"test": "npm run build && node ./test/node.mjs",
import * as nano25519 from './lib'
-export { constants } from './lib'
+type Bytes = nano25519.Bytes
-declare global {
- type Bytes = Uint8Array<ArrayBuffer>
-}
+export const constants = nano25519.constants
/**
* Nano public key derivation using WebAssembly. If byte output is required,
//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { Mutex, Nano25519TypeError, Pointers, clearMemory, constants, exports, isBytes, memory, normalize } from './wasm'
+import { allocate, byteLength, bytesToHex, copy, fill, isBytes, memoryBuffer, normalize, validateMemoryBuffer, view, } from './primordials'
+import { Mutex, Pointers, clearMemory, constants, exports, memory, } from './wasm'
const { KEY_BYTELENGTH, } = constants
export function derive (prv: unknown, out?: unknown): string | void {
- if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === KEY_BYTELENGTH)) {
+ if (typeof out !== 'undefined' && !(isBytes(out) && byteLength(out) === KEY_BYTELENGTH)) {
throw new TypeError('Derive output buffer must be 32-byte Uint8Array<ArrayBuffer>')
}
const privateKey = normalize('private key', KEY_BYTELENGTH, KEY_BYTELENGTH, prv)
- const publicKey = new Uint8Array(KEY_BYTELENGTH)
- let buffer = new Uint8Array(memory.buffer)
+ const publicKey = allocate(KEY_BYTELENGTH)
+ let buffer = view(memoryBuffer(memory))
Mutex.lock()
try {
for (let i = 0; i < KEY_BYTELENGTH; i++) {
buffer[Pointers.INPUT_PRV + i] = privateKey[i]
}
exports.derive()
- if (memory.buffer !== buffer.buffer) {
- throw new Nano25519TypeError('WASM memory buffer detached')
- }
+ validateMemoryBuffer(memory, buffer)
for (let i = 0; i < KEY_BYTELENGTH; i++) {
publicKey[i] = buffer[Pointers.OUTPUT_DERIVE + i]
}
if (out != null) {
- out.set(publicKey)
+ copy(out, publicKey)
return void 0
} else {
- let hex = ''
- for (const byte of publicKey) {
- hex += byte.toString(16).padStart(2, '0')
- }
- return hex
+ return bytesToHex(publicKey)
}
} finally {
- privateKey.fill(0)
- publicKey.fill(0)
+ fill(privateKey, 0)
+ fill(publicKey, 0)
clearMemory()
}
}
//! SPDX-License-Identifier: GPL-3.0-or-later
export { derive } from './derive'
+export { Bytes } from './primordials'
export { sign } from './sign'
export { verify, verify_blocks } from './verify'
export { constants } from './wasm'
-
--- /dev/null
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+ * 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
+ * have replaced in the meantime.
+ *
+ * Two distinct protections are at work here and they need different captures:
+ *
+ * - Binding a constructor (`Bytes`) defends against later reassignment of the
+ * global, because call sites read this binding instead of looking up
+ * `Uint8Array` afresh on every call.
+ * - Binding a method or accessor defends against later patching of the
+ * prototype, because the function reference is frozen rather than resolved
+ * through the object at call time.
+ *
+ * Neither defends against code that runs *before* this module is evaluated.
+ * That is out of scope: a same-realm attacker who loads first can replace the
+ * package's exports outright, and no copy strategy inside the library changes
+ * that. What this does close is the caller-controlled surface — a hostile
+ * `Uint8Array` subclass reaching input validation through `Symbol.species`, an
+ * overridden `length`, or a spoofed `byteLength`.
+ *
+ * Accessors taken from `%TypedArray%.prototype` read internal slots, so a
+ * subclass cannot lie to them. Prefer them to the equivalent property access
+ * everywhere in this package.
+ *
+ * Only capture what is used *after* this module finishes evaluating. Anything
+ * used solely to build the tables below runs while the realm is still as
+ * trustworthy as it will ever be, so binding it first buys nothing — and
+ * pretending otherwise makes this file harder to audit, not safer. That is why
+ * `Object.getOwnPropertyDescriptor`, `Object.getPrototypeOf` and
+ * `Object.create` appear here as plain globals.
+ *
+ * This module must stay free of imports. It is the trust root, so it has to
+ * finish evaluating before anything that uses it: give it a dependency and the
+ * bundler will order that dependency first, and any call it makes into here
+ * during its own evaluation will read captures that are still unassigned.
+ */
+
+export type Bytes = Uint8Array<ArrayBuffer>
+
+/** Declared here to avoid import loops. */
+export class Nano25519TypeError extends TypeError { }
+
+const isArray = Array.isArray
+
+/** `TypedArray` constructors, bound before anything can replace the global. */
+const Bytes = Uint8Array
+const Words = Uint16Array
+const Longs = Uint32Array
+
+const TypedArray = Object.getPrototypeOf(Bytes.prototype)
+
+const bytesSet = Bytes.prototype.set
+const bytesFill = Bytes.prototype.fill
+
+function get<T> (target: object, key: PropertyKey): (this: unknown) => T {
+ const found = Object.getOwnPropertyDescriptor(target, key)
+ if (typeof found?.get !== 'function') {
+ throw new TypeError(`Missing expected accessor while capturing builtins`)
+ }
+ return found.get
+}
+
+/** Reads `[[TypedArrayName]]`; returns `undefined` for anything else. */
+const getTag = get<string>(TypedArray, Symbol.toStringTag)
+/** Reads internal slot `[[ViewedArrayBuffer]]`. */
+const getBuffer = get<ArrayBufferLike>(TypedArray, 'buffer')
+/** Throws if passed `SharedArrayBuffer`, distinguishing it from `ArrayBuffer`. */
+const getBufferByteLength = get<number>(ArrayBuffer.prototype, 'byteLength')
+/** Reads internal slot `[[ByteLength]]`. */
+const getByteLength = get<number>(TypedArray, 'byteLength')
+/** Reads the live backing store of a `WebAssembly.Memory`. */
+const getMemoryBuffer = get<ArrayBuffer>(WebAssembly.Memory.prototype, 'buffer')
+
+/** Reads one unsigned 32-bit word from `buffer` at byte `offset`. */
+function readLong (buffer: ArrayBufferLike, offset: number): number {
+ return new Longs(buffer)[offset >>> 2]
+}
+
+const fromCharCode = String.fromCharCode
+/**
+ * Reads a UTF-16 string of `length` code units from `buffer` at `offset`.
+ * Used only by the WASM abort handler. Builds the string one code unit at a
+ * time rather than spreading a subarray, which would run the iterator and
+ * risk a stack overflow on a long message.
+ */
+function utf16 (buffer: ArrayBufferLike, offset: number, length: number): string {
+ const words = new Words(buffer)
+ let string = ''
+ for (let i = 0; i < length; i++) {
+ string += fromCharCode(words[offset + i])
+ }
+ return string
+}
+
+/** True for a genuine `ArrayBuffer`, false for a `SharedArrayBuffer`. */
+function isArrayBuffer (a: unknown): a is ArrayBuffer {
+ try {
+ getBufferByteLength.call(a)
+ return true
+ } catch { }
+ return false
+}
+
+/**
+ * True only for a genuine `Uint8Array` backed by a non-shared `ArrayBuffer`.
+ * Reads internal slot `[[TypedArrayName]]`, so a subclass overriding
+ * `Symbol.toStringTag` cannot pass, and a look-alike object cannot either.
+ */
+function isBytes (a: unknown): a is Bytes {
+ return getTag.call(a) === 'Uint8Array' && isArrayBuffer(getBuffer.call(a))
+}
+
+/**
+ * The live backing buffer of `memory`. Read through the captured accessor
+ * because every host read and write goes through it: a patched getter would
+ * redirect the whole I/O path to a decoy buffer, and the detachment check
+ * cannot notice because both of its operands come from the same getter.
+ */
+function memoryBuffer (memory: WebAssembly.Memory): ArrayBuffer {
+ return getMemoryBuffer.call(memory)
+}
+
+/** Checks if WASM memory buffer was detached. */
+function validateMemoryBuffer (memory: WebAssembly.Memory, bytes: Uint8Array): void {
+ if (getMemoryBuffer.call(memory) !== getBuffer.call(bytes)) {
+ throw new Nano25519TypeError('WASM memory buffer detached')
+ }
+}
+
+/** Captured version of `new Uint8Array(byteLength)`. */
+function allocate (length: number): Bytes {
+ return new Bytes(length)
+}
+
+/** Captured version of `Uint8Array.prototype.byteLength`. */
+function byteLength (bytes: Uint8Array): number {
+ return getByteLength.call(bytes)
+}
+
+/**
+ * A copy of `src` in a fresh buffer. Unlike `slice()` this never consults
+ * `Symbol.species`, so it cannot hand control to a caller-supplied subclass,
+ * and it sizes the copy from an internal slot rather than a spoofable `length`.
+ */
+function copy (src: Uint8Array): Bytes
+/** `target.set(source, offset)` through the captured method. */
+function copy (dst: Uint8Array, src: Uint8Array, offset?: number): void
+function copy (a: Uint8Array, b?: Uint8Array, o?: number): void | Bytes {
+ if (b) {
+ return bytesSet.call(a, b, o)
+ }
+ const t = new Bytes(getByteLength.call(a))
+ bytesSet.call(t, a)
+ return t
+}
+
+/** Captured version of `target.fill(value, start, end)`. */
+function fill (target: Uint8Array, value: number, start?: number, end?: number): void {
+ bytesFill.call(target, value, start, end)
+}
+
+/** Captured version of `new Uint8Array(buffer)`. */
+function view (buffer: ArrayBuffer): Bytes {
+ return new Bytes(buffer)
+}
+
+const LOWER = '0123456789abcdef'
+const UPPER = '0123456789ABCDEF'
+const NIBBLE: Record<string, number> = Object.create(null)
+for (let i = 0; i < 16; i++) {
+ NIBBLE[LOWER[i]] = i
+ NIBBLE[UPPER[i]] = i
+}
+
+/**
+ * Decodes hex string to Bytes. Validates input directly instead of using a
+ * patchable `RegExp.prototype.test()`.
+ */
+function hexToBytes (hex: string): Bytes {
+ const length = hex.length
+ if (length & 1) {
+ throw new TypeError(`Invalid hexadecimal length ${length}`)
+ }
+ const bytes = new Bytes(length >> 1)
+ for (let i = 0, b = 0; i < length; i += 2, b++) {
+ const hi = hex[i]
+ const lo = hex[i + 1]
+ if (NIBBLE[hi] == null) {
+ throw new TypeError(`Invalid hexadecimal character ${hi}`)
+ }
+ if (NIBBLE[lo] == null) {
+ throw new TypeError(`Invalid hexadecimal character ${lo}`)
+ }
+ bytes[b] = (NIBBLE[hi] << 4) | NIBBLE[lo]
+ }
+ return bytes
+}
+
+/** Lowercase hexadecimal encoding of `bytes`. */
+function bytesToHex (bytes: Uint8Array): string {
+ const byteLength = getByteLength.call(bytes)
+ let hex = ''
+ for (let i = 0; i < byteLength; i++) {
+ const byte = bytes[i]
+ hex += LOWER[byte >> 4] + LOWER[byte & 15]
+ }
+ return hex
+}
+
+/**
+ * Accepts an unknown input; checks that it is a representation of bytes using a
+ * hex string, ArrayBuffer, or Uint8Array; and validates that the byte length of
+ * the value is within a specified range.
+ *
+ * @param {string} name Name of variable to be validated
+ * @param {number} byteLengthMin Throws if byte length is less than this
+ * @param {number} byteLengthMax Throws if byte length is greater than this
+ * @param {unknown} value User input to validate
+ * @returns User input bytes copied to a new Uint8Array for internal use.
+ */
+function normalize (name: string, byteLengthMin: number, byteLengthMax: number, value: unknown): Bytes {
+ if (typeof value === 'string') {
+ if (value.length < (byteLengthMin << 1) || value.length > (byteLengthMax << 1)) {
+ throw new TypeError(`Invalid hexadecimal length ${value.length} for ${name}`)
+ }
+ return hexToBytes(value)
+ }
+
+ if (isArrayBuffer(value)) {
+ value = new Bytes(value)
+ }
+ if (!isBytes(value)) {
+ throw new TypeError(`${name} must be Uint8Array or ArrayBuffer`)
+ }
+ const bytes = copy(value)
+ const byteLength = getByteLength.call(bytes)
+ if (byteLength < byteLengthMin) {
+ throw new TypeError(`${name} must be at least ${byteLengthMin} bytes`)
+ }
+ if (byteLength > byteLengthMax) {
+ throw new TypeError(`${name} must be no more than ${byteLengthMax} bytes`)
+ }
+ return bytes
+}
+
+export { allocate, byteLength, bytesToHex, copy, fill, isArray, isBytes, memoryBuffer, normalize, readLong, utf16, validateMemoryBuffer, view }
//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { Mutex, Nano25519TypeError, Pointers, clearMemory, constants, exports, isBytes, memory, normalize } from './wasm'
+import { allocate, byteLength, bytesToHex, copy, fill, isBytes, memoryBuffer, normalize, validateMemoryBuffer, view, } from './primordials'
+import { Mutex, Pointers, clearMemory, constants, exports, memory, } from './wasm'
const { KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, SIGNATURE_BYTELENGTH, } = constants
export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown): string | void {
- if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === SIGNATURE_BYTELENGTH)) {
+ if (typeof out !== 'undefined' && !(isBytes(out) && byteLength(out) === SIGNATURE_BYTELENGTH)) {
throw new TypeError(`Sign output buffer must be ${SIGNATURE_BYTELENGTH}-byte Uint8Array<ArrayBuffer>`)
}
const message = normalize('message', 0, MAX_MESSAGE_BYTELENGTH, msg)
const privateKey = normalize('private key', KEY_BYTELENGTH, KEY_BYTELENGTH, prv)
const publicKey = normalize('public key', KEY_BYTELENGTH, KEY_BYTELENGTH, pub)
- const signature = new Uint8Array(SIGNATURE_BYTELENGTH)
- let buffer = new Uint8Array(memory.buffer)
+ const signature = allocate(SIGNATURE_BYTELENGTH)
+ let buffer = view(memoryBuffer(memory))
Mutex.lock()
try {
for (let i = 0; i < message.byteLength; i++) {
buffer[Pointers.INPUT_PUB + i] = publicKey[i]
}
exports.sign(message.byteLength)
- if (memory.buffer !== buffer.buffer) {
- throw new Nano25519TypeError('WASM memory buffer detached')
- }
+ validateMemoryBuffer(memory, buffer)
for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) {
signature[i] = buffer[Pointers.OUTPUT_SIGN + i]
}
if (out != null) {
- out.set(signature)
+ copy(out, signature)
return void 0
} else {
- let hex = ''
- for (const byte of signature) {
- hex += byte.toString(16).padStart(2, '0')
- }
- return hex
+ return bytesToHex(signature)
}
} finally {
- privateKey.fill(0)
- publicKey.fill(0)
- signature.fill(0)
+ fill(privateKey, 0)
+ fill(publicKey, 0)
+ fill(signature, 0)
clearMemory()
}
}
//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { Mutex, Nano25519TypeError, Pointers, clearMemory, constants, exports, memory, normalize } from './wasm'
+import { isArray, memoryBuffer, normalize, validateMemoryBuffer, view } from './primordials'
+import { Mutex, Pointers, clearMemory, constants, exports, memory, } from './wasm'
const { BLOCKHASH_BYTELENGTH, KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, MAX_VERIFY_BLOCKS, SIGNATURE_BYTELENGTH, } = constants
export function verify (sig: unknown, msg: unknown, pub: unknown): boolean {
- let buffer = new Uint8Array(memory.buffer)
+ let buffer = view(memoryBuffer(memory))
Mutex.lock()
try {
const message = normalize('message', 0, MAX_MESSAGE_BYTELENGTH, msg)
buffer[Pointers.INPUT_SIG + i] = signature[i]
}
exports.verify(message.byteLength)
- if (memory.buffer !== buffer.buffer) {
- throw new Nano25519TypeError('WASM memory buffer detached')
- }
+ validateMemoryBuffer(memory, buffer)
const verified = buffer[Pointers.OUTPUT_VERIFY]
return verified === 1
} finally {
}
export function verify_blocks (pub: unknown, data: unknown): boolean[] {
- const blocks: unknown[] = Array.isArray(data) ? data : [data]
+ const blocks: unknown[] = isArray(data) ? data : [data]
const count = blocks.length
if (count < 1) {
throw new RangeError('Must pass at least one block', { cause: data })
if (MAX_VERIFY_BLOCKS < count) {
throw new RangeError(`Bulk Nano block verification must be no more than ${MAX_VERIFY_BLOCKS} blocks`, { cause: count })
}
- const verified = new Uint8Array(count)
- let buffer = new Uint8Array(memory.buffer)
+ const verified: boolean[] = []
+ let buffer = view(memoryBuffer(memory))
Mutex.lock()
try {
const publicKey = normalize('public key', KEY_BYTELENGTH, KEY_BYTELENGTH, pub)
p += BLOCKHASH_BYTELENGTH
}
exports.verify_blocks(count)
- if (memory.buffer !== buffer.buffer) {
- throw new Nano25519TypeError('WASM memory buffer detached')
- }
+ validateMemoryBuffer(memory, buffer)
for (let i = 0; i < count; i++) {
- verified[i] = buffer[Pointers.OUTPUT_VERIFY + i]
+ verified[i] = buffer[Pointers.OUTPUT_VERIFY + i] === 1
}
- return [...verified].map(v => v === 1)
+ return verified
} finally {
clearMemory()
}
//@ts-expect-error
import nano25519_wasm from '../../build/nano25519.wasm'
+import { Nano25519TypeError, copy, fill, memoryBuffer, readLong, utf16, view, } from './primordials'
type Exports = {
exports: {
}
}
-export class Nano25519TypeError extends TypeError { }
+export { Nano25519TypeError } from './primordials'
let locked = false
export const Mutex = {
*/
export const memory = new WebAssembly.Memory({ initial: 4 })
-const wasm: Bytes = Uint8Array.from(nano25519_wasm)
+const wasm = copy(nano25519_wasm)
const module = new WebAssembly.Module(wasm)
export const { exports } = new WebAssembly.Instance(module, {
env: {
memory,
abort: (msg: any, file: any, row: any, col: any): never => {
- const getString = (pointer: number): string | null => {
- const end = pointer + new Uint32Array(memory.buffer)[pointer - 4 >>> 2] >>> 1
- const buf = new Uint16Array(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))
+ const str = (pointer: number): string | null => {
+ const end = pointer + readLong(memoryBuffer(memory), pointer - 4) >>> 1
+ const start = pointer >>> 1
+ return utf16(memoryBuffer(memory), start, end - start)
}
// ~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}`
+ const message = `Nano25519WasmError: ${str(msg)}, ${str(file)}, row ${row}, col ${col}`
throw new Error(message)
}
}
}
export function clearMemory (): void {
- const bytes = new Uint8Array(memory.buffer)
- bytes.fill(0, INPUT_MSG, INPUT_MSG + MAX_MESSAGE_BYTELENGTH)
- bytes.fill(0, INPUT_PRV, INPUT_PRV + KEY_BYTELENGTH)
- bytes.fill(0, INPUT_PUB, INPUT_PUB + KEY_BYTELENGTH)
- bytes.fill(0, INPUT_SIG, INPUT_SIG + SIGNATURE_BYTELENGTH)
- bytes.fill(0, OUTPUT_DERIVE, OUTPUT_DERIVE + KEY_BYTELENGTH)
- bytes.fill(0, OUTPUT_SIGN, OUTPUT_SIGN + SIGNATURE_BYTELENGTH)
- bytes.fill(0, OUTPUT_VERIFY, OUTPUT_VERIFY + MAX_VERIFY_BLOCKS)
+ const bytes = view(memoryBuffer(memory))
+ fill(bytes, 0, INPUT_MSG, INPUT_MSG + MAX_MESSAGE_BYTELENGTH)
+ fill(bytes, 0, INPUT_PRV, INPUT_PRV + KEY_BYTELENGTH)
+ fill(bytes, 0, INPUT_PUB, INPUT_PUB + KEY_BYTELENGTH)
+ fill(bytes, 0, INPUT_SIG, INPUT_SIG + SIGNATURE_BYTELENGTH)
+ fill(bytes, 0, OUTPUT_DERIVE, OUTPUT_DERIVE + KEY_BYTELENGTH)
+ fill(bytes, 0, OUTPUT_SIGN, OUTPUT_SIGN + SIGNATURE_BYTELENGTH)
+ fill(bytes, 0, OUTPUT_VERIFY, OUTPUT_VERIFY + MAX_VERIFY_BLOCKS)
locked = false
}
-
-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 = new Uint8Array(value)
- }
- if (!(value instanceof Uint8Array)) {
- throw new TypeError(`${name} must be Uint8Array or ArrayBuffer`)
- }
- const bytes = 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
-}
start = performance.now()
const verified = nano25519.verify_blocks(publicKey, blocks)
end = performance.now()
- if (verified.some(v => v !== true)) {
- throw new Error(`invalid result\nsignature: ${signature}\nblock hash: ${blockHash}\npublic key: ${publicKey}`)
+ for (let i = 0; i < verified.length; i++) {
+ if (verified[i] !== true) {
+ throw new Error(`invalid result ${verified[i]}\nsignature: ${blocks[i].signature}\nblock hash: ${blocks[i].hash}\npublic key: ${publicKey}`)
+ }
}
duration = (end - start) / nano25519.constants.MAX_VERIFY_BLOCKS
deriveTimes[i].push(0)
result = false
} catch (err) {
if (err != null && typeof err === 'object' && 'message' in err) {
- result = err.message === 'Invalid hexadecimal characters in private key'
+ result = err.message === 'Invalid hexadecimal character z'
} else {
result = false
}
result = false
} catch (err) {
if (err != null && typeof err === 'object' && 'message' in err) {
- result = err.message === 'Invalid hexadecimal length 65 for message'
+ result = err.message === 'Invalid hexadecimal length 65'
} else {
result = false
}
result = sign('1'.repeat((constants.MAX_MESSAGE_BYTELENGTH << 1) + 1), NANO_ORG_VECTOR.privateKey, NANO_ORG_VECTOR.publicKey)
result = false
} catch (err) {
- console.log(err)
if (err != null && typeof err === 'object' && 'message' in err) {
result = err.message === `Invalid hexadecimal length ${(constants.MAX_MESSAGE_BYTELENGTH << 1) + 1} for message`
} else {
// Check byte input validation
try {
- result = derive(NANO_ORG_VECTOR.privateKeyBytes.slice(-1))
+ result = derive(NANO_ORG_VECTOR.privateKeyBytes.slice(0, -1))
result = false
} catch (err) {
if (err != null && typeof err === 'object' && 'message' in err) {
result = sign(new Uint8Array(constants.MAX_MESSAGE_BYTELENGTH + 1).fill(1), NANO_ORG_VECTOR.privateKeyBytes, NANO_ORG_VECTOR.publicKeyBytes)
result = false
} catch (err) {
- console.log(err)
if (err != null && typeof err === 'object' && 'message' in err) {
result = err.message === `message must be no more than ${constants.MAX_MESSAGE_BYTELENGTH} bytes`
} else {
passes += +test
failures += +!test
+// Shared memory is rejected on both inputs and output buffers: isBytes()
+// requires a non-shared ArrayBuffer, so a view over a SharedArrayBuffer never
+// gets through. A bare SharedArrayBuffer stays rejected too (covered above).
+try {
+ const shared = new Uint8Array(new SharedArrayBuffer(constants.KEY_BYTELENGTH))
+ shared.set(NANO_ORG_VECTOR.privateKeyBytes)
+ //@ts-expect-error
+ derive(shared)
+ result = false
+} catch (err) {
+ result = true
+}
+test = result === true
+check(`private key bytes backed by SharedArrayBuffer`, test)
+passes += +test
+failures += +!test
+
+// An output buffer that lies about its size must be caught by this library's own
+// validation, not by a RangeError escaping from the copy. isBytes() reads
+// internal slots, but the byteLength compared against it is a plain property
+// read, which a subclass can override.
+try {
+ class SpoofedOutput extends Uint8Array {
+ get byteLength () { return 32 }
+ }
+ result = derive(NANO_ORG_VECTOR.privateKeyBytes, new SpoofedOutput(4))
+ result = false
+} catch (err) {
+ if (err != null && typeof err === 'object' && 'message' in err) {
+ result = err.message === 'Derive output buffer must be 32-byte Uint8Array<ArrayBuffer>'
+ } else {
+ result = false
+ }
+}
+test = result === true
+check(`derive output buffer lying about byteLength`, test)
+passes += +test
+failures += +!test
+
+// Hardening: a hostile Uint8Array subclass must not reach input handling.
+// It lies through Symbol.species, length, byteLength and Symbol.toStringTag;
+// none of those may be consulted, and none of its code may run inside a call.
+let speciesRan = 0
+class HostileBytes extends Uint8Array {
+ static get [Symbol.species] () {
+ speciesRan++
+ //@ts-expect-error
+ return function (...args) { return new Uint8Array(...args) }
+ }
+ get length () { return 4 }
+ get byteLength () { return 4 }
+ //@ts-expect-error
+ get [Symbol.toStringTag] () { return 'Err' }
+}
+speciesRan = 0
+//@ts-expect-error
+result = derive(new HostileBytes(NANO_ORG_VECTOR.privateKeyBytes))
+test = result === NANO_ORG_VECTOR.publicKey.toLowerCase() && speciesRan === 0
+check(`derive ignores a hostile Uint8Array subclass`, test)
+passes += +test
+failures += +!test
+
+// Hardening: builtins patched after the module loaded must not change results.
+const truth = derive(NANO_ORG_VECTOR.privateKeyBytes)
+const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype)
+/** @type {[string, (() => () => void)][]} */
+const patches = [
+ ['Uint8Array.prototype.slice', () => {
+ const real = Uint8Array.prototype.slice
+ Uint8Array.prototype.slice = () => { throw new Error('patched') }
+ return () => { Uint8Array.prototype.slice = real }
+ }],
+ ['Uint8Array.prototype.set', () => {
+ const real = Uint8Array.prototype.set
+ Uint8Array.prototype.set = () => { throw new Error('patched') }
+ return () => { Uint8Array.prototype.set = real }
+ }],
+ ['Uint8Array.prototype.fill', () => {
+ const real = Uint8Array.prototype.fill
+ Uint8Array.prototype.fill = () => { throw new Error('patched') }
+ return () => { Uint8Array.prototype.fill = real }
+ }],
+ ['String.prototype.padStart', () => {
+ const real = String.prototype.padStart
+ String.prototype.padStart = () => 'XX'
+ return () => { String.prototype.padStart = real }
+ }],
+ ['Number.prototype.toString', () => {
+ const real = Number.prototype.toString
+ Number.prototype.toString = () => 'zz'
+ return () => { Number.prototype.toString = real }
+ }],
+ ['RegExp.prototype.test', () => {
+ const real = RegExp.prototype.test
+ RegExp.prototype.test = () => true
+ return () => { RegExp.prototype.test = real }
+ }],
+ ['Array.isArray', () => {
+ const real = Array.isArray
+ //@ts-expect-error
+ Array.isArray = () => false
+ return () => { Array.isArray = real }
+ }],
+ ['globalThis.Uint8Array', () => {
+ const real = globalThis.Uint8Array
+ //@ts-expect-error
+ globalThis.Uint8Array = () => { throw new Error('patched') }
+ return () => { globalThis.Uint8Array = real }
+ }],
+ ['WebAssembly.Memory.prototype.buffer', () => {
+ const real = Object.getOwnPropertyDescriptor(WebAssembly.Memory.prototype, 'buffer')
+ if (!real) throw new TypeError('Failed to backup WebAssembly.Memory.prototype.buffer')
+ const decoy = new ArrayBuffer(262144)
+ Object.defineProperty(WebAssembly.Memory.prototype, 'buffer', { get () { return decoy }, configurable: true })
+ return () => { Object.defineProperty(WebAssembly.Memory.prototype, 'buffer', real) }
+ }],
+ ['%TypedArray%.prototype.length', () => {
+ const real = Object.getOwnPropertyDescriptor(TypedArrayPrototype, 'length')
+ if (!real) throw new TypeError('Failed to backup TypedArrayPrototype.length')
+ Object.defineProperty(TypedArrayPrototype, 'length', { get () { return 4 }, configurable: true })
+ return () => { Object.defineProperty(TypedArrayPrototype, 'length', real) }
+ }],
+ ['%TypedArray%.prototype.byteLength', () => {
+ const real = Object.getOwnPropertyDescriptor(TypedArrayPrototype, 'byteLength')
+ if (!real) throw new TypeError('Failed to backup TypedArrayPrototype.byteLength')
+ Object.defineProperty(TypedArrayPrototype, 'byteLength', { get () { return 4 }, configurable: true })
+ return () => { Object.defineProperty(TypedArrayPrototype, 'byteLength', real) }
+ }]
+]
+for (const [name, apply] of patches) {
+ const undo = apply()
+ try {
+ result = derive(NANO_ORG_VECTOR.privateKeyBytes) === truth
+ } catch (err) {
+ result = false
+ } finally {
+ undo()
+ }
+ test = result === true
+ check(`derive unaffected by patched ${name}`, test)
+ passes += +test
+ failures += +!test
+}
+
console.log(`${passes} \x1b[32mPASSED\x1b[0m`)
console.log(`${failures} \x1b[31mFAILED\x1b[0m`)
"strict": true,
"declaration": true,
"emitDeclarationOnly": true,
- "rootDir": ".",
- "outDir": "types",
- "declarationMap": true,
+ "rootDir": "src",
+ "outDir": "dist",
+ "declarationMap": false,
"lib": [
"DOM",
"ESNext"