Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285// node:crypto. Hashing and HMAC are synchronous over ./digest.ts (a TypeScript// stopgap for the WASM residue); randomness and webcrypto are the browser's own.// hkdf and generateKey are genuinely async, backed by WebCrypto and the RNG.// Everything else that would need a real implementation refuses when called.import { Buffer } from "./buffer";import { blockSizeOf, createDigest, digestLengthOf, normalizeAlgorithm, SUPPORTED_DIGESTS, type Digest } from "./digest";import { markRefused, refused, refusedClass, unsupported } from "./refuse";import { Transform, Writable, type TransformOptions, type WritableOptions } from "./stream";
type BinaryLike = string | ArrayBufferView | ArrayBuffer;
/** Node's ERR_INVALID_ARG_TYPE calls a dotted name (e.g. "options.safe") a property, * everything else an argument. */function argOrProperty(name: string): string { return name.includes(".") ? "property" : "argument";}
function invalidArgType(name: string, expected: string, value: unknown): TypeError { const got = value === null ? "null" : value === undefined ? "undefined" : `type ${typeof value}`; return Object.assign(new TypeError(`The "${name}" ${argOrProperty(name)} must be ${expected}. Received ${got}`), { code: "ERR_INVALID_ARG_TYPE" });}
/** Node's error messages echo the rejected value itself for a handful of the new * validators below (checkPrime's candidate, generateKey's type, KeyObject#equals' * argument): a string gets single-quoted, everything else prints as-is. */function quoteReceived(value: unknown): string { return typeof value === "string" ? `'${value}'` : String(value);}
function invalidArgTypeWithValue(name: string, expected: string, value: unknown): TypeError { const got = value === null ? "null" : value === undefined ? "undefined" : `type ${typeof value} (${quoteReceived(value)})`; return Object.assign(new TypeError(`The "${name}" ${argOrProperty(name)} must be ${expected}. Received ${got}`), { code: "ERR_INVALID_ARG_TYPE" });}
/** Node's validateInt32/validateUint32/validateInteger collapsed to one shape: all * three are "must be a number, must be an integer, must be within [min, max]", * differing only in the bounds the caller passes. */function validateNumberRange(value: unknown, name: string, min: number, max: number): number { if (typeof value !== "number") throw invalidArgTypeWithValue(name, "of type number", value); if (!Number.isInteger(value)) { throw Object.assign(new RangeError(`The value of "${name}" is out of range. It must be an integer. Received ${value}`), { code: "ERR_OUT_OF_RANGE" }); } if (value < min || value > max) { throw Object.assign(new RangeError(`The value of "${name}" is out of range. It must be >= ${min} && <= ${max}. Received ${value}`), { code: "ERR_OUT_OF_RANGE" }); } return value;}
function incompatibleOptionPair(a: string, b: string): Error { return Object.assign(new TypeError(`Option "${a}" cannot be used in combination with option "${b}"`), { code: "ERR_INCOMPATIBLE_OPTION_PAIR" });}
/** pbkdf2/scrypt's getArrayBufferOrView: string, ArrayBuffer, TypedArray, DataView, * or Buffer — in that message order (hkdf's own byte-source validator, below, orders * the same list differently, matching lib/internal/crypto/util.js's two call sites). */function pbkdf2BytesOrThrow(value: unknown, name: string): Uint8Array { if (typeof value === "string") return Buffer.from(value, "utf8"); if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); if (value instanceof ArrayBuffer) return new Uint8Array(value); throw invalidArgTypeWithValue(name, "of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView", value);}
/** hkdf's validateByteSource: same four shapes as above, different message order. */function hkdfBytesOrThrow(value: unknown, name: string): Uint8Array { if (typeof value === "string") return Buffer.from(value, "utf8"); if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); if (value instanceof ArrayBuffer) return new Uint8Array(value); throw invalidArgTypeWithValue(name, "of type string or an instance of ArrayBuffer, TypedArray, DataView, or Buffer", value);}
/** Every pbkdf2/scrypt/hkdf digest argument is validated for *type* up front * (Node's validateString), then only resolved to an actual algorithm at the very * end — after every other argument (including the callback) has been validated. * That ordering is why this is split from resolveKdfDigest below. */function validateDigestString(digest: unknown, name = "digest"): string { if (typeof digest !== "string") throw invalidArgTypeWithValue(name, "of type string", digest); return digest;}
/** Resolves a digest name the way pbkdf2Job/ScryptJob/HKDFJob do: anything OpenSSL * has never heard of is ERR_CRYPTO_INVALID_DIGEST (Node's own error); anything * OpenSSL has but this kernel's synchronous HMAC doesn't is ERR_NIB_UNSUPPORTED * (a kernel limitation, not a Node behavior to reproduce). */function resolveKdfDigest(digest: string): string { const normalized = normalizeAlgorithm(digest); if (normalized) return normalized; const key = digest.toLowerCase().replace(/^rsa-/, ""); if (NODE_ONLY_DIGESTS.has(key)) { throw Object.assign(new Error(`ERR_NIB_UNSUPPORTED: node:crypto digest '${digest}' is not supported (available: ${SUPPORTED_DIGESTS.join(", ")})`), { code: "ERR_NIB_UNSUPPORTED", }); } throw Object.assign(new TypeError(`Invalid digest: ${digest}`), { code: "ERR_CRYPTO_INVALID_DIGEST" });}
function toBytes(data: BinaryLike, encoding: string | undefined, name: string): Uint8Array { if (typeof data === "string") return Buffer.from(data, encoding ?? "utf8"); if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); if (data instanceof ArrayBuffer) return new Uint8Array(data); throw invalidArgType(name, "of type string or an instance of Buffer, TypedArray, or DataView", data);}
function encodeDigest(bytes: Uint8Array, encoding?: string): Buffer | string { const buf = Buffer.from(bytes); return encoding === undefined || encoding === "buffer" ? buf : buf.toString(encoding);}
function finalized(): Error { return Object.assign(new Error("Digest already called"), { code: "ERR_CRYPTO_HASH_FINALIZED" });}
/** Digests Node's OpenSSL offers that this runtime does not (yet). sha224/384 and * sha512-224/256 moved to digest.ts (truncated sha256/sha512, same compress * functions) and are gone from this list. */const NODE_ONLY_DIGESTS = new Set([ "sha3-224", "sha3-256", "sha3-384", "sha3-512", "shake128", "shake256", "ripemd160", "blake2b512", "blake2s256", "sm3", "md5-sha1",]);
function digestOrThrow(algorithm: string): Digest { if (typeof algorithm !== "string") throw invalidArgType("algorithm", "of type string", algorithm); const d = createDigest(algorithm); if (d) return d; if (NODE_ONLY_DIGESTS.has(algorithm.toLowerCase().replace(/^rsa-/, ""))) { throw Object.assign(new Error(`ERR_NIB_UNSUPPORTED: node:crypto digest '${algorithm}' is not supported (available: ${SUPPORTED_DIGESTS.join(", ")})`), { code: "ERR_NIB_UNSUPPORTED", }); } throw new Error("Digest method not supported");}
type TransformCallback = (error?: Error | null, data?: unknown) => void;
/** Node's Hash and Hmac are Transform streams: writing feeds update(), and ending the * writable side pushes the single digest chunk then ends the readable side. The * direct update()/digest() API (unaffected by whether the stream side is ever used) * stays the primary path; _transform/_flush just call through it. */export class Hash extends Transform { #digest: Digest; #done = false;
constructor(algorithm: string | Digest, options?: TransformOptions) { super(options); this.#digest = typeof algorithm === "string" ? digestOrThrow(algorithm) : algorithm; }
update(data: BinaryLike, inputEncoding?: string): this { if (this.#done) throw finalized(); this.#digest.update(toBytes(data, inputEncoding, "data")); return this; }
digest(encoding?: string): Buffer | string { if (this.#done) throw finalized(); this.#done = true; return encodeDigest(this.#digest.digest(), encoding); }
copy(options?: TransformOptions): Hash { if (this.#done) throw finalized(); return new Hash(this.#digest.copy(), options); }
override _transform(chunk: unknown, encoding: string, callback: TransformCallback): void { try { this.update(chunk as BinaryLike, encoding); callback(); } catch (err) { callback(err as Error); } }
override _flush(callback: TransformCallback): void { try { callback(null, this.digest()); } catch (err) { callback(err as Error); } }}
/** HMAC's key schedule (RFC 2104): shorten an over-long key by hashing it, then XOR * it into the inner/outer pads and prime a fresh digest with each. `algorithm` must * already be known-good (digestOrThrow or resolveKdfDigest having run). Shared by * the Hmac stream class below and by pbkdf2/hkdfSync, which need HMAC as a pure * function rather than a stream: both just copy() these primed digests per call * instead of redoing the key schedule (the only part that's actually expensive to * repeat) on every one of PBKDF2's thousands of iterations. */function hmacPads(algorithm: string, key: Uint8Array): { inner: Digest; outer: Digest } { const blockSize = blockSizeOf(algorithm); let k = key; if (k.length > blockSize) { const d = createDigest(algorithm)!; d.update(k); k = d.digest(); } const pad = (byte: number): Uint8Array => { const out = new Uint8Array(blockSize); for (let i = 0; i < blockSize; i++) out[i] = (k[i] ?? 0) ^ byte; return out; }; const inner = createDigest(algorithm)!; inner.update(pad(0x36)); const outer = createDigest(algorithm)!; outer.update(pad(0x5c)); return { inner, outer };}
/** One HMAC call against already-primed pads: clone both (so the pads can be reused * for the next call), feed `data` through the inner one, then the outer. */function hmacWithPads(pads: { inner: Digest; outer: Digest }, data: Uint8Array): Uint8Array { const inner = pads.inner.copy(); inner.update(data); const outer = pads.outer.copy(); outer.update(inner.digest()); return outer.digest();}
export class Hmac extends Transform { #inner: Digest; #outer: Digest; #done = false;
constructor(algorithm: string, key: BinaryLike, options?: TransformOptions) { super(options); digestOrThrow(algorithm); // validates before the key-shape check below if (key !== null && typeof key === "object" && !ArrayBuffer.isView(key) && !(key instanceof ArrayBuffer)) { throw Object.assign(new Error("ERR_NIB_UNSUPPORTED: node:crypto KeyObject keys are not supported"), { code: "ERR_NIB_UNSUPPORTED" }); } const { inner, outer } = hmacPads(algorithm, toBytes(key, "utf8", "key")); this.#inner = inner; this.#outer = outer; }
update(data: BinaryLike, inputEncoding?: string): this { if (this.#done) throw finalized(); this.#inner.update(toBytes(data, inputEncoding, "data")); return this; }
digest(encoding?: string): Buffer | string { if (this.#done) throw finalized(); this.#done = true; this.#outer.update(this.#inner.digest()); return encodeDigest(this.#outer.digest(), encoding); }
override _transform(chunk: unknown, encoding: string, callback: TransformCallback): void { try { this.update(chunk as BinaryLike, encoding); callback(); } catch (err) { callback(err as Error); } }
override _flush(callback: TransformCallback): void { try { callback(null, this.digest()); } catch (err) { callback(err as Error); } }}
// ---- pbkdf2: PBKDF2-HMAC (RFC 8018 / PKCS #5), built on hmacPads/hmacWithPads above ----
/** RFC 8018 section 5.2. `algorithm` must already be resolved (resolveKdfDigest). */function pbkdf2Derive(algorithm: string, password: Uint8Array, salt: Uint8Array, iterations: number, keylen: number): Uint8Array { const pads = hmacPads(algorithm, password); const hLen = digestLengthOf(algorithm); const out = new Uint8Array(keylen); const blocks = Math.ceil(keylen / hLen); // salt || INT(i), the big-endian 32-bit block index, reused across blocks. const saltBlock = new Uint8Array(salt.length + 4); saltBlock.set(salt); const indexView = new DataView(saltBlock.buffer); for (let i = 1; i <= blocks; i++) { indexView.setUint32(salt.length, i, false); let u = hmacWithPads(pads, saltBlock); const t = u.slice(); for (let j = 1; j < iterations; j++) { u = hmacWithPads(pads, u); for (let k = 0; k < t.length; k++) t[k] = t[k]! ^ u[k]!; } const offset = (i - 1) * hLen; out.set(t.subarray(0, Math.min(hLen, keylen - offset)), offset); } return out;}
/** Node's PBKDF2Job/HKDFJob derive a zero-length key by asking OpenSSL to do it, and * OpenSSL's PKCS5_PBKDF2_HMAC (unlike its scrypt path) simply fails on a zero-length * output: both throw this exact unnamed Error rather than succeeding with an empty * buffer. Confirmed under Node 24.16.0: `crypto.pbkdf2Sync("p","s",1,0,"sha256")` and * `crypto.hkdfSync("sha256","k","s","i",0)` both throw `Error: Deriving bits failed` * with no `code` at all — scrypt's own DeriveBits has an explicit early return for * length 0 that PBKDF2's and HKDF's don't, so only scrypt actually allows it. */function derivingBitsFailed(): Error { return new Error("Deriving bits failed");}
function pbkdf2Sync(password: unknown, salt: unknown, iterations: unknown, keylen: unknown, digest: unknown): Buffer { validateDigestString(digest); const passwordBytes = pbkdf2BytesOrThrow(password, "password"); const saltBytes = pbkdf2BytesOrThrow(salt, "salt"); validateNumberRange(iterations, "iterations", 1, 2147483647); validateNumberRange(keylen, "keylen", 0, 2147483647); const algorithm = resolveKdfDigest(digest as string); if ((keylen as number) === 0) throw derivingBitsFailed(); return Buffer.from(pbkdf2Derive(algorithm, passwordBytes, saltBytes, iterations as number, keylen as number));}
function pbkdf2(password: unknown, salt: unknown, iterations: unknown, keylen: unknown, digest: unknown, callback?: unknown): void { if (typeof digest === "function") { callback = digest; digest = undefined; } validateDigestString(digest); const passwordBytes = pbkdf2BytesOrThrow(password, "password"); const saltBytes = pbkdf2BytesOrThrow(salt, "salt"); validateNumberRange(iterations, "iterations", 1, 2147483647); validateNumberRange(keylen, "keylen", 0, 2147483647); if (typeof callback !== "function") throw invalidArgTypeWithValue("callback", "of type function", callback); // Node resolves the digest name (and hence throws ERR_CRYPTO_INVALID_DIGEST) at job // creation, which happens synchronously before job.run() schedules the real work — // confirmed by `crypto.pbkdf2(..., "bogus", cb)` throwing synchronously, not via cb. const algorithm = resolveKdfDigest(digest as string); const cb = callback as (err: Error | null, key?: Buffer) => void; const kl = keylen as number; queueMicrotask(() => { try { if (kl === 0) throw derivingBitsFailed(); cb(null, Buffer.from(pbkdf2Derive(algorithm, passwordBytes, saltBytes, iterations as number, kl))); } catch (err) { cb(err as Error); } });}
/** Node's Cipheriv/Decipheriv are Transform streams whose update() does the actual * block-cipher work; that needs a real cipher implementation (AES, ChaCha20...), * which is a genuine WASM-residue gap, not something WebCrypto's async-only * subtle.encrypt/decrypt can stand in for here. The class exists with the right * base (so instanceof and the stream wiring match Node) and refuses on construction. */export class Cipheriv extends Transform { constructor(..._args: unknown[]) { super(); throw unsupported("crypto", "Cipheriv", WHY_CIPHER); }}
export class Decipheriv extends Transform { constructor(..._args: unknown[]) { super(); throw unsupported("crypto", "Decipheriv", WHY_CIPHER); }}
/** Node's Sign/Verify are Writable streams; the actual signature math needs an * asymmetric key (RSA/EC/Ed25519) imported from arbitrary PEM/DER/JWK input, which * is real cryptographic work this runtime doesn't do. See WHY_ASYMMETRIC. */export class Sign extends Writable { constructor(_algorithm?: string, _options?: WritableOptions) { super(); throw unsupported("crypto", "Sign", WHY_ASYMMETRIC); }}
export class Verify extends Writable { constructor(_algorithm?: string, _options?: WritableOptions) { super(); throw unsupported("crypto", "Verify", WHY_ASYMMETRIC); }}
/** A secret-only KeyObject: the kind generateKey/generateKeySync actually produce * (plain random bytes). Asymmetric KeyObjects (from createPublicKey et al.) would * need DER/PEM parsing and real key material, so the public constructor refuses — * only generateKey's internal factory below can make one. */const SECRET_KEY = Symbol("secret key");
export class KeyObject { #bytes: Uint8Array;
constructor(token?: symbol, bytes?: Uint8Array) { if (token !== SECRET_KEY || !bytes) { throw unsupported("crypto", "KeyObject", "only secret keys made by generateKey/generateKeySync are supported"); } this.#bytes = bytes; }
get type(): string { return "secret"; }
get symmetricKeySize(): number { return this.#bytes.length; }
export(): Buffer { return Buffer.from(this.#bytes); }
equals(other: unknown): boolean { if (!(other instanceof KeyObject)) { throw invalidArgTypeWithValue("otherKeyObject", "an instance of KeyObject", other); } return timingSafeEqualBytes(this.#bytes, other.#bytes); }
static from(..._args: unknown[]): KeyObject { throw unsupported("crypto", "KeyObject.from", "only secret keys made by generateKey/generateKeySync are supported"); }}
function makeSecretKeyObject(bytes: Uint8Array): KeyObject { return new KeyObject(SECRET_KEY, bytes);}
/** hkdf/hkdfSync's `ikm`: Node's prepareKey (lib/internal/crypto/hkdf.js) accepts a * SecretKeyObject as-is, wraps an ArrayBuffer/string/view into one via * createSecretKey, and otherwise lists exactly these shapes in its error. */function hkdfKeyBytes(key: unknown): Uint8Array { if (key instanceof KeyObject) return key.export(); if (typeof key === "string") return Buffer.from(key, "utf8"); if (ArrayBuffer.isView(key)) return new Uint8Array(key.buffer, key.byteOffset, key.byteLength); if (key instanceof ArrayBuffer) return new Uint8Array(key); throw invalidArgTypeWithValue("ikm", "of type string or an instance of SecretKeyObject, ArrayBuffer, TypedArray, DataView, or Buffer", key);}
const MAX_RANDOM_CHUNK = 65536; // getRandomValues' per-call limitconst MAX_POSSIBLE_LENGTH = 2 ** 31 - 1; // min(buffer.kMaxLength, 2**31-1): the latter always binds here
/** Node adds a numeric separator to large numbers inside ERR_OUT_OF_RANGE's "Received" text. */function addNumericalSeparator(text: string): string { let res = ""; let i = text.length; const start = text[0] === "-" ? 1 : 0; for (; i >= start + 4; i -= 3) res = `_${text.slice(i - 3, i)}${res}`; return `${text.slice(0, i)}${res}`;}
function outOfRange(name: string, range: string, received: number | bigint): RangeError { const shown = typeof received === "bigint" ? `${received > 2n ** 32n || received < -(2n ** 32n) ? addNumericalSeparator(String(received)) : String(received)}n` : Number.isNaN(received) ? "NaN" : Number.isInteger(received) && Math.abs(received) > 2 ** 32 ? addNumericalSeparator(String(received)) : String(received); return Object.assign(new RangeError(`The value of "${name}" is out of range. It must be ${range}. Received ${shown}`), { code: "ERR_OUT_OF_RANGE" });}
function isAnyArrayBufferOrView(value: unknown): value is ArrayBufferView | ArrayBuffer { return value instanceof ArrayBuffer || ArrayBuffer.isView(value);}
function elementSizeOf(buf: ArrayBufferView | ArrayBuffer): number { return (buf as unknown as { BYTES_PER_ELEMENT?: number }).BYTES_PER_ELEMENT || 1;}
function viewBytes(buf: ArrayBufferView | ArrayBuffer): Uint8Array { return buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);}
/** Node's lib/internal/crypto/random.js assertOffset: offset is in elements, converted * to bytes via elementSize, then bounds-checked against the buffer's byte length. */function assertOffset(offset: unknown, elementSize: number, length: number): number { if (typeof offset !== "number") throw invalidArgTypeWithValue("offset", "of type number", offset); const off = offset * elementSize; const maxLength = Math.min(length, MAX_POSSIBLE_LENGTH); if (Number.isNaN(off) || off > maxLength || off < 0) throw outOfRange("offset", `>= 0 && <= ${maxLength}`, off); return off >>> 0;}
/** Node's assertSize: same element->byte conversion, plus the size+offset bound. */function assertSize(size: unknown, elementSize: number, offset: number, length: number): number { if (typeof size !== "number") throw invalidArgTypeWithValue("size", "of type number", size); const sz = size * elementSize; if (Number.isNaN(sz) || sz > MAX_POSSIBLE_LENGTH || sz < 0) throw outOfRange("size", `>= 0 && <= ${MAX_POSSIBLE_LENGTH}`, sz); if (sz + offset > length) throw outOfRange("size + offset", `<= ${length}`, sz + offset); return sz >>> 0;}
function fillRandomBytes(bytes: Uint8Array, off: number, sz: number): void { for (let at = off; at < off + sz; at += MAX_RANDOM_CHUNK) { globalThis.crypto.getRandomValues(bytes.subarray(at, Math.min(off + sz, at + MAX_RANDOM_CHUNK))); }}
function randomFillSync<T extends ArrayBufferView | ArrayBuffer>(buf: T, offset: unknown = 0, size?: unknown): T { if (!isAnyArrayBufferOrView(buf)) throw invalidArgTypeWithValue("buf", "an instance of ArrayBuffer or ArrayBufferView", buf); const elementSize = elementSizeOf(buf); const off = assertOffset(offset, elementSize, buf.byteLength); const sz = size === undefined ? buf.byteLength - off : assertSize(size, elementSize, off, buf.byteLength); if (sz === 0) return buf; fillRandomBytes(viewBytes(buf), off, sz); return buf;}
/** Node's randomFill: same validation as randomFillSync (shared via assertOffset/ * assertSize), plus the (offset|size) overload detection and an async callback. * The underlying RNG is synchronous here (getRandomValues), so unlike Node's * thread-pool job, the fill itself runs synchronously too; only the callback * is deferred, which is enough to make this a real async API from the caller's * point of view (and keeps error throwing synchronous, as Node's own does). */function randomFill( buf: unknown, offset?: unknown, size?: unknown, callback?: (err: Error | null, buf?: unknown) => void,): void { if (!isAnyArrayBufferOrView(buf)) throw invalidArgTypeWithValue("buf", "an instance of ArrayBuffer or ArrayBufferView", buf); const elementSize = elementSizeOf(buf); if (typeof offset === "function") { callback = offset as typeof callback; offset = 0; size = (buf as { length?: number }).length; // undefined for ArrayBuffer/DataView; resolved below } else if (typeof size === "function") { callback = size as typeof callback; size = (buf as { length?: number }).length! - (offset as number); // NaN for ArrayBuffer/DataView, as Node's own does } else if (typeof callback !== "function") { throw invalidArgTypeWithValue("callback", "of type function", callback); } const off = assertOffset(offset, elementSize, buf.byteLength); const sz = size === undefined ? buf.byteLength - off : assertSize(size, elementSize, off, buf.byteLength); const cb = callback!; // one of the three branches above always sets a function, or throws if (sz === 0) { queueMicrotask(() => cb(null, buf)); return; } queueMicrotask(() => { fillRandomBytes(viewBytes(buf as ArrayBufferView | ArrayBuffer), off, sz); cb(null, buf); });}
function randomBytes(size: number, cb?: (err: Error | null, buf?: Buffer) => void): Buffer | undefined { if (typeof size !== "number" || !Number.isInteger(size) || size < 0 || size > 2 ** 31 - 1) { throw Object.assign(new RangeError(`The value of "size" is out of range. It must be >= 0 && <= 2147483647. Received ${String(size)}`), { code: "ERR_OUT_OF_RANGE", }); } const buf = randomFillSync(Buffer.alloc(size)); if (cb === undefined) return buf; queueMicrotask(() => cb(null, buf)); return undefined;}
// Largest integer readable from 6 random bytes, same width Node's randomInt uses.const RAND_MAX = 0xFFFF_FFFF_FFFF;
/** Rejection sampling over 6 random bytes at a time, same approach as Node's * randomInt: draw x uniformly from [0, RAND_MAX], reject the biased tail above * the largest multiple of `range` that still fits, otherwise (x % range) + min * is uniform. Node caches the random bytes across calls (its RNG call is a * thread-pool job); getRandomValues is synchronous here, so a fresh 6-byte draw * each attempt is just as fast and needs no shared cache. */function randomIntInRange(min: number, max: number): number { const range = max - min; const randLimit = RAND_MAX - (RAND_MAX % range); const buf = Buffer.alloc(6); for (;;) { randomFillSync(buf); let x = 0; for (let i = 0; i < 6; i++) x = x * 256 + buf[i]!; if (x < randLimit) return (x % range) + min; }}
/** crypto.randomInt(min, max, callback) / randomInt(max[, callback]), sync when no * callback is given. Validation ports Node's lib/internal/crypto/random.js exactly * (safe-integer min/max, range < 2**48); generation differs only in not sharing a * cache across calls (see randomIntInRange), which isn't observable here since the * underlying RNG is synchronous rather than a thread-pool job. */function randomInt(min: unknown, max?: unknown, callback?: (err?: Error, n?: number) => void): number | undefined { const minNotSpecified = max === undefined || typeof max === "function"; if (minNotSpecified) { callback = max as typeof callback; max = min; min = 0; } const isSync = callback === undefined; if (!isSync && typeof callback !== "function") throw invalidArgTypeWithValue("callback", "of type function", callback); if (typeof min !== "number" || !Number.isSafeInteger(min)) throw invalidArgTypeWithValue("min", "a safe integer", min); if (typeof max !== "number" || !Number.isSafeInteger(max)) throw invalidArgTypeWithValue("max", "a safe integer", max); if (max <= min) { throw Object.assign(new RangeError(`The value of "max" is out of range. It must be greater than the value of "min" (${min}). Received ${max}`), { code: "ERR_OUT_OF_RANGE", }); } const range = max - min; if (!(range <= RAND_MAX)) { throw outOfRange(minNotSpecified ? "max" : "max - min", `<= ${RAND_MAX}`, range); } const n = randomIntInRange(min, max); if (isSync) return n; queueMicrotask(() => callback!(undefined, n)); return undefined;}
/** The actual byte comparison; timingSafeEqual requires equal length, KeyObject#equals * treats a length mismatch as simply unequal. */function timingSafeEqualBytes(x: Uint8Array, y: Uint8Array): boolean { if (x.length !== y.length) return false; let diff = 0; for (let i = 0; i < x.length; i++) diff |= x[i]! ^ y[i]!; return diff === 0;}
function timingSafeEqual(a: ArrayBufferView, b: ArrayBufferView): boolean { if (!ArrayBuffer.isView(a)) throw invalidArgType("buf1", "an instance of ArrayBuffer, Buffer, TypedArray, or DataView", a); if (!ArrayBuffer.isView(b)) throw invalidArgType("buf2", "an instance of ArrayBuffer, Buffer, TypedArray, or DataView", b); if (a.byteLength !== b.byteLength) { throw Object.assign(new RangeError("Input buffers must have the same byte length"), { code: "ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH" }); } const x = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); const y = new Uint8Array(b.buffer, b.byteOffset, b.byteLength); return timingSafeEqualBytes(x, y);}
// ---- small, pure helpers (no residue needed) ----
/** crypto.randomUUIDv7: a UUIDv7 is just 48 bits of Unix-ms timestamp followed by * random bits, with the version/variant nibbles forced. No digest or signature is * involved, so this needs nothing beyond the RNG already used for randomUUID. */function randomUUIDv7(): string { const bytes = new Uint8Array(16); globalThis.crypto.getRandomValues(bytes); let ms = Date.now(); for (let i = 5; i >= 0; i--) { bytes[i] = ms & 0xff; ms = Math.floor(ms / 256); } bytes[6] = (bytes[6]! & 0x0f) | 0x70; // version 7 bytes[8] = (bytes[8]! & 0x3f) | 0x80; // variant 10 const hex = Buffer.from(bytes).toString("hex"); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;}
function getFips(): number { return 0; // this runtime never runs a FIPS-validated module}
function setFips(enable: unknown): void { // Off is the only state there is; asking for FIPS mode cannot be honoured. if (enable) throw unsupported("crypto", "setFips(true)", "no FIPS-validated provider exists in a page");}
function secureHeapUsed(): { total: number; used: number; utilization: number; min: number } { // No secure heap exists in a page; Node's own shape when none was configured. return { total: 0, used: 0, utilization: NaN, min: 2 };}
// ---- checkPrime: BigInt Miller-Rabin, no residue needed ----
function toCheckPrimeCandidate(candidate: unknown): bigint { if (typeof candidate === "bigint") { if (candidate < 0n) { throw Object.assign(new RangeError(`The value of "candidate" is out of range. It must be >= 0. Received ${candidate}n`), { code: "ERR_OUT_OF_RANGE" }); } return candidate; } if (ArrayBuffer.isView(candidate) || candidate instanceof ArrayBuffer) { const bytes = toBytes(candidate as BinaryLike, undefined, "candidate"); let n = 0n; for (const b of bytes) n = (n << 8n) | BigInt(b); return n; } throw invalidArgTypeWithValue("candidate", "of type bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView", candidate);}
function modPow(base: bigint, exponent: bigint, modulus: bigint): bigint { let result = 1n; let b = base % modulus; let e = exponent; while (e > 0n) { if (e & 1n) result = (result * b) % modulus; e >>= 1n; b = (b * b) % modulus; } return result;}
/** Deterministic Miller-Rabin over the first 12 primes as witnesses: exact for every * candidate below ~3.3e24 (far past what checkPrime is realistically used for here), * and still a sound (if only probabilistic) primality test above that, same as any * Miller-Rabin implementation with a fixed witness set. */const MILLER_RABIN_WITNESSES = [2n, 3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n, 37n];
function isProbablePrime(n: bigint): boolean { if (n < 2n) return false; for (const p of MILLER_RABIN_WITNESSES) { if (n === p) return true; if (n % p === 0n) return false; } let d = n - 1n; let r = 0n; while (d % 2n === 0n) { d /= 2n; r++; } for (const a of MILLER_RABIN_WITNESSES) { if (a >= n) continue; let x = modPow(a, d, n); if (x === 1n || x === n - 1n) continue; let composite = true; for (let i = 1n; i < r; i++) { x = (x * x) % n; if (x === n - 1n) { composite = false; break; } } if (composite) return false; } return true;}
function checkPrimeSync(candidate: unknown, _options?: { checks?: number }): boolean { return isProbablePrime(toCheckPrimeCandidate(candidate));}
function checkPrime(candidate: unknown, options: unknown, callback?: (err?: Error, result?: boolean) => void): void { if (typeof options === "function") { callback = options as typeof callback; options = undefined; } if (typeof callback !== "function") throw invalidArgType("callback", "of type function", callback); const n = toCheckPrimeCandidate(candidate); // validation throws synchronously, as Node's does // Node's own callback fires with no error argument at all on success (so it reads as // undefined, not null) — matching that, unlike generateKey's callback(null, ...). queueMicrotask(() => callback!(undefined, isProbablePrime(n)));}
// ---- scrypt: salsa20/8 + BlockMix + ROMix over PBKDF2-HMAC-SHA256 (RFC 7914) ----
const SCRYPT_DEFAULTS = { N: 16384, r: 8, p: 1, maxmem: 32 * 1024 * 1024 };
/** Salsa20/8 core (RFC 7914 section 3): 4 double-rounds over 16 32-bit words, * in place. `x` is scratch reused across calls — scrypt never re-enters this * (there's no concurrency inside one derivation), so a module-scope buffer avoids * reallocating it per block, which matters at N=16384 (thousands of calls/lane). */const rotl = (x: number, n: number): number => (x << n) | (x >>> (32 - n));
/** Plain locals rather than array slots: faster (no bounds-checked indexing in the * hot loop) and sidesteps noUncheckedIndexedAccess entirely for a fixed 16-word * state, which array indexing here would otherwise require `!`-asserting on every * one of the ~2,600 reads this does at N=16384, r=8. */function salsa20_8(B: Int32Array): void { let x0 = B[0]!, x1 = B[1]!, x2 = B[2]!, x3 = B[3]!; let x4 = B[4]!, x5 = B[5]!, x6 = B[6]!, x7 = B[7]!; let x8 = B[8]!, x9 = B[9]!, x10 = B[10]!, x11 = B[11]!; let x12 = B[12]!, x13 = B[13]!, x14 = B[14]!, x15 = B[15]!; const b0 = x0, b1 = x1, b2 = x2, b3 = x3, b4 = x4, b5 = x5, b6 = x6, b7 = x7; const b8 = x8, b9 = x9, b10 = x10, b11 = x11, b12 = x12, b13 = x13, b14 = x14, b15 = x15;
for (let i = 0; i < 8; i += 2) { x4 ^= rotl(x0 + x12, 7); x8 ^= rotl(x4 + x0, 9); x12 ^= rotl(x8 + x4, 13); x0 ^= rotl(x12 + x8, 18); x9 ^= rotl(x5 + x1, 7); x13 ^= rotl(x9 + x5, 9); x1 ^= rotl(x13 + x9, 13); x5 ^= rotl(x1 + x13, 18); x14 ^= rotl(x10 + x6, 7); x2 ^= rotl(x14 + x10, 9); x6 ^= rotl(x2 + x14, 13); x10 ^= rotl(x6 + x2, 18); x3 ^= rotl(x15 + x11, 7); x7 ^= rotl(x3 + x15, 9); x11 ^= rotl(x7 + x3, 13); x15 ^= rotl(x11 + x7, 18);
x1 ^= rotl(x0 + x3, 7); x2 ^= rotl(x1 + x0, 9); x3 ^= rotl(x2 + x1, 13); x0 ^= rotl(x3 + x2, 18); x6 ^= rotl(x5 + x4, 7); x7 ^= rotl(x6 + x5, 9); x4 ^= rotl(x7 + x6, 13); x5 ^= rotl(x4 + x7, 18); x11 ^= rotl(x10 + x9, 7); x8 ^= rotl(x11 + x10, 9); x9 ^= rotl(x8 + x11, 13); x10 ^= rotl(x9 + x8, 18); x12 ^= rotl(x15 + x14, 7); x13 ^= rotl(x12 + x15, 9); x14 ^= rotl(x13 + x12, 13); x15 ^= rotl(x14 + x13, 18); }
B[0] = (b0 + x0) | 0; B[1] = (b1 + x1) | 0; B[2] = (b2 + x2) | 0; B[3] = (b3 + x3) | 0; B[4] = (b4 + x4) | 0; B[5] = (b5 + x5) | 0; B[6] = (b6 + x6) | 0; B[7] = (b7 + x7) | 0; B[8] = (b8 + x8) | 0; B[9] = (b9 + x9) | 0; B[10] = (b10 + x10) | 0; B[11] = (b11 + x11) | 0; B[12] = (b12 + x12) | 0; B[13] = (b13 + x13) | 0; B[14] = (b14 + x14) | 0; B[15] = (b15 + x15) | 0;}
const blockMixT = new Int32Array(16);
/** RFC 7914 section 4: B is 2r 64-byte (16-word) blocks; Y is the same size. */function blockMix(B: Int32Array, r: number, Y: Int32Array): void { const X = B.subarray((2 * r - 1) * 16, 2 * r * 16).slice(); for (let i = 0; i < 2 * r; i++) { for (let j = 0; j < 16; j++) blockMixT[j] = X[j]! ^ B[i * 16 + j]!; salsa20_8(blockMixT); X.set(blockMixT); const dest = i % 2 === 0 ? i / 2 : r + (i - 1) / 2; Y.set(X, dest * 16); }}
/** RFC 7914 section 5: N calls to BlockMix building up V, then N more mixing a * random (mod N) earlier state back in — scrypt's actual memory-hardness. N is * required to be a power of two (checked by scryptParams below), which is what * lets Integerify's "mod N" collapse to just the low bits of the block's first * word (little-endian, so those are its least-significant bits) — no BigInt or * multi-word modulo needed even though the integer being reduced is 128r bytes * wide. `B` is mutated in place. */function romix(B: Int32Array, r: number, N: number): void { const words = 32 * r; const X = B.slice(); const T = new Int32Array(words); const V = new Int32Array(words * N); for (let i = 0; i < N; i++) { V.set(X, i * words); blockMix(X, r, T); X.set(T); } const mask = N - 1; for (let i = 0; i < N; i++) { const j = (X[(2 * r - 1) * 16]! >>> 0) & mask; for (let k = 0; k < words; k++) X[k] = X[k]! ^ V[j * words + k]!; blockMix(X, r, T); X.set(T); } B.set(X);}
function scryptCompute(password: Uint8Array, salt: Uint8Array, keylen: number, N: number, r: number, p: number): Uint8Array { const blockWords = 32 * r; const bBytes = pbkdf2Derive("sha256", password, salt, 1, p * 128 * r); const B = new Int32Array(bBytes.buffer, bBytes.byteOffset, bBytes.byteLength / 4); for (let i = 0; i < p; i++) romix(B.subarray(i * blockWords, (i + 1) * blockWords), r, N); return pbkdf2Derive("sha256", password, new Uint8Array(B.buffer, B.byteOffset, B.byteLength), 1, keylen);}
/** N must be a power of two greater than 1 (required by romix's Integerify shortcut, * and by OpenSSL's EVP_PBE_scrypt, which is why Node's real error for a non-power- * of-two N is also ERR_CRYPTO_INVALID_SCRYPT_PARAMS — confirmed under Node 24.16.0: * `scryptSync(..., {N: 100, r: 8, p: 1, maxmem: 2**30})` throws it even with maxmem * raised well out of the way). */function isPowerOfTwoAboveOne(n: number): boolean { if (n <= 1) return false; const big = BigInt(n); return (big & (big - 1n)) === 0n;}
/** ScryptTraits::AdditionalConfig reports whatever OpenSSL last left on its global * error queue (via ERR_peek_last_error) appended to the message, or the bare * message if that queue happens to be empty. That queue is process-wide and never * cleared by a peek, so Node's exact message for the very same invalid params can * differ depending on what *earlier, unrelated* crypto calls in the process last * failed with — confirmed under Node 24.16.0 by running two invalid scryptSync * calls back to back and seeing the second one's message pick up the first one's * leftover OpenSSL error text. That's not a message this kernel can or should try * to reproduce; `code` (ERR_CRYPTO_INVALID_SCRYPT_PARAMS) is what's stable. */function invalidScryptParams(): Error { return Object.assign(new RangeError("Invalid scrypt params"), { code: "ERR_CRYPTO_INVALID_SCRYPT_PARAMS" });}
interface ScryptOptions { password: Uint8Array; salt: Uint8Array; keylen: number; N: number; r: number; p: number; maxmem: number;}
/** lib/internal/crypto/scrypt.js's check(): password/salt/keylen, then N/r/p with * their cost/blockSize/parallelization aliases (ERR_INCOMPATIBLE_OPTION_PAIR if both * of a pair are set) and maxmem — all validateUint32/validateInteger range checks. * Node reads straight off `options.N` etc. with no typeof options check at all, so * a non-object `options` (confirmed: a number, even) is silently equivalent to {}. */function scryptParseOptions(password: unknown, salt: unknown, keylen: unknown, options: unknown): ScryptOptions { const passwordBytes = pbkdf2BytesOrThrow(password, "password"); const saltBytes = pbkdf2BytesOrThrow(salt, "salt"); validateNumberRange(keylen, "keylen", 0, 2147483647);
let { N, r, p, maxmem } = SCRYPT_DEFAULTS; if (options !== undefined && options !== null) { const opts = options as Record<string, unknown>; const hasN = opts.N !== undefined; if (hasN) N = validateNumberRange(opts.N, "N", 0, 4294967295); if (opts.cost !== undefined) { if (hasN) throw incompatibleOptionPair("N", "cost"); N = validateNumberRange(opts.cost, "cost", 0, 4294967295); } const hasR = opts.r !== undefined; if (hasR) r = validateNumberRange(opts.r, "r", 0, 4294967295); if (opts.blockSize !== undefined) { if (hasR) throw incompatibleOptionPair("r", "blockSize"); r = validateNumberRange(opts.blockSize, "blockSize", 0, 4294967295); } const hasP = opts.p !== undefined; if (hasP) p = validateNumberRange(opts.p, "p", 0, 4294967295); if (opts.parallelization !== undefined) { if (hasP) throw incompatibleOptionPair("p", "parallelization"); p = validateNumberRange(opts.parallelization, "parallelization", 0, 4294967295); } if (opts.maxmem !== undefined) maxmem = validateNumberRange(opts.maxmem, "maxmem", 0, Number.MAX_SAFE_INTEGER); if (N === 0) N = SCRYPT_DEFAULTS.N; if (r === 0) r = SCRYPT_DEFAULTS.r; if (p === 0) p = SCRYPT_DEFAULTS.p; if (maxmem === 0) maxmem = SCRYPT_DEFAULTS.maxmem; } return { password: passwordBytes, salt: saltBytes, keylen: keylen as number, N, r, p, maxmem };}
/** The job-creation-time checks (ScryptTraits::AdditionalConfig / ncrypto's * checkScryptParams): these run even when keylen is 0, and even for the async form * they run synchronously before any callback is involved — confirmed under Node * 24.16.0 by `crypto.scrypt("p","s",16,{N:3}, badCallback)` throwing synchronously * with ERR_INCOMPATIBLE_OPTION_PAIR/ERR_INVALID_ARG_TYPE precedence matching the * order used here (validateFunction(callback) happens between parse and this). */function scryptValidateParams(N: number, r: number, maxmem: number): void { if (!isPowerOfTwoAboveOne(N)) throw invalidScryptParams(); if (128 * N * r > maxmem) throw invalidScryptParams();}
function scryptSync(password: unknown, salt: unknown, keylen: unknown, options?: unknown): Buffer { const opts = scryptParseOptions(password, salt, keylen, options); scryptValidateParams(opts.N, opts.r, opts.maxmem); if (opts.keylen === 0) return Buffer.alloc(0); return Buffer.from(scryptCompute(opts.password, opts.salt, opts.keylen, opts.N, opts.r, opts.p));}
function scrypt(password: unknown, salt: unknown, keylen: unknown, options: unknown, callback?: unknown): void { if (typeof options === "function") { callback = options; options = undefined; } const opts = scryptParseOptions(password, salt, keylen, options); if (typeof callback !== "function") throw invalidArgTypeWithValue("callback", "of type function", callback); scryptValidateParams(opts.N, opts.r, opts.maxmem); const cb = callback as (err: Error | null, key?: Buffer) => void; queueMicrotask(() => { try { const out = opts.keylen === 0 ? Buffer.alloc(0) : Buffer.from(scryptCompute(opts.password, opts.salt, opts.keylen, opts.N, opts.r, opts.p)); cb(null, out); } catch (err) { cb(err as Error); } });}
// ---- generatePrime/generatePrimeSync: random candidates over the same BigInt// Miller-Rabin as checkPrime, searched by trial (stepping by 2, or by `add` when// a congruence is required) rather than OpenSSL's BN_generate_prime_ex. The// congruence math (add/rem/safe) and validation/errors match Node's// lib/internal/crypto/random.js and its native AdditionalConfig checks; only the// search strategy is this runtime's own. ----
function bitLength(n: bigint): number { return n === 0n ? 0 : n.toString(2).length;}
/** Node's unsignedBigIntToBuffer / the reverse direction used by add/rem: a bigint * or a buffer-like both resolve to the same unsigned bigint for the congruence math. */function toCongruenceValue(value: unknown, name: string): bigint { if (typeof value === "bigint") { if (value < 0n) throw outOfRange(name, ">= 0", value); return value; } if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) { const bytes = toBytes(value as BinaryLike, undefined, name); let n = 0n; for (const b of bytes) n = (n << 8n) | BigInt(b); return n; } throw invalidArgTypeWithValue(name, "of type bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView", value);}
function validateOptionsObject(value: unknown, name: string): Record<string, unknown> { if (Array.isArray(value)) { throw Object.assign(new TypeError(`The "${name}" argument must be of type object. Received an instance of Array`), { code: "ERR_INVALID_ARG_TYPE" }); } if (value === null || typeof value !== "object") throw invalidArgTypeWithValue(name, "of type object", value); return value as Record<string, unknown>;}
function validateSizeInt32(size: unknown, name: string, min: number): number { if (typeof size !== "number") throw invalidArgTypeWithValue(name, "of type number", size); if (!Number.isInteger(size)) throw outOfRange(name, "an integer", size); if (size < min || size > 2 ** 31 - 1) throw outOfRange(name, `>= ${min} && <= ${2 ** 31 - 1}`, size); return size;}
interface PrimeOptions { safe: boolean; bigint: boolean; add?: bigint; rem?: bigint }
function parsePrimeOptions(bits: number, options: unknown): PrimeOptions { const o = validateOptionsObject(options, "options"); const safe = o.safe ?? false; const bigint = o.bigint ?? false; if (typeof safe !== "boolean") throw invalidArgTypeWithValue("options.safe", "of type boolean", safe); if (typeof bigint !== "boolean") throw invalidArgTypeWithValue("options.bigint", "of type boolean", bigint); const add = o.add !== undefined ? toCongruenceValue(o.add, "options.add") : undefined; const rem = o.rem !== undefined ? toCongruenceValue(o.rem, "options.rem") : undefined; if (add !== undefined) { if (bitLength(add) > bits) throw Object.assign(new RangeError("invalid options.add"), { code: "ERR_OUT_OF_RANGE" }); if (rem !== undefined && add <= rem) throw Object.assign(new RangeError("invalid options.rem"), { code: "ERR_OUT_OF_RANGE" }); } return { safe, bigint, add, rem };}
/** A `bits`-bit uniformly random odd candidate: top bit forced to 1 (so it has * exactly `bits` bits, like OpenSSL's BN_generate_prime_ex), bottom bit forced * to 1 (odd). */function randomOddCandidate(bits: number): bigint { const byteLen = Math.ceil(bits / 8); const extraBits = byteLen * 8 - bits; const bytes = randomFillSync(Buffer.alloc(byteLen)); bytes[0] = (bytes[0]! & (0xff >> extraBits)) | (1 << (7 - extraBits)); let n = 0n; for (const b of bytes) n = (n << 8n) | BigInt(b); return n | 1n;}
/** Searches for a prime of exactly `bits` bits satisfying the add/rem/safe * congruence (see parsePrimeOptions' doc comment on the table of cases), by * trial division/Miller-Rabin over isProbablePrime, same as checkPrime uses. * Candidates are walked by `add` (or by 2, to stay odd, when there's no `add`) * from a random starting point, which is dense enough that this stays fast up * through at least the 512-bit sizes this runtime is expected to generate. */function searchPrime(bits: number, safe: boolean, add?: bigint, rem?: bigint): bigint { const step = add ?? 2n; const residue = add !== undefined ? (((rem ?? (safe ? 3n : 1n)) % add) + add) % add : undefined; const low = 1n << BigInt(bits - 1); const high = (1n << BigInt(bits)) - 1n; for (;;) { let candidate = randomOddCandidate(bits); if (residue !== undefined) candidate += ((residue - (candidate % add!)) % add! + add!) % add!; while (candidate <= high) { if (candidate >= low) { if (isProbablePrime(candidate) && (!safe || isProbablePrime((candidate - 1n) / 2n))) return candidate; } candidate += step; } // Walked off the top of the bit range; draw a fresh random start and retry. }}
function primeToArrayBuffer(n: bigint, byteLen: number): ArrayBuffer { const bytes = new Uint8Array(byteLen); let v = n; for (let i = byteLen - 1; i >= 0; i--) { bytes[i] = Number(v & 0xffn); v >>= 8n; } return bytes.buffer;}
function encodePrime(prime: bigint, bits: number, bigint: boolean): ArrayBuffer | bigint { return bigint ? prime : primeToArrayBuffer(prime, Math.ceil(bits / 8));}
function generatePrimeSync(size: unknown, options: unknown = {}): ArrayBuffer | bigint { const bits = validateSizeInt32(size, "size", 1); const { safe, bigint, add, rem } = parsePrimeOptions(bits, options); return encodePrime(searchPrime(bits, safe, add, rem), bits, bigint);}
function generatePrime(size: unknown, options: unknown, callback?: (err?: Error, prime?: ArrayBuffer | bigint) => void): void { if (typeof options === "function") { callback = options as typeof callback; options = {}; } if (typeof callback !== "function") throw invalidArgTypeWithValue("callback", "of type function", callback); const bits = validateSizeInt32(size, "size", 1); // sync validation, as Node's does const { safe, bigint, add, rem } = parsePrimeOptions(bits, options); queueMicrotask(() => callback!(undefined, encodePrime(searchPrime(bits, safe, add, rem), bits, bigint)));}
// ---- hkdf: real HKDF (RFC 5869) over WebCrypto's own deriveBits ----
const HKDF_HASHES: Record<string, { name: string; bytes: number }> = { sha1: { name: "SHA-1", bytes: 20 }, sha256: { name: "SHA-256", bytes: 32 }, sha384: { name: "SHA-384", bytes: 48 }, sha512: { name: "SHA-512", bytes: 64 },};
function hkdf( digest: string, ikm: BinaryLike, salt: BinaryLike, info: BinaryLike, keylen: number, callback: (err: Error | null, derivedKey?: ArrayBuffer) => void,): void { if (typeof callback !== "function") throw invalidArgType("callback", "of type function", callback); if (typeof digest !== "string") throw invalidArgType("digest", "of type string", digest); const hash = HKDF_HASHES[digest.toLowerCase()]; if (!hash) throw Object.assign(new TypeError(`Invalid digest: ${digest}`), { code: "ERR_CRYPTO_INVALID_DIGEST" }); if (typeof keylen !== "number" || !Number.isInteger(keylen) || keylen < 0 || keylen > 255 * hash.bytes) { throw Object.assign(new RangeError("Invalid key length"), { code: "ERR_CRYPTO_INVALID_KEYLEN" }); } const ikmBytes = toBytes(ikm, "utf8", "ikm"); const saltBytes = toBytes(salt, "utf8", "salt"); const infoBytes = toBytes(info, "utf8", "info"); (async () => { try { // OpenSSL's HKDF (unlike its scrypt path) fails outright on a zero-length // output rather than succeeding with an empty buffer — confirmed under Node // 24.16.0: `crypto.hkdf("sha256","k","s","i",0,cb)` calls cb with a plain, // codeless `Error: Deriving bits failed`, same as pbkdf2's. if (keylen === 0) throw derivingBitsFailed(); const key = await globalThis.crypto.subtle.importKey("raw", ikmBytes as Uint8Array<ArrayBuffer>, "HKDF", false, ["deriveBits"]); const bits = await globalThis.crypto.subtle.deriveBits( { name: "HKDF", hash: hash.name, salt: saltBytes as Uint8Array<ArrayBuffer>, info: infoBytes as Uint8Array<ArrayBuffer> }, key, keylen * 8, ); callback(null, bits); } catch (err) { callback(err instanceof Error ? err : new Error(String(err))); } })();}
/** hkdfSync (RFC 5869): WebCrypto's HKDF has no synchronous form, so this runs * HKDF-Extract-and-Expand directly over hmacPads/hmacWithPads instead — the same * synchronous HMAC pbkdf2Derive is built on — rather than sharing the async hkdf's * WebCrypto call. That means its digest support is exactly digest.ts's own * (md5/sha1/sha224/sha256/sha384/sha512/sha512-224/sha512-256), resolved the same * way pbkdf2/scrypt do (resolveKdfDigest, below), not the smaller sha1/256/384/512 * set the async hkdf is limited to by WebCrypto's own digest list (no sha224 or * sha512/t there at all, so that table — HKDF_HASHES, above — stays as is). */function hkdfSync(digest: unknown, ikm: unknown, salt: unknown, info: unknown, keylen: unknown): Buffer { validateDigestString(digest); const ikmBytes = hkdfKeyBytes(ikm); const saltBytes = hkdfBytesOrThrow(salt, "salt"); const infoBytes = hkdfBytesOrThrow(info, "info"); validateNumberRange(keylen, "length", 0, Number.MAX_SAFE_INTEGER); if ((infoBytes as Uint8Array).byteLength > 1024) { throw Object.assign(new RangeError('The value of "info" is out of range. It must be must not contain more than 1024 bytes. ' + `Received ${(infoBytes as Uint8Array).byteLength}`), { code: "ERR_OUT_OF_RANGE" }); } const algorithm = resolveKdfDigest(digest as string); const kl = keylen as number; if (kl > 255 * digestLengthOf(algorithm)) throw Object.assign(new RangeError("Invalid key length"), { code: "ERR_CRYPTO_INVALID_KEYLEN" }); if (kl === 0) throw derivingBitsFailed(); return Buffer.from(hkdfDerive(algorithm, ikmBytes, saltBytes as Uint8Array, infoBytes as Uint8Array, kl));}
/** RFC 5869 sections 2.2/2.3: PRK = HMAC-Hash(salt, IKM); then * T(i) = HMAC-Hash(PRK, T(i-1) | info | i), OKM = T(1) | T(2) | ... truncated to * `length`. PRK's pads are computed once and reused for every T(i), the same * reuse pbkdf2Derive does for its password pads. */function hkdfDerive(algorithm: string, ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, length: number): Uint8Array { const prk = hmacWithPads(hmacPads(algorithm, salt), ikm); const hLen = digestLengthOf(algorithm); const pads = hmacPads(algorithm, prk); const out = new Uint8Array(length); let previous: Uint8Array = new Uint8Array(0); const blocks = Math.ceil(length / hLen); for (let i = 1; i <= blocks; i++) { const input = new Uint8Array(previous.length + info.length + 1); input.set(previous); input.set(info, previous.length); input[previous.length + info.length] = i; previous = hmacWithPads(pads, input); const offset = (i - 1) * hLen; out.set(previous.subarray(0, Math.min(hLen, length - offset)), offset); } return out;}
// ---- generateKey/generateKeySync: a secret key is just random bytes of the right// length (Node's own OpenSSL binding does the same RAND_bytes call); no residue// needed, unlike generateKeyPair's asymmetric math. ----
function secretKeyLengthBytes(type: unknown, options: unknown): number { if (type !== "hmac" && type !== "aes") { throw Object.assign(new TypeError(`The argument 'type' must be a supported key type. Received ${quoteReceived(type)}`), { code: "ERR_INVALID_ARG_VALUE", }); } if (options === null || typeof options !== "object") throw invalidArgType("options", "of type object", options); const length = (options as { length?: unknown }).length; if (type === "aes") { if (length !== 128 && length !== 192 && length !== 256) { throw Object.assign(new TypeError(`The property 'options.length' must be one of: 128, 192, 256. Received ${quoteReceived(length)}`), { code: "ERR_INVALID_ARG_VALUE", }); } return length / 8; } if (typeof length !== "number") { throw Object.assign(new TypeError(`The "options.length" property must be of type number. Received ${length === undefined ? "undefined" : typeof length}`), { code: "ERR_INVALID_ARG_TYPE", }); } if (!Number.isInteger(length) || length < 8 || length > 2 ** 31 - 1) { throw Object.assign(new RangeError(`The value of "options.length" is out of range. It must be >= 8 && <= 2147483647. Received ${length}`), { code: "ERR_OUT_OF_RANGE", }); } return Math.floor(length / 8);}
function generateKeySync(type: unknown, options: unknown): KeyObject { return makeSecretKeyObject(randomFillSync(new Uint8Array(secretKeyLengthBytes(type, options))));}
function generateKey(type: unknown, options: unknown, callback: (err: Error | null, key?: KeyObject) => void): void { if (typeof callback !== "function") throw invalidArgType("callback", "of type function", callback); const bytes = randomFillSync(new Uint8Array(secretKeyLengthBytes(type, options))); // sync validation, as Node's does queueMicrotask(() => callback(null, makeSecretKeyObject(bytes)));}
const WHY = "only hashing, HMAC and randomness are implemented";const WHY_CIPHER = "block-cipher encryption needs a real AES/ChaCha20 implementation; a genuine WASM-residue gap, not something WebCrypto's async-only subtle.encrypt/decrypt covers for a sync stream API";const WHY_ASYMMETRIC = "needs an asymmetric key imported from arbitrary PEM/DER/JWK and the matching signature algorithm; out of scope here";const WHY_RESIDUE = "needs synchronous crypto beyond hashing; WebCrypto is async-only, so this waits on the WASM residue";
// These classes exist so instanceof and the stream wiring match Node; constructing any refuses.markRefused(Cipheriv, WHY_CIPHER);markRefused(Decipheriv, WHY_CIPHER);markRefused(Sign, WHY_ASYMMETRIC);markRefused(Verify, WHY_ASYMMETRIC);
export const crypto = { createHash: (algorithm: string) => new Hash(algorithm), createHmac: (algorithm: string, key: BinaryLike) => new Hmac(algorithm, key), hash: (algorithm: string, data: BinaryLike, outputEncoding: string = "hex") => new Hash(algorithm).update(data).digest(outputEncoding), getHashes: () => [...SUPPORTED_DIGESTS], Hash, Hmac, randomBytes, randomFillSync, randomUUID: () => globalThis.crypto.randomUUID(), getRandomValues: <T extends ArrayBufferView>(array: T) => globalThis.crypto.getRandomValues(array), timingSafeEqual, webcrypto: globalThis.crypto, subtle: globalThis.crypto.subtle, constants: {}, createCipheriv: refused("crypto", "createCipheriv", WHY_CIPHER), createDecipheriv: refused("crypto", "createDecipheriv", WHY_CIPHER), createSign: refused("crypto", "createSign", WHY_ASYMMETRIC), createVerify: refused("crypto", "createVerify", WHY_ASYMMETRIC), createSecretKey: (key: BinaryLike, encoding?: string) => makeSecretKeyObject(toBytes(key, encoding, "key")), createPublicKey: refused("crypto", "createPublicKey", WHY_ASYMMETRIC), createPrivateKey: refused("crypto", "createPrivateKey", WHY_ASYMMETRIC), generateKeyPair: refused("crypto", "generateKeyPair", WHY_ASYMMETRIC), generateKeyPairSync: refused("crypto", "generateKeyPairSync", WHY_ASYMMETRIC), pbkdf2, pbkdf2Sync, scrypt, scryptSync, randomFill, randomInt, getCiphers: refused("crypto", "getCiphers", WHY_CIPHER), KeyObject, Cipheriv, Decipheriv, Sign, Verify, randomUUIDv7, getCurves: refused("crypto", "getCurves", WHY_ASYMMETRIC), getCipherInfo: refused("crypto", "getCipherInfo", WHY_CIPHER), getFips, setFips, secureHeapUsed, checkPrime, checkPrimeSync, hkdf, generateKey, generateKeySync, // Sync-only surface this runtime can't back: WebCrypto has no sync form, and these // need either real asymmetric key material or prime-generation math this pass // didn't implement. Each exists and throws, naming the gap, rather than being faked. Certificate: refusedClass("crypto", "Certificate", WHY_ASYMMETRIC), X509Certificate: refusedClass("crypto", "X509Certificate", WHY_ASYMMETRIC), DiffieHellman: refusedClass("crypto", "DiffieHellman", WHY_RESIDUE), DiffieHellmanGroup: refusedClass("crypto", "DiffieHellmanGroup", WHY_RESIDUE), ECDH: refusedClass("crypto", "ECDH", WHY_RESIDUE), createDiffieHellman: refused("crypto", "createDiffieHellman", WHY_RESIDUE), createDiffieHellmanGroup: refused("crypto", "createDiffieHellmanGroup", WHY_RESIDUE), createECDH: refused("crypto", "createECDH", WHY_RESIDUE), getDiffieHellman: refused("crypto", "getDiffieHellman", WHY_RESIDUE), diffieHellman: refused("crypto", "diffieHellman", WHY_RESIDUE), decapsulate: refused("crypto", "decapsulate", WHY_RESIDUE), encapsulate: refused("crypto", "encapsulate", WHY_RESIDUE), generatePrime, generatePrimeSync, argon2: refused("crypto", "argon2", WHY_RESIDUE), argon2Sync: refused("crypto", "argon2Sync", WHY_RESIDUE), hkdfSync, sign: refused("crypto", "sign", WHY_ASYMMETRIC), verify: refused("crypto", "verify", WHY_ASYMMETRIC), privateDecrypt: refused("crypto", "privateDecrypt", WHY_ASYMMETRIC), privateEncrypt: refused("crypto", "privateEncrypt", WHY_ASYMMETRIC), publicDecrypt: refused("crypto", "publicDecrypt", WHY_ASYMMETRIC), publicEncrypt: refused("crypto", "publicEncrypt", WHY_ASYMMETRIC),};