311 lines
8.2 KiB
TypeScript
311 lines
8.2 KiB
TypeScript
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> = T extends Promise<infer U>
|
|
? U
|
|
: // biome-ignore lint/suspicious/noExplicitAny: TS quirks
|
|
T extends (...args: any[]) => Promise<infer V>
|
|
? 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<string, Promise<WebAssembly.Module>>();
|
|
|
|
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<ReturnType<typeof WASMInterface>>;
|