/** * Abstract logic for hashes. * @module */ import { abool, abytes, anumber, clean, concatBytes, copyBytes, copyFast, isBytes, mkAsync, type AsyncRunOpts, type AsyncSetup, type Asyncify, type TArg, type TRet, } from './utils.ts'; export type HashMod = { readonly segments: { readonly buffer: Uint8Array; // no need to copy on streaming mode readonly 'state.state_chunks': ReadonlyArray; // actual state for iv // Should contain everything needed to export/import and restart a streaming hash. readonly state: Uint8Array; readonly state_chunks: ReadonlyArray; // chunks == batch }; reset( batchPos: number, batchCnt: number, maxWritten: number, blockLen: number, maxBlocks: number ): void; // resets all internal buffers // - pad remainder of block // - returns amount of "padding" blocks to process // - left = how much empty space in last block // NOTE: cannot batch here (returns value) padding( batchPos: number, take: number, maxBlocks: number, left: number, blockLen: number, suffix: number ): number; // proceesses N blocks of size blockLen // left = how much empty space in last block (blockLen-lastBlockSize) // - reads to buffer // - blocks can be less than chunks! (streaming mode). // - only last block can be non-full by construction // - no blocks can be processed after call with isLast processBlocks( batchPos: number, batchCnt: number, blocks: number, maxBlocks: number, blockLen: number, isLast: number, left: number, padBlocks: number ): void; // NOTE: outBlockLen is not neccesarily same as blockLen! // - writes to buffer // - if not isLast we can expect user to ask for more blocks later. processOutBlocks( batchPos: number, batchCnt: number, blocks: number, maxBlocks: number, outBlockLen: number, isLast: number ): void; }; /** Hash algorithm definition shared by all runtime/target wrappers. */ export type HashDef = { /** Optional domain-separation suffix byte applied before the shared padding path. */ suffix?: number; /** Preferred chunk count for batch helpers. */ chunks?: number; // = 16 /** Compression/input block size in bytes. */ blockLen: number; /** Output block size in bytes when it differs from {@link blockLen}. */ outputBlockLen?: number; /** Default digest length in bytes. */ outputLen: number; /** Whether the hash supports variable-length XOF output. */ canXOF?: boolean; /** Optional ASN.1 object identifier for fixed-output hash variants. */ oid?: TRet; /** * Initialize state and optionally override block count or effective output length. * @param batchPos - batch slot being initialized. * @param maxBlocks - maximum number of blocks available in the shared buffers. * @param mod - backend hash module implementation for this batch slot. * @param hash - public hash helper being configured. * @param opts - merged hash and output options for this initialization; see {@link MergeOpts}. * @returns Optional overrides for preloaded block count or effective digest length. */ init?: ( batchPos: number, maxBlocks: number, mod: Mod, hash: HashInstance, opts: MergeOpts // We can use HashOpts via `this`. ) => void | { blocks?: number; outputLen?: number }; }; /* Since we finally can do zero-alloc hashes, we should expose nice zero-alloc API for them: - we write to user provided buffer at user provided position - user provided buffer can be bigger than required length, but not less - user may ask for less output from non-xof hashes (if hash is used for consistency checks instead of cryptography) - out.length >= outPos+dkLen? */ /** Shared output-buffer options for one-shot and streaming hash APIs. */ export type OutputOpts = { /** Optional destination buffer to write the digest or XOF output into. */ out?: TArg; /** Starting offset inside {@link out}. */ outPos?: number; /** Requested digest length in bytes. */ dkLen?: number; // outLen, but compat with old API. }; /** Options accepted by multi-input hash APIs: `chunks` and `parallel`. */ export type HashBatchOpts = OutputOpts & { /** * Opaque state from `create().exportState()`. * The only supported use is passing it back to the same hash, platform, and library version. */ prefixState?: TArg; }; /** Options accepted by `parallel`, including caller-owned per-lane output views. */ export type HashParallelOpts = Omit & { /** Optional flat output buffer or one exact-size destination view per input lane. */ out?: TArg; /** * Opaque state from `create().exportState()`. * The only supported use is passing it back to the same hash, platform, and library version. */ prefixState?: TArg; }; type ParallelOutput = { dkLen: number; out: TRet[]; }; const checkParallelOutput = ( opts: TArg, count: number, outputLen: number, canXOF?: boolean ): TRet => { const raw = (isBytes(opts) ? {} : opts || {}) as HashParallelOpts; if (raw.dkLen !== undefined) anumber(raw.dkLen, 'opts.dkLen'); if (raw.outPos !== undefined) anumber(raw.outPos, 'opts.outPos'); const dkLen = (raw.dkLen === undefined ? outputLen : raw.dkLen) | 0; if (!canXOF && dkLen > outputLen) throw new RangeError(`"opts.dkLen" expected <= ${outputLen}, got ${dkLen}`); if (Array.isArray(raw.out)) { if (raw.outPos !== undefined && raw.outPos !== 0) throw new Error('outPos is not supported with output view arrays'); if (raw.out.length !== count) throw new RangeError('output array length must match messages length'); for (let i = 0; i < count; i++) abytes(raw.out[i], dkLen, 'output'); return { dkLen, out: raw.out as TRet[] } as TRet; } if (raw.out !== undefined) abytes(raw.out, undefined, 'output'); const out = raw.out || new Uint8Array(dkLen * count); const outPos = (raw.outPos === undefined ? 0 : raw.outPos) | 0; if (outPos < 0 || outPos + dkLen * count > out.length) throw new RangeError('out/outPos too small'); const slices: TRet[] = []; for (let i = 0; i < count; i++) { const pos = outPos + i * dkLen; slices.push(out.subarray(pos, pos + dkLen) as TRet); } return { dkLen, out: slices } as TRet; }; type CreateOutputOpts = Pick; type CreateOpts = MergeOpts; declare const HASH_STATE: unique symbol; /** * Opaque exported hash state; not necessarily a byte array. * The value is an owned cleanup handle for one hash/platform/version only. */ export type HashState = { /** Internal brand that prevents treating arbitrary values as exported hash states. */ readonly [HASH_STATE]: true; }; /** Streaming hash instance returned by {@link HashInstance.create}. */ export type HashStream = { // streaming mode /** Input block size in bytes for the underlying hash. */ blockLen: number; /** Current digest length in bytes, including `create()` digest-length overrides. */ outputLen: number; /** Whether this stream supports variable-length XOF output via xof()/xofInto(). */ canXOF: boolean; /** Absorb more message bytes into the stream and return the same stream. */ update(msg: TArg): HashStream; // this, but without weird recursive types // finish(): void; /** * Finalize the stream and return the digest bytes. * @param opts - optional {@link OutputOpts} controlling digest length or destination buffer. * @returns Digest bytes. */ digest(opts?: OutputOpts): TRet; /** Wipe internal state and make the stream unusable. */ destroy(): void; /** * Finalize the stream and return variable-length XOF output. * @param bytes - number of XOF bytes to return. * @param opts - optional {@link OutputOpts} controlling output length or destination buffer. * @returns XOF output bytes. */ xof(bytes: number, opts?: OutputOpts): TRet; // clone /** Copy the current stream state into another stream or a freshly created clone target. */ _cloneInto(to?: HashStream): HashStream; /** Clone the current stream state. */ clone(): HashStream; /** * Export an opaque state that can be reused as `prefixState` by this exact hash/platform/version. * Callers must eventually pass it to `hash.cleanState(state)`. */ exportState(): TRet; // Fixed-size in-place digest: writes only the digest prefix and returns nothing. /** * Finalize the stream and write the fixed-size digest into `buf`. * @param buf - destination buffer for the digest bytes. */ digestInto(buf: TArg): void; // Variable-size XOF output: fills the whole destination buffer and returns it back. /** * Finalize the stream and fill `buf` with XOF output. * @param buf - destination buffer for XOF output. * @returns The filled destination buffer. */ xofInto(buf: TArg): TRet; }; type MergeOpts = [Opts] extends [undefined] ? Out : Opts & Out; /** One-shot hash helper plus streaming constructor and metadata. */ export type HashInstance = Asyncify< (msg: TArg, opts?: MergeOpts) => TRet > & { // process multiple messages without concatBytes chunks: Asyncify< (chunks: TArg, opts?: MergeOpts) => TRet >; parallel: Asyncify< (chunks: TArg, opts?: MergeOpts) => TRet >; // create() feeds initHash(), so dkLen must be valid before update(). create: (opts?: CreateOpts) => HashStream; /** Wipe and invalidate an opaque state returned by `create().exportState()`. */ cleanState: (state: TArg) => void; getPlatform: () => string | undefined; getDefinition: () => HashDef; isSupported?: () => boolean | Promise; blockLen: number; outputLen: number; // Whether this hash surface supports variable-length XOF output. canXOF: boolean; oid?: TRet; }; const BRAND = /* @__PURE__ */ Symbol('hash_impl_brand'); const brandSet = /* @__PURE__ */ new WeakSet(); function isBranded(x: unknown): x is object { return typeof x === 'function' || (typeof x === 'object' && x !== null) ? brandSet.has(x as object) : false; } // Universal generic wrapper export function mkHash( modFn: () => Mod, def_: TArg>, platform?: string ): TRet> { const def = def_ as HashDef; const { outputLen, blockLen, suffix = 0, init, canXOF, oid } = def; const outBlockLen = def.outputBlockLen ? def.outputBlockLen : def.outputLen; let hashImpl: ( msg: TArg, opts?: TArg> ) => TRet; let hashAsyncImpl: ( msg: TArg, opts?: TArg & AsyncRunOpts> ) => Promise>; let chunksImpl: ( parts: TArg, opts?: TArg ) => TRet; let chunksAsyncImpl: ( parts: TArg, opts?: TArg ) => Promise>; let parallelImpl: ( chunks: TArg, opts?: TArg ) => TRet; let parallelAsyncImpl: ( chunks: TArg, opts?: TArg ) => Promise>; let createImpl: (opts?: TArg>) => TRet>; let cleanStateImpl: (state: TArg) => void; let inited = false; function lazyInit() { if (inited) throw new Error('second lazyInit call'); const mod = modFn(); inited = true; const { processBlocks, processOutBlocks, padding, reset } = mod; const BUFFER: Uint8Array = mod.segments.buffer; const STATE: Uint8Array = mod.segments.state_chunks[0]; const STATE_CHUNKS: ReadonlyArray = mod.segments.state_chunks; const parallelChunks = mod.segments.state_chunks.length; const maxBlocks = Math.floor(BUFFER.length / blockLen); const chunks = maxBlocks - 2; // 2 for padding if (chunks < 1) throw new Error('wrong chunks'); const maxOutBlocks = Math.floor(BUFFER.length / outBlockLen); type PrefixState = { state: Uint8Array; buf: Uint8Array }; const prefixStates = new WeakSet(); function initHash(opts: TArg>, batchPos = 0) { const rawOpts = opts as MergeOpts; reset(batchPos, 1, 0, outBlockLen, maxOutBlocks); let blocks = 0; let streamOutputLen = outputLen; if (rawOpts.dkLen !== undefined) { anumber(rawOpts.dkLen, 'opts.dkLen'); streamOutputLen = rawOpts.dkLen | 0; if (!canXOF && streamOutputLen > outputLen) throw new RangeError(`"opts.dkLen" expected <= ${outputLen}, got ${streamOutputLen}`); } if (init) { const i = init(batchPos, maxBlocks, mod, hash, rawOpts); if (i && i.blocks !== undefined) blocks = i.blocks; if (i && i.outputLen !== undefined) streamOutputLen = i.outputLen; } return { blocks, outputLen: streamOutputLen }; } // reset(0, 1); function processMessage(msg: TArg, blocks: number) { // Preloaded blocks from init(): e.g., keyed BLAKE2 writes key||zeros into BUFFER // Invariant: these are full blocks, already in BUFFER, and must *not* be treated as 'last'. if (blocks > 0) { if (msg.length === 0) { const padBlocks = padding(0, blocks * blockLen, maxBlocks, 0, blockLen, suffix); processBlocks(0, 1, blocks + padBlocks, maxBlocks, blockLen, 1, 0, padBlocks); return; } processBlocks(0, 1, blocks, maxBlocks, blockLen, 0, 0, 0); } let pos = 0; do { const take = Math.min(msg.length - pos, chunks * blockLen); copyFast(BUFFER, 0, msg, pos, take); const isLast = pos + take == msg.length; // How many full/partial blocks do we have here? Can be zero. let blocks = Math.ceil(take / blockLen); // now, we need position where we add padding? take? // How much space remains? blockLen-rem, except rem=0. const left = blocks * blockLen - take; let padBlocks = 0; if (isLast) { // suffix for sha3 and length for blake1, nothing for others padBlocks = padding(0, take, maxBlocks, left, blockLen, suffix); blocks += padBlocks; } processBlocks(0, 1, blocks, maxBlocks, blockLen, isLast ? 1 : 0, left, padBlocks); pos += take; } while (pos < msg.length); // no extra final take=0 pass } function processMessages( parts: TArg, blocks: number, prefix?: TArg ) { // TODO: cleanup, garbage. const cap = (chunks * blockLen) | 0; // max bytes per batch // total length to keep isLast identical to single-buffer path const prefixLen = prefix ? prefix.buf.length : 0; let totalLen = prefixLen; for (let k = 0; k < parts.length; k++) totalLen += parts[k].length; // Preloaded blocks from init(): // those are full blocks, already in BUFFER, and must NOT be treated as 'last'. if (blocks > 0) { if (totalLen === 0) { const padBlocks = padding(0, blocks * blockLen, maxBlocks, 0, blockLen, suffix); processBlocks(0, 1, blocks + padBlocks, maxBlocks, blockLen, 1, 0, padBlocks); return; } processBlocks(0, 1, blocks, maxBlocks, blockLen, 0, 0, 0); } let pos = 0; // global position in virtual concatenation let idx = 0; // which part let off = 0; // offset within current part while (pos < totalLen) { const take = Math.min(totalLen - pos, cap) | 0; // fill BUFFER[0..take) from parts[idx..] without concat let written = 0; if (prefix && pos < prefixLen) { const n = Math.min(take, prefixLen - pos) | 0; copyFast(BUFFER, written, prefix.buf, pos, n); written += n; } while (written < take) { const cur = parts[idx]; const rem = (cur.length - off) | 0; const n = (take - written < rem ? take - written : rem) | 0; copyFast(BUFFER, written, cur, off, n); written += n; off += n; if (off === cur.length) { idx++; off = 0; } } const isLast = pos + take === totalLen; let blocks = Math.ceil(take / blockLen) | 0; const left = (blocks * blockLen - take) | 0; let padBlocks = 0; if (isLast) { padBlocks = padding(0, take, maxBlocks, left, blockLen, suffix) | 0; blocks = (blocks + padBlocks) | 0; } processBlocks(0, 1, blocks, maxBlocks, blockLen, isLast ? 1 : 0, left, padBlocks); pos += take; } return; } // same as processMessage but with chunkPos. function processMessageParallel( msg: TArg, chunkPos: number, chunkLen: number, blocks: number, maxBlocks: number, prefix?: TArg ) { const chunks = maxBlocks - 2; if (chunks < 1) throw new Error('wrong chunks'); const prefixLen = prefix ? prefix.buf.length : 0; const msgLen = msg[chunkPos].length; const totalLen = prefixLen + msgLen; const copyLane = (dst: number, lane: TArg, pos: number, take: number) => { let written = 0; if (prefix && pos < prefixLen) { const n = Math.min(take, prefixLen - pos); copyFast(BUFFER, dst, prefix.buf, pos, n); written += n; } if (written < take) copyFast(BUFFER, dst + written, lane, pos + written - prefixLen, take - written); }; // Caller validates per-group size equality before touching module state. if (blocks > 0) { if (totalLen === 0) { // No more message input: do a normal finalization over an empty tail. // Keep 'left' consistent with the wrapper contract for take=0: left=0. const padBlocks = padding(0, blocks * blockLen, maxBlocks, 0, blockLen, suffix); for (let j = 0; j < chunkLen; j++) { const pb2 = padding(j, blocks * blockLen, maxBlocks, 0, blockLen, suffix); if (pb2 !== padBlocks) throw new Error('different padding across batch'); } processBlocks(0, chunkLen, blocks + padBlocks, maxBlocks, blockLen, 1, 0, padBlocks); return; } processBlocks(0, chunkLen, blocks, maxBlocks, blockLen, 0, 0, 0); } let pos = 0; do { const take = Math.min(totalLen - pos, chunks * blockLen); for (let i = 0; i < chunkLen; i++) { copyLane(i * maxBlocks * blockLen, msg[chunkPos + i], pos, take); } const isLast = pos + take == totalLen; // How many full/partial blocks do we have here? Can be zero. let blocks = Math.ceil(take / blockLen); // How much space remains? blockLen-rem, except rem=0. const left = blocks * blockLen - take; let padBlocks = 0; if (isLast) { // suffix for sha3 and length for blake1, nothing for others padBlocks = padding(0, take, maxBlocks, left, blockLen, suffix); for (let j = 1; j < chunkLen; j++) { const pb2 = padding(j, take, maxBlocks, left, blockLen, suffix); if (pb2 !== padBlocks) throw new Error('parallel batch: different padding'); } blocks += padBlocks; } processBlocks(0, chunkLen, blocks, maxBlocks, blockLen, isLast ? 1 : 0, left, padBlocks); pos += take; } while (pos < totalLen); // no extra final take=0 pass return; } function checkOutputOpts( o = {} as TArg, bytes?: number, defaultLen: number = outputLen ): TRet<{ dkLen: number; out: TRet; outPos: number }> { const raw = o as OutputOpts; if (raw.dkLen !== undefined) anumber(raw.dkLen, 'opts.dkLen'); if (raw.outPos !== undefined) anumber(raw.outPos, 'opts.outPos'); if (raw.out !== undefined) abytes(raw.out, undefined, 'output'); if (bytes !== undefined) anumber(bytes, 'xof.bytes'); let dkLen = bytes !== undefined ? bytes : (raw.dkLen === undefined ? defaultLen : raw.dkLen) | 0; // Old awasm hash output opts intentionally allow requesting a shorter fixed digest, but // must reject oversize lengths instead of silently clamping or zero-extending the tail. if (!canXOF && dkLen > outputLen) throw new RangeError(`"opts.dkLen" expected <= ${outputLen}, got ${dkLen}`); const out = raw.out || new Uint8Array(dkLen); const outPos = (raw.outPos === undefined ? 0 : raw.outPos) | 0; if (outPos < 0 || outPos + dkLen > out.length) throw new RangeError('out/outPos too small'); return { dkLen, out: out as TRet, outPos } as TRet<{ dkLen: number; out: TRet; outPos: number; }>; } function processOutput( o = {} as TArg, checked: TArg> = checkOutputOpts(o) ): TRet<{ maxWritten: number; out: TRet }> { const { dkLen, out, outPos } = checked as ReturnType; const batch = chunks | 0; const blocksTotal = ((dkLen + outBlockLen - 1) / outBlockLen) | 0; // ceil div let produced = 0, blocksLeft = blocksTotal; let maxWritten = 0; while (blocksLeft > 0) { const takeBlocks = blocksLeft > batch ? batch : blocksLeft; const isLast = blocksLeft === takeBlocks ? 1 : 0; processOutBlocks(0, 1, takeBlocks, maxOutBlocks, outBlockLen, isLast); const emitted = takeBlocks * outBlockLen; const need = dkLen - produced; const takeBytes = need < emitted ? need : emitted; copyFast(out, outPos + produced, BUFFER, 0, takeBytes); maxWritten = Math.max(maxWritten, takeBytes, takeBlocks * outBlockLen); produced += takeBytes; blocksLeft -= takeBlocks; } // NOTE: it would be nice to return slices subarray here, but it is not free! return { maxWritten, out } as TRet<{ maxWritten: number; out: TRet }>; } function processOutputParallel( chunkLen: number, maxOutBlocks: number, checked: TArg, outOffset: number ): TRet<{ out: TRet[]; maxWritten: number }> { const chunks = maxOutBlocks; const { dkLen, out } = checked as ParallelOutput; let maxWritten = 0; const batch = chunks | 0; const blocksTotal = ((dkLen + outBlockLen - 1) / outBlockLen) | 0; // ceil div let produced = 0, blocksLeft = blocksTotal; while (blocksLeft > 0) { const takeBlocks = blocksLeft > batch ? batch : blocksLeft; const isLast = blocksLeft === takeBlocks ? 1 : 0; processOutBlocks(0, chunkLen, takeBlocks, maxOutBlocks, outBlockLen, isLast); const emitted = takeBlocks * outBlockLen; const need = dkLen - produced; const takeBytes = need < emitted ? need : emitted; for (let i = 0; i < chunkLen; i++) { copyFast(out[outOffset + i], produced, BUFFER, i * outBlockLen * maxOutBlocks, takeBytes); // Some modules (e.g. blake3) may write full output blocks even when only part is copied out. // Track emitted block span for reset() so unread bytes are also zeroized. maxWritten = Math.max(maxWritten, takeBytes, emitted); } produced += takeBytes; blocksLeft -= takeBlocks; } // NOTE: it would be nice to return slices subarray here, but it is not free! return { out, maxWritten } as TRet<{ out: TRet[]; maxWritten: number }>; } const stateBytes = (state: TArg) => { if (!isBytes(state)) throw new Error('prefixState is not from this hash'); if (state.length < STATE.length) throw new Error('prefixState is too short'); // Byte-state backends serialize only `state || partialBlock`; a full block must be flushed. if (state.length >= STATE.length + blockLen) throw new Error('prefixState is too long'); if (!prefixStates.has(state)) throw new Error('prefixState is not from this hash'); return state; }; const prefixState = (opts = {} as TArg) => { const raw = opts as Opts & (HashBatchOpts | HashParallelOpts); if (raw.prefixState === undefined) return; const state = stateBytes(raw.prefixState); return { state: state.subarray(0, STATE.length), buf: state.subarray(STATE.length) }; }; class StreamHash { canXOF: boolean; blockLen: number; outputLen: number; private buf: Uint8Array; private pos: number; private state: Uint8Array; private finished: boolean; private destroyed: boolean; constructor(opts: any) { if (opts.streamBufLen % blockLen || opts.streamBufLen < blockLen) throw new Error('wrong streamBufLen'); this.finished = false; this.destroyed = false; // this.reserve = blockLen; // TODO: change when sha2 this.buf = new Uint8Array(opts.streamBufLen); this.pos = 0; if (opts.blocks !== undefined) { if (opts.blocks * blockLen > this.buf.length) throw new Error('too much blocks'); copyFast(this.buf, 0, BUFFER, 0, opts.blocks * blockLen); this.pos = opts.blocks * blockLen; } this.canXOF = !!canXOF; this.blockLen = blockLen; this.outputLen = opts.outputLen; this.state = copyBytes(STATE); reset(0, 1, 0, outBlockLen, maxOutBlocks); // cleanup } private restoreState() { copyFast(STATE, 0, this.state, 0, STATE.length); } private saveState() { this.state = copyBytes(STATE); } update(msg: Uint8Array): this { abytes(msg); if (this.destroyed) throw new Error('Hash instance has been destroyed'); if (this.finished) throw new Error('Hash#digest() has already been called'); let msgPos = 0; let blocks = Math.ceil((this.pos + msg.length) / blockLen) - 1; // never process last block here! if (blocks > 0) { this.restoreState(); let takeBlocks = Math.min(chunks, blocks); // first can be unaligned! const msgFirstLen = takeBlocks * blockLen - this.pos; copyFast(BUFFER, 0, this.buf, 0, this.pos); copyFast(BUFFER, this.pos, msg, msgPos, msgFirstLen); this.pos = 0; msgPos += msgFirstLen; processBlocks(0, 1, takeBlocks, maxBlocks, blockLen, 0, 0, 0); blocks -= takeBlocks; for (; msgPos < msg.length && blocks; ) { const takeBlocks = Math.min(chunks, blocks); copyFast(BUFFER, 0, msg, msgPos, takeBlocks * blockLen); processBlocks(0, 1, takeBlocks, maxBlocks, blockLen, 0, 0, 0); blocks -= takeBlocks; msgPos += takeBlocks * blockLen; } this.saveState(); reset(0, 1, 0, outBlockLen, maxOutBlocks); } // save leftovers const leftover = msg.length - msgPos; if (leftover) { copyFast(this.buf, this.pos, msg, msgPos, leftover); this.pos += leftover; } return this; } private finish() { if (this.finished) throw new Error('Hash#digest() has already been called'); this.restoreState(); this.finished = true; const isLast = true; let take = this.pos; let blocks = Math.ceil(take / blockLen); copyFast(BUFFER, 0, this.buf, 0, this.pos); const left = blocks * blockLen - take; // how much space we have. blockLen-rem except when rem=0 let padBlocks = 0; if (isLast) { padBlocks = padding(0, take, maxBlocks, left, blockLen, suffix); // suffix for sha3 and length for blake1, nothing for others blocks += padBlocks; } processBlocks(0, 1, blocks, maxBlocks, blockLen, 1, left, padBlocks); this.pos = outBlockLen; return blocks * blockLen; } digest(opts: OutputOpts = {}): TRet { if (this.destroyed) throw new Error('Hash instance has been destroyed'); const outChecked = checkOutputOpts(opts, undefined, this.outputLen); this.finish(); const { out: res, maxWritten } = processOutput(opts, outChecked); this.destroy(); reset(0, 1, maxWritten, outBlockLen, maxOutBlocks); return res; } /** * Resets internal state. Makes Hash instance unusable. * Reset is impossible for keyed hashes if key is consumed into state. If digest is not consumed * by user, they will need to manually call `destroy()` when zeroing is necessary. */ destroy(): void { this.destroyed = true; this.state.fill(0); this.buf.fill(0); } xof(bytes: number, opts: OutputOpts = {}): TRet { if (!canXOF) throw new Error('XOF is not possible for this instance'); if (this.destroyed) throw new Error('Hash instance has been destroyed'); const outChecked = checkOutputOpts(opts, bytes); if (!this.finished) { this.finish(); this.saveState(); } // XOF mutates module state via processOutBlocks(). Snapshot/restore around calls so // stream instances don't leak squeeze state into global hash wrappers. this.restoreState(); const { out, outPos } = outChecked; for (let pos = 0; pos < bytes; ) { if (this.pos >= outBlockLen) { // get next squeeze block processOutBlocks(0, 1, 1, maxOutBlocks, outBlockLen, 0); // never 'last' in XOF copyFast(this.buf, 0, BUFFER, 0, outBlockLen); this.pos = 0; } const take = (outBlockLen - this.pos < bytes - pos ? outBlockLen - this.pos : bytes - pos) | 0; copyFast(out, (outPos + pos) | 0, this.buf, this.pos, take); this.pos = (this.pos + take) | 0; pos = (pos + take) | 0; } this.saveState(); reset(0, 1, 0, outBlockLen, maxOutBlocks); return out; } // old api digestInto(buf: Uint8Array): void { // digestInto is the fixed-size surface even for XOF hashes, but callers may provide a // larger workspace buffer and expect the tail to stay untouched. abytes(buf, undefined, 'output'); if (buf.length < this.outputLen) // Keep short digest destinations aligned with the shared noble-hashes contract: // wrong output type -> TypeError, too-short output -> RangeError. throw new RangeError( 'digestInto() expects output buffer of length at least ' + this.outputLen ); this.digest({ out: buf.subarray(0, this.outputLen), dkLen: this.outputLen }); } xofInto(buf: Uint8Array): TRet { return this.xof(buf.length, { out: buf }); } /** * Clones hash instance. Unsafe: doesn't check whether `to` is valid. Can be used as `clone()` * when no options are passed. * Reasons to use `_cloneInto` instead of clone: 1) performance 2) reuse instance => all internal * buffers are overwritten => causes buffer overwrite which is used for digest in some cases. * There are no guarantees for clean-up because it's impossible in JS. */ _cloneInto(to?: this): this { if (this.destroyed) throw new Error('Hash instance has been destroyed'); // Allocate target if needed (ensure same stream buffer capacity). const dst = to || (new (this as any).constructor({ streamBufLen: this.buf.length }) as this); if (!(dst instanceof StreamHash)) throw new Error('wrong instance'); if (dst.buf.length !== this.buf.length) throw new Error('wrong buffer length'); // Clones must preserve the public stream metadata too. digest()/digestInto() allocate and // validate against outputLen, xof()/xofInto() gate on canXOF, and callers may read blockLen. dst.blockLen = this.blockLen; dst.outputLen = this.outputLen; dst.canXOF = this.canXOF; // Copy tail / XOF cache content up to bufPos. if (this.pos) copyFast(dst.buf, 0, this.buf, 0, this.pos); dst.pos = this.pos | 0; // Snapshot of full engine state used by restoreState(). dst.state = copyBytes(this.state); // Flags dst.finished = !!this.finished; dst.destroyed = !!this.destroyed; return dst; } // Safe version that clones internal state clone(): this { return this._cloneInto(); } exportState(): TRet { if (this.destroyed) throw new Error('Hash instance has been destroyed'); if (this.finished) throw new Error('Hash#digest() has already been called'); // update() keeps one full tail block unprocessed for finalization; export must absorb it // so a full-block prefix is represented only by the engine state plus a short leftover. if (this.pos === blockLen) { this.restoreState(); copyFast(BUFFER, 0, this.buf, 0, blockLen); processBlocks(0, 1, 1, maxBlocks, blockLen, 0, 0, 0); this.saveState(); this.pos = 0; this.buf.fill(0); reset(0, 1, 0, outBlockLen, maxOutBlocks); } // Returned bytes are exactly `state || partialBlock`, with no outputLen, pos, or header. const out = new Uint8Array(this.state.length + this.pos); out.set(this.state); if (this.pos) out.set(this.buf.subarray(0, this.pos), this.state.length); prefixStates.add(out); return out as unknown as TRet; } } const hashSync = (msg: TArg, opts = {} as MergeOpts) => { abytes(msg); const outChecked = checkOutputOpts(opts); const { blocks } = initHash(opts); processMessage(msg, blocks); const { out: res, maxWritten } = processOutput(opts, outChecked); reset(0, 1, maxWritten, outBlockLen, maxOutBlocks); return res; }; const chunksSync = (parts: TArg, opts = {} as Opts & HashBatchOpts) => { if (!Array.isArray(parts)) throw new Error('expected array of messages'); let total = 0; for (const p of parts) { abytes(p); total += p.length; } const prefix = prefixState(opts); if (prefix && total === 0) throw new Error('prefixState requires a non-empty message'); const outChecked = checkOutputOpts(opts); if (prefix) { reset(0, 1, 0, outBlockLen, maxOutBlocks); copyFast(STATE, 0, prefix.state, 0, prefix.state.length); processMessages(parts, 0, prefix); } else { const { blocks } = initHash(opts); processMessages(parts, blocks); } const { out: res, maxWritten } = processOutput(opts, outChecked); reset(0, 1, maxWritten, outBlockLen, maxOutBlocks); return res; }; const parallelSync = (chunks: TArg, opts = {} as Opts & HashParallelOpts) => { if (!Array.isArray(chunks)) throw new Error('expected array of messages'); const prefix = prefixState(opts); if (prefix && chunks.length === 0) throw new Error('prefixState requires a non-empty message'); // At least 3 blocks (for padding). const maxGroups = Math.floor(BUFFER.length / (blockLen * 3)); const maxOutGroups = Math.floor(BUFFER.length / outBlockLen); // Validate user input first. Wrong input must throw before touching module state/memory. for (let i = 0; i < chunks.length; i += parallelChunks) { const groupLen = Math.min(parallelChunks, chunks.length - i, maxGroups, maxOutGroups); for (let j = 0; j < groupLen; j++) if (!isBytes(chunks[i + j])) throw new Error(`expected Uint8Array, got type=${typeof chunks[i + j]}`); if (prefix && chunks[i].length === 0) throw new Error('prefixState requires a non-empty message'); for (let j = 1; j < groupLen; j++) { if (chunks[i + j].length !== chunks[i].length) throw new Error('different sizes inside chunk'); } } const outChecked = checkParallelOutput(opts, chunks.length, outputLen, canXOF); for (let i = 0; i < chunks.length; i += parallelChunks) { const groupLen = Math.min(parallelChunks, chunks.length - i, maxGroups, maxOutGroups); const maxBlocks = Math.floor(BUFFER.length / (blockLen * groupLen)); const maxOutBlocks = Math.floor(BUFFER.length / (outBlockLen * groupLen)); let blocks = 0; if (prefix) { reset(0, groupLen, 0, outBlockLen, maxOutBlocks); for (let j = 0; j < groupLen; j++) copyFast(STATE_CHUNKS[j], 0, prefix.state, 0, prefix.state.length); } else if (init) { const i = init(0, maxBlocks, mod, hash, opts as any); if (i && i.blocks !== undefined) blocks = i.blocks; for (let j = 0; j < groupLen; j++) { const t = init(j, maxBlocks, mod, hash, opts as any); if ((t && t.blocks) !== (i && i.blocks)) throw new Error('inconsistent init blocks inside parallel group'); } } processMessageParallel(chunks, i, groupLen, blocks, maxBlocks, prefix); const { maxWritten } = processOutputParallel(groupLen, maxOutBlocks, outChecked, i); reset(0, groupLen, maxWritten, outBlockLen, maxOutBlocks); } return outChecked.out; }; const setupTick = ( setup: TArg, total: number, opts?: TArg ) => { const rawSetup = setup as AsyncSetup & { isAsync?: boolean }; const rawOpts = opts as AsyncRunOpts | undefined; return !rawSetup.isAsync ? !rawOpts?.onProgress ? (rawSetup({ total: 0 }), (_inc?: number) => false) : rawSetup({ total, onProgress: rawOpts.onProgress }) : rawSetup({ total, asyncTick: rawOpts?.asyncTick, onProgress: rawOpts?.onProgress, nextTick: rawOpts?.nextTick, }); }; const hashRun = mkAsync(function* ( setup: TArg, msg: TArg, opts = {} as MergeOpts & AsyncRunOpts ) { const setupMode = setup as AsyncSetup & { isAsync?: boolean }; if (!setupMode.isAsync && !opts?.onProgress) return hashSync(msg, opts); const tick = setupTick(setupMode, msg.length, opts); const out = hashSync(msg, opts); if ((setupMode.isAsync || !!opts?.onProgress) && tick(msg.length)) yield; return out; }); const chunksRun = mkAsync(function* ( setup: TArg, parts: TArg, opts = {} as Opts & HashBatchOpts & AsyncRunOpts ) { const setupMode = setup as AsyncSetup & { isAsync?: boolean }; if (!setupMode.isAsync && !opts?.onProgress) return chunksSync(parts, opts); let total = 0; for (const p of parts) total += p.length; const tick = setupTick(setupMode, total, opts); const out = chunksSync(parts, opts); if ((setupMode.isAsync || !!opts?.onProgress) && tick(total)) yield; return out; }); const parallelRun = mkAsync(function* ( setup: TArg, parts: TArg, opts = {} as Opts & HashParallelOpts & AsyncRunOpts ) { const setupMode = setup as AsyncSetup & { isAsync?: boolean }; if (!setupMode.isAsync && !opts?.onProgress) return parallelSync(parts, opts); let total = 0; for (const p of parts) total += p.length; const tick = setupTick(setupMode, total, opts); const out = parallelSync(parts, opts); if ((setupMode.isAsync || !!opts?.onProgress) && tick(total)) yield; return out; }); hashImpl = (msg, opts = {} as TArg>) => hashSync(msg, opts as MergeOpts); hashAsyncImpl = async (msg, opts?: TArg & AsyncRunOpts>) => { const rawOpts = opts as (MergeOpts & AsyncRunOpts) | undefined; if (!rawOpts) return hashSync(msg, {} as MergeOpts); return rawOpts.asyncTick !== undefined || rawOpts.onProgress !== undefined || rawOpts.nextTick !== undefined ? hashRun.async(msg, rawOpts) : hashSync(msg, rawOpts); }; chunksImpl = (parts: TArg, opts = {} as TArg) => chunksSync(parts, opts as Opts & HashBatchOpts); chunksAsyncImpl = async ( parts: TArg, opts?: TArg ) => { const rawOpts = opts as (Opts & HashBatchOpts & AsyncRunOpts) | undefined; if (!rawOpts) return chunksSync(parts, {} as Opts & HashBatchOpts); return rawOpts.asyncTick !== undefined || rawOpts.onProgress !== undefined || rawOpts.nextTick !== undefined ? chunksRun.async(parts, rawOpts) : chunksSync(parts, rawOpts); }; parallelImpl = (parts: TArg, opts = {} as TArg) => parallelSync(parts, opts as Opts & HashParallelOpts); parallelAsyncImpl = async ( parts: TArg, opts?: TArg ) => { const rawOpts = opts as (Opts & HashParallelOpts & AsyncRunOpts) | undefined; if (!rawOpts) return parallelSync(parts, {} as Opts & HashParallelOpts); return rawOpts.asyncTick !== undefined || rawOpts.onProgress !== undefined || rawOpts.nextTick !== undefined ? parallelRun.async(parts, rawOpts) : parallelSync(parts, rawOpts); }; createImpl = (opts = {} as TArg>) => { const rawOpts = opts as CreateOpts; reset(0, 1, 0, outBlockLen, maxOutBlocks); const { blocks, outputLen } = initHash(rawOpts); return new StreamHash({ ...rawOpts, streamBufLen: blockLen, blocks, outputLen }) as TRet< HashStream >; }; cleanStateImpl = (state: TArg) => { const bytes = stateBytes(state); clean(bytes); prefixStates.delete(bytes); }; } // Trampoline: init only on first usage hashImpl = (msg, opts) => { lazyInit(); return hashImpl(msg, opts); }; hashAsyncImpl = (msg, opts) => { lazyInit(); return hashAsyncImpl(msg, opts); }; chunksImpl = (parts, opts) => { lazyInit(); return chunksImpl(parts, opts); }; chunksAsyncImpl = (parts, opts) => { lazyInit(); return chunksAsyncImpl(parts, opts); }; parallelImpl = (chunks, opts) => { lazyInit(); return parallelImpl(chunks, opts); }; parallelAsyncImpl = (chunks, opts) => { lazyInit(); return parallelAsyncImpl(chunks, opts); }; createImpl = (opts) => { lazyInit(); return createImpl(opts); }; cleanStateImpl = (state) => { lazyInit(); return cleanStateImpl(state); }; const hash = ((msg, opts = {} as TArg>) => hashImpl(msg, opts as TArg>)) as TRet>; const chunksFn = (parts: TArg, opts = {} as TArg) => chunksImpl(parts, opts); Object.assign(chunksFn, { async: (parts: TArg, opts?: TArg) => chunksAsyncImpl(parts, opts), }); const parallel = (chunks: TArg, opts = {} as TArg) => parallelImpl(chunks, opts); Object.assign(parallel, { async: (chunks: TArg, opts?: TArg) => parallelAsyncImpl(chunks, opts), }); Object.assign(hash, { async: (msg: TArg, opts?: TArg) => hashAsyncImpl(msg, opts as TArg & AsyncRunOpts> | undefined), chunks: chunksFn, parallel, create: (opts = {} as CreateOpts) => createImpl(opts), cleanState: (state: TArg) => cleanStateImpl(state), getPlatform: () => platform, getDefinition: () => def, canXOF: !!canXOF, outputLen, blockLen, oid, }); Object.defineProperty(hash, BRAND, { value: true, enumerable: false }); brandSet.add(hash); return Object.freeze(hash) as TRet>; } type NobleHashStream = { blockLen: number; outputLen: number; canXOF?: boolean; update(msg: TArg): NobleHashStream; digest(): TRet; digestInto(out: TArg): void; destroy(): void; xof?(bytes: number): TRet; xofInto?(out: TArg): TRet; _cloneInto(to?: NobleHashStream): NobleHashStream; }; type NobleHashImpl = { hash: (msg: TArg, opts?: TArg>) => TRet; create?: (opts?: TArg>) => NobleHashStream; }; export function mkHashNoble( def_: TArg>, impl_: TArg>, platform = 'noble' ): TRet> { const def = def_ as HashDef; const impl = impl_ as NobleHashImpl; const { outputLen, blockLen, canXOF, oid } = def; const rawOutputOpts = (opts?: any) => (isBytes(opts) ? {} : opts || {}) as HashBatchOpts; const hasOutputOpts = (opts?: any) => { const raw = rawOutputOpts(opts); return raw.dkLen !== undefined || raw.out !== undefined || raw.outPos !== undefined; }; const prefixStates = new WeakSet(); const stateStream = (state: TArg) => { if ((typeof state !== 'object' && typeof state !== 'function') || state === null) throw new Error('prefixState is not from this hash'); const stream = state as unknown as NobleHashStream & object; if (!prefixStates.has(stream)) throw new Error('prefixState is not from this hash'); return stream; }; const prefixState = (opts?: TArg) => { const raw = rawOutputOpts(opts); if (raw.prefixState === undefined) return; return stateStream(raw.prefixState); }; const checkOutput = (opts?: any, bytes?: number, defaultLen = outputLen) => { const raw = rawOutputOpts(opts); if (raw.dkLen !== undefined) anumber(raw.dkLen, 'opts.dkLen'); if (raw.outPos !== undefined) anumber(raw.outPos, 'opts.outPos'); if (raw.out !== undefined) abytes(raw.out, undefined, 'output'); if (bytes !== undefined) anumber(bytes, 'xof.bytes'); const dkLen = (bytes !== undefined ? bytes : raw.dkLen === undefined ? defaultLen : raw.dkLen) | 0; if (!canXOF && dkLen > outputLen) throw new RangeError(`"opts.dkLen" expected <= ${outputLen}, got ${dkLen}`); const hasOut = raw.out !== undefined; const out = raw.out || new Uint8Array(dkLen); const outPos = (raw.outPos === undefined ? 0 : raw.outPos) | 0; if (outPos < 0 || outPos + dkLen > out.length) throw new RangeError('out/outPos too small'); return { dkLen, out: out as TRet, outPos, hasOut }; }; const writeInto = ( inner: TArg, out: TArg, dkLen: number, tmp?: TArg ): TRet => { const rawInner = inner as NobleHashStream; const rawOut = out as Uint8Array; let rawTmp = tmp as Uint8Array | undefined; if (canXOF) { rawInner.xofInto!(rawOut.subarray(0, dkLen)); return rawTmp as TRet; } // noble BLAKE2 streams require aligned digestInto() views, but awasm outPos is byte-granular. if (dkLen === outputLen && (rawOut.byteOffset & 3) === 0) rawInner.digestInto(rawOut); else { if (!rawTmp || rawTmp.length < outputLen) rawTmp = new Uint8Array(outputLen); rawInner.digestInto(rawTmp); if (dkLen) copyFast(rawOut, 0, rawTmp, 0, dkLen); } return rawTmp as TRet; }; class Stream { canXOF = !!canXOF; blockLen = blockLen; outputLen: number; private inner: NobleHashStream; constructor(inner: NobleHashStream, outputLen: number) { this.inner = inner; this.outputLen = outputLen; } update(msg: TArg) { abytes(msg); this.inner.update(msg); return this; } digest(opts?: TArg): TRet { if (!hasOutputOpts(opts) && !canXOF && this.outputLen === outputLen) return this.inner.digest(); const checked = checkOutput(opts as TArg, undefined, this.outputLen); const tmp = writeInto( this.inner, checked.out.subarray(checked.outPos, checked.outPos + checked.dkLen), checked.dkLen ); if (tmp) clean(tmp); return checked.out; } destroy(): void { this.inner.destroy(); } xof(bytes: number, opts?: TArg): TRet { if (!canXOF) throw new Error('XOF is not possible for this instance'); if (!hasOutputOpts(opts)) { anumber(bytes, 'xof.bytes'); if (this.inner.xof) return this.inner.xof(bytes); const out = new Uint8Array(bytes); this.inner.xofInto!(out); return out as TRet; } const checked = checkOutput(opts as TArg, bytes); this.inner.xofInto!(checked.out.subarray(checked.outPos, checked.outPos + checked.dkLen)); return checked.out; } digestInto(buf: TArg): void { abytes(buf, undefined, 'output'); if (buf.length < this.outputLen) throw new RangeError( 'digestInto() expects output buffer of length at least ' + this.outputLen ); const tmp = writeInto(this.inner, buf.subarray(0, this.outputLen), this.outputLen); if (tmp) clean(tmp); } xofInto(buf: TArg): TRet { abytes(buf, undefined, 'output'); if (!canXOF) throw new Error('XOF is not possible for this instance'); this.inner.xofInto!(buf); return buf as TRet; } _cloneInto(to?: Stream): Stream { if (to !== undefined && !(to instanceof Stream)) throw new Error('wrong instance'); const inner = this.inner._cloneInto(to?.inner); if (to) { to.inner = inner; to.outputLen = this.outputLen; return to; } return new Stream(inner, this.outputLen); } clone(): Stream { return this._cloneInto(); } exportState(): TRet { // Native noble streams may carry structured state, e.g. BLAKE3. The clone itself is the state. const state = this.inner._cloneInto() as NobleHashStream & object; prefixStates.add(state); return state as unknown as TRet; } } const createSync = (opts = {} as TArg>) => { if (!impl.create) throw new Error('streaming is not supported'); const inner = impl.create(opts); const raw = rawOutputOpts(opts as TArg); if (raw.dkLen !== undefined) anumber(raw.dkLen, 'opts.dkLen'); const fallback = inner.outputLen || outputLen; const len = raw.dkLen === undefined ? fallback : raw.dkLen; if (!canXOF && len > outputLen) throw new RangeError(`"opts.dkLen" expected <= ${outputLen}, got ${len}`); return new Stream(inner, len) as HashStream; }; const hashSync = (msg: TArg, opts = {} as TArg>) => { abytes(msg); const raw = rawOutputOpts(opts); if ( raw.out === undefined && raw.outPos === undefined && (raw.dkLen === undefined || (canXOF && raw.dkLen !== undefined)) ) return impl.hash(msg, opts); const checked = checkOutput(opts as TArg); const res = impl.hash(msg, opts); const { dkLen, out, outPos, hasOut } = checked; if (res.length < dkLen) throw new RangeError('output is too small'); if (!hasOut && res.length === dkLen) return res as TRet; if (dkLen) copyFast(out, outPos, res, 0, dkLen); if (out !== res) clean(res); return out; }; const chunksSync = (parts: TArg, opts = {} as TArg) => { if (!Array.isArray(parts)) throw new Error('expected array of messages'); let total = 0; for (const p of parts) { abytes(p); total += p.length; } const prefix = prefixState(opts); if (prefix && total === 0) throw new Error('prefixState requires a non-empty message'); if (prefix) { const h = new Stream(prefix._cloneInto(), outputLen); for (const p of parts) h.update(p); return h.digest(rawOutputOpts(opts)); } if (impl.create) { const h = createSync(opts as TArg>); for (const p of parts) h.update(p); return h.digest(rawOutputOpts(opts)); } const joined = concatBytes(...parts); try { return hashSync(joined, opts as TArg>); } finally { clean(joined); } }; const parallelSync = (parts: TArg, opts = {} as TArg) => { if (!Array.isArray(parts)) throw new Error('expected array of messages'); for (const p of parts) abytes(p); const prefix = prefixState(opts); if (prefix && parts.length === 0) throw new Error('prefixState requires a non-empty message'); if (prefix) for (const p of parts) if (p.length === 0) throw new Error('prefixState requires a non-empty message'); const checked = checkParallelOutput(opts, parts.length, outputLen, canXOF); let tmp: Uint8Array | undefined; const base = prefix || (impl.create ? impl.create(opts as TArg>) : undefined); let work: NobleHashStream | undefined; for (let i = 0; i < parts.length; i++) { const lane = checked.out[i]; if (base) { work = base._cloneInto(work); work.update(parts[i]); tmp = writeInto(work, lane, checked.dkLen, tmp); } else { const outOpts = ( isBytes(opts) ? { key: opts, out: lane, outPos: 0, dkLen: checked.dkLen } : { ...((opts || {}) as object), out: lane, outPos: 0, dkLen: checked.dkLen } ) as TArg>; hashSync(parts[i], outOpts); } } if (tmp) clean(tmp); if (work) work.destroy(); if (base && base !== prefix) base.destroy(); return checked.out; }; const setupTick = ( setup: TArg, total: number, opts?: TArg ) => { const rawSetup = setup as AsyncSetup & { isAsync?: boolean }; const rawOpts = opts as AsyncRunOpts | undefined; return !rawSetup.isAsync ? !rawOpts?.onProgress ? (rawSetup({ total: 0 }), (_inc?: number) => false) : rawSetup({ total, onProgress: rawOpts.onProgress }) : rawSetup({ total, asyncTick: rawOpts?.asyncTick, onProgress: rawOpts?.onProgress, nextTick: rawOpts?.nextTick, }); }; const hash = ((msg: TArg, opts = {} as TArg>) => hashSync(msg, opts as any)) as TRet>; const chunks = (parts: TArg, opts = {} as TArg) => chunksSync(parts, opts); Object.assign(chunks, { async: mkAsync(function* ( setup: TArg, parts: TArg, opts = {} as TArg ) { const setupMode = setup as AsyncSetup & { isAsync?: boolean }; const rawOpts = opts as any; if (!setupMode.isAsync && !rawOpts?.onProgress) return chunksSync(parts, rawOpts); let total = 0; for (const p of parts) total += p.length; const tick = setupTick(setupMode, total, rawOpts); const out = chunksSync(parts, rawOpts); if ((setupMode.isAsync || !!rawOpts?.onProgress) && tick(total)) yield; return out; }).async, }); const parallel = (parts: TArg, opts = {} as TArg) => parallelSync(parts, opts); Object.assign(parallel, { async: mkAsync(function* ( setup: TArg, parts: TArg, opts = {} as TArg ) { const setupMode = setup as AsyncSetup & { isAsync?: boolean }; const rawOpts = opts as any; if (!setupMode.isAsync && !rawOpts?.onProgress) return parallelSync(parts, rawOpts); let total = 0; for (const p of parts) total += p.length; const tick = setupTick(setupMode, total, rawOpts); const out = parallelSync(parts, rawOpts); if ((setupMode.isAsync || !!rawOpts?.onProgress) && tick(total)) yield; return out; }).async, }); Object.assign(hash, { async: mkAsync(function* ( setup: TArg, msg: TArg, opts = {} as TArg & AsyncRunOpts> ) { const setupMode = setup as AsyncSetup & { isAsync?: boolean }; // TArg expands byte-like option unions here; keep the cast local to the async shim. const rawOpts = opts as any; if (!setupMode.isAsync && !rawOpts?.onProgress) return hashSync(msg, rawOpts); const tick = setupTick(setupMode, msg.length, rawOpts); const out = hashSync(msg, rawOpts); if ((setupMode.isAsync || !!rawOpts?.onProgress) && tick(msg.length)) yield; return out; }).async, chunks, parallel, create: (opts = {} as CreateOpts) => createSync(opts), cleanState(state: TArg) { const stream = stateStream(state); stream.destroy(); prefixStates.delete(stream); }, getPlatform: () => platform, getDefinition: () => def, canXOF: !!canXOF, outputLen, blockLen, oid, }); Object.defineProperty(hash, BRAND, { value: true, enumerable: false }); brandSet.add(hash); return Object.freeze(hash) as TRet>; } type AsyncHashImpl = { hash: (msg: TArg, opts?: MergeOpts) => Promise>; chunks?: ( parts: TArg, opts?: MergeOpts ) => Promise>; parallel?: ( parts: TArg, opts?: MergeOpts ) => Promise>; }; const copyOutput = ( out: TArg, opts: TArg, outputLen: number, canXOF?: boolean ): TRet => { const rawLen = opts?.dkLen; if (rawLen !== undefined) anumber(rawLen, 'dkLen'); const dkLen = rawLen === undefined ? outputLen : rawLen; anumber(dkLen, 'dkLen'); // Old awasm hash output opts intentionally allow requesting a shorter fixed digest, but // must reject oversize lengths instead of silently clamping or zero-extending the tail. if (!canXOF && dkLen > outputLen) throw new RangeError(`"dkLen" expected <= ${outputLen}, got ${dkLen}`); if (out.length < dkLen) throw new RangeError(`expected output length >= dkLen, got ${out.length}`); const msg = out.subarray(0, dkLen); if (!opts?.out) { const res = copyBytes(msg); clean(out); return res as TRet; } const outPos = opts?.outPos || 0; anumber(outPos, 'outPos'); if (opts.out.length < outPos + dkLen) throw new RangeError('output is too small'); opts.out.set(msg, outPos); clean(out); return opts.out as TRet; }; const copyParallelOutput = ( outs: TArg, views: TArg, dkLen: number, outputLen: number, canXOF?: boolean ): TRet => { if (views.length !== outs.length) throw new RangeError('output array length must match messages length'); for (let i = 0; i < outs.length; i++) copyOutput(outs[i], { out: views[i], dkLen }, outputLen, canXOF); return views as TRet; }; const hashSyncError = () => { throw new Error('sync is not supported'); }; export function mkHashAsync( def_: TArg>, impl_: TArg>, platform = 'webcrypto', isSupported?: () => boolean | Promise, meta?: Record ): TRet> { const def = def_ as HashDef; const impl = impl_ as AsyncHashImpl; const { outputLen, blockLen, canXOF, oid } = def; const hash = ((_msg: TArg) => hashSyncError()) as unknown as HashInstance; const hashAsync = async (msg: TArg, opts = {} as Opts & OutputOpts) => { abytes(msg); const out = await impl.hash(msg, opts); return copyOutput(out, opts, outputLen, canXOF); }; const chunks = ((_parts: TArg) => hashSyncError()) as unknown as HashInstance['chunks']; const chunksAsync = async (parts: TArg, opts = {} as Opts & HashBatchOpts) => { if (opts.prefixState !== undefined) throw new Error('prefixState is not supported'); for (let i = 0; i < parts.length; i++) abytes(parts[i]); if (impl.chunks) { const out = await impl.chunks(parts, opts); return copyOutput(out, opts, outputLen, canXOF); } const joined = concatBytes(...parts); try { const out = await impl.hash(joined, opts); return copyOutput(out, opts, outputLen, canXOF); } finally { clean(joined); } }; Object.assign(chunks, { async: chunksAsync }); const parallel = ((_parts: TArg) => hashSyncError()) as unknown as HashInstance['parallel']; const parallelAsync = async (parts: TArg, opts = {} as Opts & HashParallelOpts) => { if (opts.prefixState !== undefined) throw new Error('prefixState is not supported'); for (let i = 0; i < parts.length; i++) abytes(parts[i]); const checked = checkParallelOutput(opts, parts.length, outputLen, canXOF); const runOpts = { ...opts, out: undefined, outPos: undefined } as any; const out = impl.parallel ? await impl.parallel(parts, runOpts) : await Promise.all(parts.map((i) => impl.hash(i, runOpts))); return copyParallelOutput(out, checked.out, checked.dkLen, outputLen, canXOF); }; Object.assign(parallel, { async: parallelAsync }); Object.assign(hash, { async: hashAsync, chunks, parallel, create: () => { throw new Error('streaming is not supported'); }, cleanState: () => { throw new Error('prefixState is not supported'); }, getPlatform: () => platform, getDefinition: () => def, canXOF: !!canXOF, outputLen, blockLen, oid, }); if (meta) Object.assign(hash, meta); if (isSupported) (hash as any).isSupported = isSupported; Object.defineProperty(hash, BRAND, { value: true, enumerable: false }); brandSet.add(hash as object); return Object.freeze(hash) as TRet>; } /* stubbing allows to install specific implementation of hash later. - Some apps (React Native; also ones wish wasm-unsafe-eval) can't use wasm - Some apps (no CORS; broken bundling) can't use wasm_threads - So they use js Risks such as: - Installing an implementation from previous version of the library - Installing a malware Are mitigated by: - Checking for unique Symbol on install() - Test result is preserved in inaccessible WeakSet Multiple installations are allowed, the last one wins. */ type InstallOpts = { onlyMissing?: boolean }; /** Installable hash stub used by the `stub` platform. */ export type Stub = { /** * Install a branded hash implementation into this stub. * @param impl - Hash implementation created by an awasm hash constructor. * @param opts - {@link InstallOpts}; `onlyMissing` leaves an existing implementation unchanged. */ install: (impl: HashInstance, opts?: InstallOpts) => void; }; export function mkHashStub( def_: TArg> ): TRet & Stub> { const def = def_ as HashDef; const { outputLen, blockLen, canXOF, oid } = def; let inner: HashInstance | undefined; function checkInner( inner: TArg | undefined> ): asserts inner is HashInstance { if (inner === undefined) throw new Error('implementation not installed'); } const hash = ((msg, opts = {} as Opts & OutputOpts) => { checkInner(inner); return inner(msg, opts); }) as HashInstance & Stub; const chunks = (parts: TArg, opts = {} as Opts & HashBatchOpts) => { checkInner(inner); return inner.chunks(parts, opts); }; Object.assign(chunks, { async: async (parts: TArg, opts = {} as Opts & HashBatchOpts) => { checkInner(inner); return inner.chunks.async(parts, opts); }, }); const parallel = (chunks: TArg, opts = {} as Opts & HashParallelOpts) => { checkInner(inner); return inner.parallel(chunks, opts); }; Object.assign(parallel, { async(chunks: TArg, opts = {} as Opts & HashParallelOpts) { checkInner(inner); return inner.parallel.async(chunks, opts); }, }); Object.assign(hash, { async: async (msg: TArg, opts = {} as Opts & OutputOpts) => { checkInner(inner); return inner.async(msg, opts); }, chunks, parallel, create(opts = {} as CreateOpts) { checkInner(inner); return inner.create(opts); }, cleanState(state: TArg) { checkInner(inner); return inner.cleanState(state); }, getPlatform: () => { checkInner(inner); return inner.getPlatform(); }, getDefinition: () => { checkInner(inner); return inner.getDefinition(); }, install: (impl: TArg>, opts = {} as TArg) => { const { onlyMissing } = opts as InstallOpts; if (onlyMissing !== undefined) abool(onlyMissing); if (onlyMissing && inner !== undefined) return; // install() accepts only constructor-created hashes: WeakSet branding rejects copied fields, // and exact definition identity rejects same-shaped but different hash families. if (!isBranded(impl)) throw new Error('install: non-branded implementation'); // NOTE: this strict check works because all implementations will use exact same frozen definition // which means it is impossible to use same blockLen/outputLen hash from different definition if (impl.getDefinition() !== def) throw new Error('wrong implementation definition'); inner = impl as HashInstance; }, canXOF, outputLen, blockLen, oid, }); return Object.freeze(hash) as TRet & Stub>; }