import Mutex from "./mutex"; import { type IDataType, type IEmbeddedWasm, decodeBase64, getDigestHex, getUInt8Buffer, hexStringEqualsUInt8, writeHexToUInt8, } from "./util"; export const MAX_HEAP = 16 * 1024; const WASM_FUNC_HASH_LENGTH = 4; const wasmMutex = new Mutex(); type ThenArg = T extends Promise ? U : // biome-ignore lint/suspicious/noExplicitAny: TS quirks T extends (...args: any[]) => Promise ? V : T; export type IHasher = { /** * Initializes hash state to default value */ init: () => IHasher; /** * Updates the hash content with the given data */ update: (data: IDataType) => IHasher; /** * Calculates the hash of all of the data passed to be hashed with hash.update(). * Defaults to hexadecimal string * @param outputType If outputType is "binary", it returns Uint8Array. Otherwise it * returns hexadecimal string */ digest: { (outputType: "binary"): Uint8Array; (outputType?: "hex"): string; }; /** * Save the current internal state of the hasher for later resumption with load(). * Cannot be called before .init() or after .digest() * * Note that this state can include arbitrary information about the value being hashed (e.g. * could include N plaintext bytes from the value), so needs to be treated as being as * sensitive as the input value itself. */ save: () => Uint8Array; /** * Resume a state that was created by save(). If this state was not created by a * compatible build of hash-wasm, an exception will be thrown. */ load: (state: Uint8Array) => IHasher; /** * Block size in bytes */ blockSize: number; /** * Digest size in bytes */ digestSize: number; }; const wasmModuleCache = new Map>(); export async function WASMInterface(binary: IEmbeddedWasm, hashLength: number) { let wasmInstance = null; let memoryView: Uint8Array = null; let initialized = false; if (typeof WebAssembly === "undefined") { throw new Error("WebAssembly is not supported in this environment!"); } const writeMemory = (data: Uint8Array, offset = 0) => { memoryView.set(data, offset); }; const getMemory = () => memoryView; const getExports = () => wasmInstance.exports; const setMemorySize = (totalSize: number) => { wasmInstance.exports.Hash_SetMemorySize(totalSize); const arrayOffset: number = wasmInstance.exports.Hash_GetBuffer(); const memoryBuffer = wasmInstance.exports.memory.buffer; memoryView = new Uint8Array(memoryBuffer, arrayOffset, totalSize); }; const getStateSize = () => { const view = new DataView(wasmInstance.exports.memory.buffer); const stateSize = view.getUint32(wasmInstance.exports.STATE_SIZE, true); return stateSize; }; const loadWASMPromise = wasmMutex.dispatch(async () => { if (!wasmModuleCache.has(binary.name)) { const asm = decodeBase64(binary.data); const promise = WebAssembly.compile(asm); wasmModuleCache.set(binary.name, promise); } const module = await wasmModuleCache.get(binary.name); wasmInstance = await WebAssembly.instantiate(module, { // env: { // emscripten_memcpy_big: (dest, src, num) => { // const memoryBuffer = wasmInstance.exports.memory.buffer; // const memView = new Uint8Array(memoryBuffer, 0); // memView.set(memView.subarray(src, src + num), dest); // }, // print_memory: (offset, len) => { // const memoryBuffer = wasmInstance.exports.memory.buffer; // const memView = new Uint8Array(memoryBuffer, 0); // console.log('print_int32', memView.subarray(offset, offset + len)); // }, // }, }); // wasmInstance.exports._start(); }); const setupInterface = async () => { if (!wasmInstance) { await loadWASMPromise; } const arrayOffset: number = wasmInstance.exports.Hash_GetBuffer(); const memoryBuffer = wasmInstance.exports.memory.buffer; memoryView = new Uint8Array(memoryBuffer, arrayOffset, MAX_HEAP); }; const init = (bits: number = null) => { initialized = true; wasmInstance.exports.Hash_Init(bits); }; const updateUInt8Array = (data: Uint8Array): void => { let read = 0; while (read < data.length) { const chunk = data.subarray(read, read + MAX_HEAP); read += chunk.length; memoryView.set(chunk); wasmInstance.exports.Hash_Update(chunk.length); } }; const update = (data: IDataType) => { if (!initialized) { throw new Error("update() called before init()"); } const Uint8Buffer = getUInt8Buffer(data); updateUInt8Array(Uint8Buffer); }; const digestChars = new Uint8Array(hashLength * 2); const digest = ( outputType: "hex" | "binary", padding: number = null, ): Uint8Array | string => { if (!initialized) { throw new Error("digest() called before init()"); } initialized = false; wasmInstance.exports.Hash_Final(padding); if (outputType === "binary") { // the data is copied to allow GC of the original memory object return memoryView.slice(0, hashLength); } return getDigestHex(digestChars, memoryView, hashLength); }; const save = (): Uint8Array => { if (!initialized) { throw new Error( "save() can only be called after init() and before digest()", ); } const stateOffset: number = wasmInstance.exports.Hash_GetState(); const stateLength: number = getStateSize(); const memoryBuffer = wasmInstance.exports.memory.buffer; const internalState = new Uint8Array( memoryBuffer, stateOffset, stateLength, ); // prefix is 4 bytes from SHA1 hash of the WASM binary // it is used to detect incompatible internal states between different versions of hash-wasm const prefixedState = new Uint8Array(WASM_FUNC_HASH_LENGTH + stateLength); writeHexToUInt8(prefixedState, binary.hash); prefixedState.set(internalState, WASM_FUNC_HASH_LENGTH); return prefixedState; }; const load = (state: Uint8Array) => { if (!(state instanceof Uint8Array)) { throw new Error("load() expects an Uint8Array generated by save()"); } const stateOffset: number = wasmInstance.exports.Hash_GetState(); const stateLength: number = getStateSize(); const overallLength: number = WASM_FUNC_HASH_LENGTH + stateLength; const memoryBuffer = wasmInstance.exports.memory.buffer; if (state.length !== overallLength) { throw new Error( `Bad state length (expected ${overallLength} bytes, got ${state.length})`, ); } if ( !hexStringEqualsUInt8( binary.hash, state.subarray(0, WASM_FUNC_HASH_LENGTH), ) ) { throw new Error( "This state was written by an incompatible hash implementation", ); } const internalState = state.subarray(WASM_FUNC_HASH_LENGTH); new Uint8Array(memoryBuffer, stateOffset, stateLength).set(internalState); initialized = true; }; const isDataShort = (data: IDataType) => { if (typeof data === "string") { // worst case is 4 bytes / char return data.length < MAX_HEAP / 4; } return data.byteLength < MAX_HEAP; }; let canSimplify: (data: IDataType, initParam?: number) => boolean = isDataShort; switch (binary.name) { case "argon2": case "scrypt": canSimplify = () => true; break; case "blake2b": case "blake2s": // if there is a key at blake2 then cannot simplify canSimplify = (data, initParam) => initParam <= 512 && isDataShort(data); break; case "blake3": // if there is a key at blake3 then cannot simplify canSimplify = (data, initParam) => initParam === 0 && isDataShort(data); break; case "xxhash64": // cannot simplify case "xxhash3": case "xxhash128": case "crc64": canSimplify = () => false; break; default: break; } // shorthand for (init + update + digest) for better performance const calculate = ( data: IDataType, initParam = null, digestParam = null, ): string => { if (!canSimplify(data, initParam)) { init(initParam); update(data); return digest("hex", digestParam) as string; } const buffer = getUInt8Buffer(data); memoryView.set(buffer); wasmInstance.exports.Hash_Calculate(buffer.length, initParam, digestParam); return getDigestHex(digestChars, memoryView, hashLength); }; await setupInterface(); return { getMemory, writeMemory, getExports, setMemorySize, init, update, digest, save, load, calculate, hashLength, }; } export type IWASMInterface = ThenArg>;