// 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 limit const 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(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; 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 { 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; } 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 = { 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, "HKDF", false, ["deriveBits"]); const bits = await globalThis.crypto.subtle.deriveBits( { name: "HKDF", hash: hash.name, salt: saltBytes as Uint8Array, info: infoBytes as Uint8Array }, 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: (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), };