391 lines
15 KiB
JavaScript
391 lines
15 KiB
JavaScript
/**
|
|
* Utilities for hex, bytearray and number handling.
|
|
* @module
|
|
*/
|
|
/*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
|
|
import { abytes, abytes as abytes_, concatBytes, isLE, randomBytes as randb, } from '@noble/hashes/utils.js';
|
|
/**
|
|
* Asserts that a value is a byte array and optionally checks its length.
|
|
* Returns the original reference unchanged on success, and currently also accepts Node `Buffer`
|
|
* values through the upstream validator.
|
|
* This helper throws on malformed input, so APIs that must return `false` need to guard lengths
|
|
* before decoding or before calling it.
|
|
* @example
|
|
* Validate that a value is a byte array with the expected length.
|
|
* ```ts
|
|
* abytes(new Uint8Array([1]), 1);
|
|
* ```
|
|
*/
|
|
const abytesDoc = abytes;
|
|
export { abytesDoc as abytes };
|
|
/**
|
|
* Concatenates byte arrays into a new `Uint8Array`.
|
|
* Zero arguments return an empty `Uint8Array`.
|
|
* Invalid segments throw before allocation because each argument is validated first.
|
|
* @example
|
|
* Concatenate two byte arrays into one result.
|
|
* ```ts
|
|
* concatBytes(new Uint8Array([1]), new Uint8Array([2]));
|
|
* ```
|
|
*/
|
|
const concatBytesDoc = concatBytes;
|
|
export { concatBytesDoc as concatBytes };
|
|
/**
|
|
* Returns cryptographically secure random bytes.
|
|
* Requires `globalThis.crypto.getRandomValues` and throws if that API is unavailable.
|
|
* `bytesLength` is validated by the upstream helper as a non-negative integer before allocation,
|
|
* so negative and fractional values both throw instead of truncating through JS `ToIndex`.
|
|
* @param bytesLength - Number of random bytes to generate.
|
|
* @returns Fresh random bytes.
|
|
* @example
|
|
* Generate a fresh random seed.
|
|
* ```ts
|
|
* const seed = randomBytes(4);
|
|
* ```
|
|
*/
|
|
export const randomBytes = randb;
|
|
/**
|
|
* Compares two byte arrays in a length-constant way for equal lengths.
|
|
* Unequal lengths return `false` immediately, and there is no runtime type validation.
|
|
* @param a - First byte array.
|
|
* @param b - Second byte array.
|
|
* @returns Whether both arrays contain the same bytes.
|
|
* @example
|
|
* Compare two byte arrays for equality.
|
|
* ```ts
|
|
* equalBytes(new Uint8Array([1]), new Uint8Array([1]));
|
|
* ```
|
|
*/
|
|
export function equalBytes(a, b) {
|
|
if (a.length !== b.length)
|
|
return false;
|
|
let diff = 0;
|
|
for (let i = 0; i < a.length; i++)
|
|
diff |= a[i] ^ b[i];
|
|
return diff === 0;
|
|
}
|
|
/**
|
|
* Copies bytes into a fresh `Uint8Array`.
|
|
* Returns a detached plain `Uint8Array` after validating that the input is real bytes.
|
|
* @param bytes - Source bytes.
|
|
* @returns Copy of the input bytes.
|
|
* @example
|
|
* Copy bytes into a fresh array.
|
|
* ```ts
|
|
* copyBytes(new Uint8Array([1, 2]));
|
|
* ```
|
|
*/
|
|
export function copyBytes(bytes) {
|
|
// `Uint8Array.from(...)` would also accept arrays / other typed arrays. Keep this helper strict
|
|
// because callers use it at byte-validation boundaries before mutating the detached copy.
|
|
return Uint8Array.from(abytes(bytes));
|
|
}
|
|
/**
|
|
* Byte-swaps each 64-bit lane in place.
|
|
* Falcon's exact binary64 tables are stored as little-endian byte payloads, so BE runtimes need
|
|
* this boundary helper before aliasing them as host `Float64Array` lanes.
|
|
* @param arr - Byte buffer whose length is a multiple of 8.
|
|
* @returns The same buffer after in-place 64-bit lane byte swaps.
|
|
* @example
|
|
* Byte-swap one 64-bit lane in place.
|
|
* ```ts
|
|
* byteSwap64(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]));
|
|
* ```
|
|
*/
|
|
export function byteSwap64(arr) {
|
|
const bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
for (let i = 0; i < bytes.length; i += 8) {
|
|
const a0 = bytes[i + 0];
|
|
const a1 = bytes[i + 1];
|
|
const a2 = bytes[i + 2];
|
|
const a3 = bytes[i + 3];
|
|
bytes[i + 0] = bytes[i + 7];
|
|
bytes[i + 1] = bytes[i + 6];
|
|
bytes[i + 2] = bytes[i + 5];
|
|
bytes[i + 3] = bytes[i + 4];
|
|
bytes[i + 4] = a3;
|
|
bytes[i + 5] = a2;
|
|
bytes[i + 6] = a1;
|
|
bytes[i + 7] = a0;
|
|
}
|
|
return arr;
|
|
}
|
|
/**
|
|
* Byte-swaps 64-bit lanes on big-endian runtimes and returns the input unchanged on little-endian.
|
|
* This keeps Falcon's binary64 tables in canonical little-endian order before aliasing them as
|
|
* `Float64Array` lanes on the current host.
|
|
* @param arr - Buffer to pass through or swap in place.
|
|
* @returns The same buffer, normalized for Falcon's little-endian table layout.
|
|
* @example
|
|
* Normalize one host-endian buffer for Falcon's float tables.
|
|
* ```ts
|
|
* baswap64If(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]));
|
|
* ```
|
|
*/
|
|
export const baswap64If = isLE
|
|
? (arr) => arr
|
|
: byteSwap64;
|
|
/**
|
|
* Validates that an options bag is a plain object.
|
|
* @param opts - Options object to validate.
|
|
* @throws On wrong argument types. {@link TypeError}
|
|
* @example
|
|
* Validate that an options bag is a plain object.
|
|
* ```ts
|
|
* validateOpts({});
|
|
* ```
|
|
*/
|
|
export function validateOpts(opts) {
|
|
// Arrays silently passed here before, but these call sites expect named option-bag fields.
|
|
if (Object.prototype.toString.call(opts) !== '[object Object]')
|
|
throw new TypeError('expected valid options object');
|
|
}
|
|
/**
|
|
* Validates common verification options.
|
|
* `context` itself is validated with `abytes(...)`, and individual algorithms may narrow support
|
|
* further after this shared plain-object gate.
|
|
* @param opts - Verification options. See {@link VerOpts}.
|
|
* @throws On wrong argument types. {@link TypeError}
|
|
* @example
|
|
* Validate common verification options.
|
|
* ```ts
|
|
* validateVerOpts({ context: new Uint8Array([1]) });
|
|
* ```
|
|
*/
|
|
export function validateVerOpts(opts) {
|
|
validateOpts(opts);
|
|
if (opts.context !== undefined)
|
|
abytes(opts.context, undefined, 'opts.context');
|
|
}
|
|
/**
|
|
* Validates common signing options.
|
|
* `extraEntropy` is validated with `abytes(...)`; exact lengths and extra algorithm-specific
|
|
* restrictions are enforced later by callers.
|
|
* @param opts - Signing options. See {@link SigOpts}.
|
|
* @throws On wrong argument types. {@link TypeError}
|
|
* @example
|
|
* Validate common signing options.
|
|
* ```ts
|
|
* validateSigOpts({ extraEntropy: new Uint8Array([1]) });
|
|
* ```
|
|
*/
|
|
export function validateSigOpts(opts) {
|
|
validateVerOpts(opts);
|
|
if (opts.extraEntropy !== false && opts.extraEntropy !== undefined)
|
|
abytes(opts.extraEntropy, undefined, 'opts.extraEntropy');
|
|
}
|
|
/**
|
|
* Builds a fixed-layout coder from byte lengths and nested coders.
|
|
* Raw-length fields decode as zero-copy `subarray(...)` views, and nested coders may preserve that
|
|
* aliasing too. Nested coder `encode(...)` results are treated as owned scratch: `splitCoder`
|
|
* copies them into the output and then zeroizes them with `fill(0)`. If a nested encoder forwards
|
|
* caller-owned bytes, it must do so only after detaching them into a disposable copy.
|
|
* @param label - Label used in validation errors.
|
|
* @param lengths - Field lengths or nested coders.
|
|
* @returns Composite fixed-length coder.
|
|
* @example
|
|
* Build a fixed-layout coder from byte lengths and nested coders.
|
|
* ```ts
|
|
* splitCoder('demo', 1, 2).encode([new Uint8Array([1]), new Uint8Array([2, 3])]);
|
|
* ```
|
|
*/
|
|
export function splitCoder(label, ...lengths) {
|
|
const getLength = (c) => typeof c === 'number' ? c : c.bytesLen;
|
|
const bytesLen = lengths.reduce((sum, a) => sum + getLength(a), 0);
|
|
return {
|
|
bytesLen,
|
|
encode: (bufs) => {
|
|
const res = new Uint8Array(bytesLen);
|
|
for (let i = 0, pos = 0; i < lengths.length; i++) {
|
|
const c = lengths[i];
|
|
const l = getLength(c);
|
|
const b = typeof c === 'number' ? bufs[i] : c.encode(bufs[i]);
|
|
abytes_(b, l, label);
|
|
res.set(b, pos);
|
|
if (typeof c !== 'number')
|
|
b.fill(0); // clean
|
|
pos += l;
|
|
}
|
|
return res;
|
|
},
|
|
decode: (buf) => {
|
|
abytes_(buf, bytesLen, label);
|
|
const res = [];
|
|
for (const c of lengths) {
|
|
const l = getLength(c);
|
|
const b = buf.subarray(0, l);
|
|
res.push(typeof c === 'number' ? b : c.decode(b));
|
|
buf = buf.subarray(l);
|
|
}
|
|
return res;
|
|
},
|
|
};
|
|
}
|
|
// nano-packed.array (fixed size)
|
|
/**
|
|
* Builds a fixed-length vector coder from another fixed-length coder.
|
|
* Element decoding receives `subarray(...)` views, so aliasing depends on the element coder.
|
|
* Element coder `encode(...)` results are treated as owned scratch: `vecCoder` copies them into
|
|
* the output and then zeroizes them with `fill(0)`. If an element encoder forwards caller-owned
|
|
* bytes, it must do so only after detaching them into a disposable copy. `vecCoder` also trusts
|
|
* the `BytesCoderLen` contract: each encoded element must already be exactly `c.bytesLen` bytes.
|
|
* @param c - Element coder.
|
|
* @param vecLen - Number of elements in the vector.
|
|
* @returns Fixed-length vector coder.
|
|
* @example
|
|
* Build a fixed-length vector coder from another fixed-length coder.
|
|
* ```ts
|
|
* vecCoder(
|
|
* { bytesLen: 1, encode: (n: number) => Uint8Array.of(n), decode: (b: Uint8Array) => b[0] || 0 },
|
|
* 2
|
|
* ).encode([1, 2]);
|
|
* ```
|
|
*/
|
|
export function vecCoder(c, vecLen) {
|
|
const coder = c;
|
|
const bytesLen = vecLen * coder.bytesLen;
|
|
return {
|
|
bytesLen,
|
|
encode: (u) => {
|
|
if (u.length !== vecLen)
|
|
throw new RangeError(`vecCoder.encode: wrong length=${u.length}. Expected: ${vecLen}`);
|
|
const res = new Uint8Array(bytesLen);
|
|
for (let i = 0, pos = 0; i < u.length; i++) {
|
|
const b = coder.encode(u[i]);
|
|
res.set(b, pos);
|
|
b.fill(0); // clean
|
|
pos += b.length;
|
|
}
|
|
return res;
|
|
},
|
|
decode: (a) => {
|
|
abytes_(a, bytesLen);
|
|
const r = [];
|
|
for (let i = 0; i < a.length; i += coder.bytesLen)
|
|
r.push(coder.decode(a.subarray(i, i + coder.bytesLen)));
|
|
return r;
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Overwrites supported typed-array inputs with zeroes in place.
|
|
* Accepts direct typed arrays and one-level arrays of them.
|
|
* @param list - Typed arrays or one-level lists of typed arrays to clear.
|
|
* @example
|
|
* Overwrite typed arrays with zeroes.
|
|
* ```ts
|
|
* const buf = Uint8Array.of(1, 2, 3);
|
|
* cleanBytes(buf);
|
|
* ```
|
|
*/
|
|
export function cleanBytes(...list) {
|
|
for (const t of list) {
|
|
if (Array.isArray(t))
|
|
for (const b of t)
|
|
b.fill(0);
|
|
else
|
|
t.fill(0);
|
|
}
|
|
}
|
|
/**
|
|
* Creates a 32-bit mask with the lowest `bits` bits set.
|
|
* @param bits - Number of low bits to keep.
|
|
* @returns Bit mask with `bits` ones.
|
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
* @example
|
|
* Create a low-bit mask for packed-field operations.
|
|
* ```ts
|
|
* const mask = getMask(4);
|
|
* ```
|
|
*/
|
|
export function getMask(bits) {
|
|
if (!Number.isSafeInteger(bits) || bits < 0 || bits > 32)
|
|
throw new RangeError(`expected bits in [0..32], got ${bits}`);
|
|
// JS shifts are modulo 32, so bit 32 needs an explicit full-width mask.
|
|
return bits === 32 ? 0xffffffff : ~(-1 << bits) >>> 0;
|
|
}
|
|
/** Shared empty byte array used as the default context. */
|
|
export const EMPTY = /* @__PURE__ */ Uint8Array.of();
|
|
/**
|
|
* Builds the domain-separated message payload for the pure sign/verify paths.
|
|
* Context length `255` is valid; only `ctx.length > 255` is rejected.
|
|
* @param msg - Message bytes.
|
|
* @param ctx - Optional context bytes.
|
|
* @returns Domain-separated message payload.
|
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
* @example
|
|
* Build the domain-separated payload before direct signing.
|
|
* ```ts
|
|
* const payload = getMessage(new Uint8Array([1, 2]));
|
|
* ```
|
|
*/
|
|
export function getMessage(msg, ctx = EMPTY) {
|
|
abytes_(msg);
|
|
abytes_(ctx);
|
|
if (ctx.length > 255)
|
|
throw new RangeError('context should be 255 bytes or less');
|
|
return concatBytes(new Uint8Array([0, ctx.length]), ctx, msg);
|
|
}
|
|
// DER tag+length plus the shared NIST hash OID arc 2.16.840.1.101.3.4.2.* used by the
|
|
// FIPS 204 / FIPS 205 pre-hash wrappers; the final byte selects SHA-256, SHA-512, SHAKE128,
|
|
// SHAKE256, or another approved hash/XOF under that subtree.
|
|
// 06 09 60 86 48 01 65 03 04 02
|
|
const oidNistP = /* @__PURE__ */ Uint8Array.from([6, 9, 0x60, 0x86, 0x48, 1, 0x65, 3, 4, 2]);
|
|
/**
|
|
* Validates that a hash exposes a NIST hash OID and enough collision resistance.
|
|
* Current accepted surface is broader than the FIPS algorithm tables: any hash/XOF under the NIST
|
|
* `2.16.840.1.101.3.4.2.*` subtree is accepted if its effective `outputLen` is strong enough.
|
|
* XOF callers must pass a callable whose `outputLen` matches the digest length they actually intend
|
|
* to sign; bare `shake128` / `shake256` defaults are too short for the stronger prehash modes.
|
|
* @param hash - Hash function to validate.
|
|
* @param requiredStrength - Minimum required collision-resistance strength in bits.
|
|
* @throws If the hash metadata or collision resistance is insufficient. {@link Error}
|
|
* @example
|
|
* Validate that a hash exposes a NIST hash OID and enough collision resistance.
|
|
* ```ts
|
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
* import { checkHash } from '@noble/post-quantum/utils.js';
|
|
* checkHash(sha256, 128);
|
|
* ```
|
|
*/
|
|
export function checkHash(hash, requiredStrength = 0) {
|
|
if (!hash.oid || !equalBytes(hash.oid.subarray(0, 10), oidNistP))
|
|
throw new Error('hash.oid is invalid: expected NIST hash');
|
|
// FIPS 204 / FIPS 205 require both collision and second-preimage strength; for approved NIST
|
|
// hashes/XOFs under this OID subtree, the collision bound from the configured digest length is
|
|
// the tighter runtime check, so enforce that lower bound here.
|
|
const collisionResistance = (hash.outputLen * 8) / 2;
|
|
if (requiredStrength > collisionResistance) {
|
|
throw new Error('Pre-hash security strength too low: ' +
|
|
collisionResistance +
|
|
', required: ' +
|
|
requiredStrength);
|
|
}
|
|
}
|
|
/**
|
|
* Builds the domain-separated prehash payload for the prehash sign/verify paths.
|
|
* Callers are expected to vet `hash.oid` first, e.g. via `checkHash(...)`; calling this helper
|
|
* directly with a hash object that lacks `oid` currently throws later inside `concatBytes(...)`.
|
|
* Context length `255` is valid; only `ctx.length > 255` is rejected.
|
|
* @param hash - Prehash function.
|
|
* @param msg - Message bytes.
|
|
* @param ctx - Optional context bytes.
|
|
* @returns Domain-separated prehash payload.
|
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
* @example
|
|
* Build the domain-separated prehash payload for external hashing.
|
|
* ```ts
|
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
* import { getMessagePrehash } from '@noble/post-quantum/utils.js';
|
|
* getMessagePrehash(sha256, new Uint8Array([1, 2]));
|
|
* ```
|
|
*/
|
|
export function getMessagePrehash(hash, msg, ctx = EMPTY) {
|
|
abytes_(msg);
|
|
abytes_(ctx);
|
|
if (ctx.length > 255)
|
|
throw new RangeError('context should be 255 bytes or less');
|
|
const hashed = hash(msg);
|
|
return concatBytes(new Uint8Array([1, ctx.length]), ctx, hash.oid, hashed);
|
|
}
|
|
//# sourceMappingURL=utils.js.map
|