yescrypt-wasm
Version:
WebAssembly module for Yescrypt
366 lines (338 loc) • 11.1 kB
text/typescript
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);
}
}