UNPKG

yescrypt-wasm

Version:
366 lines (338 loc) 11.1 kB
import { bundled } from './bundled.js'; import { base64ToBytes } from './utils.js'; import yescryptWasm, { MainModule } from './yescrypt_wasm.js'; export * from './utils.js'; type yescrypt_kdf_wasm = ( passwd: number, passwdLen: number, salt: number, saltLen: number, N: bigint, r: number, p: number, t: number, ) => number; // Returns 0 on success, -1 on failure (output buffer is zeroed on failure). type yescrypt_kdf_full = ( flags: number, passwd: number, passwdLen: number, salt: number, saltLen: number, N: bigint, r: number, p: number, t: number, g: number, output: number, outputLen: number, ) => number; // Returns a pointer to a freshly malloc'd, NUL-terminated C string, or 0 (NULL) // on failure. The caller is responsible for _free()'ing a non-zero pointer. type yescrypt_hash_full_ptr = ( flags: number, passwd: number, passwdLen: number, salt: number, saltLen: number, N: bigint, r: number, p: number, t: number, g: number, ) => number; /** * yescrypt flag constants (see yescrypt-c/yescrypt.h). OR these together to * build a custom `flags` value. YESCRYPT_DEFAULTS is what the yescrypt_* helpers * use by default; scrypt_* helpers use 0 (classic scrypt). */ export const YESCRYPT_WORM = 1; export const YESCRYPT_RW = 0x002; export const YESCRYPT_ROUNDS_3 = 0x000; export const YESCRYPT_ROUNDS_6 = 0x004; export const YESCRYPT_GATHER_1 = 0x000; export const YESCRYPT_GATHER_2 = 0x008; export const YESCRYPT_GATHER_4 = 0x010; export const YESCRYPT_GATHER_8 = 0x018; export const YESCRYPT_SIMPLE_1 = 0x000; export const YESCRYPT_SIMPLE_2 = 0x020; export const YESCRYPT_SIMPLE_4 = 0x040; export const YESCRYPT_SIMPLE_8 = 0x060; export const YESCRYPT_SBOX_6K = 0x000; export const YESCRYPT_SBOX_12K = 0x080; export const YESCRYPT_SBOX_24K = 0x100; export const YESCRYPT_SBOX_48K = 0x180; export const YESCRYPT_SBOX_96K = 0x200; export const YESCRYPT_SBOX_192K = 0x280; export const YESCRYPT_SBOX_384K = 0x300; export const YESCRYPT_SBOX_768K = 0x380; export const YESCRYPT_RW_DEFAULTS = YESCRYPT_RW | YESCRYPT_ROUNDS_6 | YESCRYPT_GATHER_4 | YESCRYPT_SIMPLE_2 | YESCRYPT_SBOX_12K; export const YESCRYPT_DEFAULTS = YESCRYPT_RW_DEFAULTS; const FULL_KDF_ARGS = [ 'number', // flags 'number', // passwd 'number', // passwdLen 'number', // salt 'number', // saltLen 'number', // N (bigint) 'number', // r 'number', // p 'number', // t 'number', // g 'number', // output 'number', // outputLen ] as const; const FULL_HASH_ARGS = [ 'number', // flags 'number', // passwd 'number', // passwdLen 'number', // salt 'number', // saltLen 'number', // N (bigint) 'number', // r 'number', // p 'number', // t 'number', // g ] as const; /** * Validate the cost parameters against yescrypt's documented constraints * (see crypto_scrypt / yescrypt_kdf in yescrypt-c/yescrypt.h). Throwing here * gives callers a clear error instead of an all-zero "hash". */ function validateParams(N: number, r: number, p: number): void { if (!Number.isInteger(N) || N < 2 || (N & (N - 1)) !== 0) { throw new RangeError(`yescrypt: N must be a power of two greater than 1 (got ${N})`); } if (!Number.isInteger(r) || r < 1) { throw new RangeError(`yescrypt: r must be a positive integer (got ${r})`); } if (!Number.isInteger(p) || p < 1) { throw new RangeError(`yescrypt: p must be a positive integer (got ${p})`); } if (r * p >= 1 << 30) { throw new RangeError(`yescrypt: r * p must be < 2^30 (got ${r * p})`); } } export class Yescrypt { nByte: number; // The generated MainModule typings omit the heap views that Emscripten // exposes at runtime, so widen with the ones we touch directly. Module: MainModule & { HEAPU8: Uint8Array }; scrypt_kdf_wasm: yescrypt_kdf_wasm; yescrypt_kdf_wasm: yescrypt_kdf_wasm; yescrypt_kdf_full_: yescrypt_kdf_full; yescrypt_hash_full_: yescrypt_hash_full_ptr; scrypt_hash_full_: yescrypt_hash_full_ptr; constructor(Module: MainModule & { HEAPU8: Uint8Array }) { this.nByte = 1; this.Module = Module; // Legacy fixed-flag KDF entry points kept for backward compatibility. this.scrypt_kdf_wasm = this.Module.cwrap('scrypt_kdf_wasm', 'number', [ 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', ]) as yescrypt_kdf_wasm; this.yescrypt_kdf_wasm = this.Module.cwrap('yescrypt_kdf_wasm', 'number', [ 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', ]) as yescrypt_kdf_wasm; // Full-control entry points (return status / pointer so we can surface errors). this.yescrypt_kdf_full_ = this.Module.cwrap('yescrypt_kdf_full', 'number', [ ...FULL_KDF_ARGS, ]) as yescrypt_kdf_full; this.yescrypt_hash_full_ = this.Module.cwrap('yescrypt_hash_full_str', 'number', [ ...FULL_HASH_ARGS, ]) as yescrypt_hash_full_ptr; this.scrypt_hash_full_ = this.Module.cwrap('scrypt_hash_full', 'number', [ ...FULL_HASH_ARGS, ]) as yescrypt_hash_full_ptr; } static async init() { // eslint-disable-next-line @typescript-eslint/no-explicit-any if (typeof (globalThis as any).WebAssembly === 'undefined') { throw new Error('WebAssembly is not enabled with this browser'); } const wasmBinary = base64ToBytes(bundled); const module = await yescryptWasm({ wasmBinary, locateFile: (file: string) => file, }); return new Yescrypt(module as MainModule & { HEAPU8: Uint8Array }); } // https://stackoverflow.com/questions/41875728/pass-a-javascript-array-as-argument-to-a-webassembly-function // Takes an Uint8Array, copies it to the heap and returns a pointer. // Allocates one extra byte and NUL-terminates it so that any strlen-based // consumer in the C code is safe even though the wrappers pass explicit // lengths. arrayToPtr(array: Uint8Array): number { const ptr = this.Module._malloc((array.length + 1) * this.nByte); if (!ptr) { throw new Error('yescrypt: out of WASM memory (malloc failed)'); } // Re-read HEAPU8 *after* _malloc: with ALLOW_MEMORY_GROWTH the heap may // have grown and the previous typed-array view become detached. const heap = this.Module.HEAPU8; const base = ptr / this.nByte; heap.set(array, base); heap[base + array.length] = 0; return ptr; } // Takes a pointer and array length, and returns a Uint8Array from the heap ptrToArray(ptr: number, length: number): Uint8Array { const array = new Uint8Array(length); const pos = ptr / this.nByte; array.set(this.Module.HEAPU8.subarray(pos, pos + length)); return array; } freePtr(ptr: number) { if (ptr) { this.Module._free(ptr); } } private runKdf( flags: number, passwd: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, t: number, g: number, dklen: number, ): Uint8Array { validateParams(N, r, p); if (!Number.isInteger(dklen) || dklen < 1) { throw new RangeError(`yescrypt: dklen must be a positive integer (got ${dklen})`); } let passwdPtr = 0; let saltPtr = 0; let outPtr = 0; try { passwdPtr = this.arrayToPtr(passwd); saltPtr = this.arrayToPtr(salt); outPtr = this.Module._malloc(dklen); if (!outPtr) { throw new Error('yescrypt: out of WASM memory (malloc failed)'); } const status = this.yescrypt_kdf_full_( flags, passwdPtr, passwd.length, saltPtr, salt.length, BigInt(N), r, p, t, g, outPtr, dklen, ); if (status !== 0) { throw new Error( `yescrypt: KDF failed (invalid parameters or out of memory; N=${N}, r=${r}, p=${p})`, ); } return this.ptrToArray(outPtr, dklen); } finally { this.freePtr(passwdPtr); this.freePtr(saltPtr); this.freePtr(outPtr); } } private runHash( impl: yescrypt_hash_full_ptr, label: string, flags: number, passwd: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, t: number, g: number, ): string { validateParams(N, r, p); let passwdPtr = 0; let saltPtr = 0; let resultPtr = 0; try { passwdPtr = this.arrayToPtr(passwd); saltPtr = this.arrayToPtr(salt); resultPtr = impl(flags, passwdPtr, passwd.length, saltPtr, salt.length, BigInt(N), r, p, t, g); if (!resultPtr) { throw new Error( `yescrypt: ${label} failed (invalid parameters or out of memory; N=${N}, r=${r}, p=${p})`, ); } return this.Module.UTF8ToString(resultPtr); } finally { this.freePtr(passwdPtr); this.freePtr(saltPtr); // The C helper malloc'd the result string; free it here. this.freePtr(resultPtr); } } scrypt_kdf( passwd: Uint8Array, salt: Uint8Array, N = 4096, r = 32, p = 1, t = 0, flags = 0, g = 0, dklen = 64, ): Uint8Array { return this.runKdf(flags, passwd, salt, N, r, p, t, g, dklen); } yescrypt_kdf( passwd: Uint8Array, salt: Uint8Array, N = 4096, r = 32, p = 1, t = 0, flags = YESCRYPT_DEFAULTS, g = 0, dklen = 64, ): Uint8Array { return this.runKdf(flags, passwd, salt, N, r, p, t, g, dklen); } scrypt_hash( passwd: Uint8Array, salt: Uint8Array, N = 4096, r = 32, p = 1, t = 0, flags = 0, g = 0, ): string { return this.runHash(this.scrypt_hash_full_, 'scrypt_hash', flags, passwd, salt, N, r, p, t, g); } yescrypt_hash( passwd: Uint8Array, salt: Uint8Array, N = 4096, r = 32, p = 1, t = 0, flags = YESCRYPT_DEFAULTS, g = 0, ): string { return this.runHash(this.yescrypt_hash_full_, 'yescrypt_hash', flags, passwd, salt, N, r, p, t, g); } }