Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445// Synchronous incremental MD5, SHA-1, SHA-256 and SHA-512 in TypeScript. The browser// has no synchronous digest, and the precedence table routes this to the WASM residue// (step 5); until it lands these produce the same bytes in plain JS. Round constants// are derived from their definitions (prime roots, sines) rather than transcribed.
export interface Digest { update(bytes: Uint8Array): void; /** Pads and finishes; the digest must not be updated afterwards. */ digest(): Uint8Array; copy(): Digest;}
/** The parts of a Merkle-Damgard hash that differ per algorithm. */interface Algorithm { blockSize: 64 | 128; /** Bytes of the message-length suffix: 8, or 16 for SHA-512. */ lengthBytes: 8 | 16; littleEndian: boolean; init(): Int32Array; compress(state: Int32Array, block: DataView): void; /** Bytes of state emitted as the digest. */ outputBytes: number;}
class BlockDigest implements Digest { readonly #algo: Algorithm; #state: Int32Array; #buffer: Uint8Array; #buffered = 0; #length = 0; // total bytes; exact up to 2^53
constructor(algo: Algorithm, from?: BlockDigest) { this.#algo = algo; this.#state = from ? from.#state.slice() : algo.init(); this.#buffer = from ? from.#buffer.slice() : new Uint8Array(algo.blockSize); if (from) { this.#buffered = from.#buffered; this.#length = from.#length; } }
update(bytes: Uint8Array): void { const size = this.#algo.blockSize; this.#length += bytes.length; let at = 0; if (this.#buffered > 0) { const take = Math.min(size - this.#buffered, bytes.length); this.#buffer.set(bytes.subarray(0, take), this.#buffered); this.#buffered += take; at = take; if (this.#buffered < size) return; this.#algo.compress(this.#state, new DataView(this.#buffer.buffer)); this.#buffered = 0; } const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); for (; at + size <= bytes.length; at += size) { this.#algo.compress(this.#state, new DataView(view.buffer, view.byteOffset + at, size)); } this.#buffer.set(bytes.subarray(at), 0); this.#buffered = bytes.length - at; }
digest(): Uint8Array { const { blockSize, lengthBytes, littleEndian, outputBytes } = this.#algo; const bits = this.#length * 8; const padLength = ((blockSize - lengthBytes - 1 - (this.#length % blockSize)) % blockSize + blockSize) % blockSize; const tail = new Uint8Array(1 + padLength + lengthBytes); tail[0] = 0x80; const lengthView = new DataView(tail.buffer, 1 + padLength); const lo = bits >>> 0; const hi = Math.floor(bits / 0x100000000) >>> 0; if (littleEndian) { lengthView.setUint32(0, lo, true); lengthView.setUint32(4, hi, true); } else { lengthView.setUint32(lengthBytes - 4, lo); lengthView.setUint32(lengthBytes - 8, hi); } const length = this.#length; this.update(tail); this.#length = length; const out = new Uint8Array(outputBytes); const outView = new DataView(out.buffer); for (let i = 0; i < outputBytes / 4; i++) outView.setUint32(i * 4, this.#state[i]! >>> 0, littleEndian); return out; }
copy(): Digest { return new BlockDigest(this.#algo, this); }}
// ---- constants from their definitions ----
function primes(n: number): number[] { const out: number[] = []; for (let c = 2; out.length < n; c++) if (out.every((p) => c % p !== 0)) out.push(c); return out;}
function iroot(n: bigint, k: bigint): bigint { // Newton's method on integers: floor(n^(1/k)). let x = 1n << (BigInt(n.toString(2).length) / k + 1n); for (;;) { const y = ((k - 1n) * x + n / x ** (k - 1n)) / k; if (y >= x) return x; x = y; }}
/** First `bits` bits of the fractional part of p^(1/k), for the `count` primes * starting `offset` primes in: SHA-256/512's own K and H use the 1st..8th/64th * primes (offset 0); SHA-224/384's H (FIPS 180-4 5.3.2/5.3.3) are "the ninth * through sixteenth" — the same construction, just offset 8. */function fractionalRoots(count: number, k: 2 | 3, bits: 32 | 64, offset = 0): bigint[] { const mask = (1n << BigInt(bits)) - 1n; return primes(count + offset).slice(offset).map((p) => iroot(BigInt(p) << BigInt(bits * k), BigInt(k)) & mask);}
/** Every digest this module serves, as one source of truth: `algorithms()` is * checked against this tuple below (`satisfies`), so the table can't drift * from the name list createDigest/normalizeAlgorithm use. */const DIGEST_NAMES = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512", "sha512-224", "sha512-256"] as const;type DigestName = (typeof DIGEST_NAMES)[number];
// The round constants below (BigInt Newton iteration, two SHA-512 compresses for the// SHA-512/t IVs) cost more to evaluate than the rest of this ~6 KB module combined, so// nothing here runs until the first digest is actually requested.let built: Record<DigestName, Algorithm> | undefined;function algorithms(): Record<DigestName, Algorithm> { return built ?? (built = buildAlgorithms());}
function buildAlgorithms(): Record<DigestName, Algorithm> { const toInt32 = (v: bigint): number => Number(BigInt.asIntN(32, v));
// ---- MD5 ----
const MD5_K = Int32Array.from({ length: 64 }, (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000) | 0); const MD5_S = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21];
const md5: Algorithm = { blockSize: 64, lengthBytes: 8, littleEndian: true, outputBytes: 16, init: () => Int32Array.of(0x67452301, 0xefcdab89 | 0, 0x98badcfe | 0, 0x10325476), compress(state, block) { let [a, b, c, d] = state as unknown as [number, number, number, number]; for (let i = 0; i < 64; i++) { let f: number; let g: number; if (i < 16) { f = (b & c) | (~b & d); g = i; } else if (i < 32) { f = (d & b) | (~d & c); g = (5 * i + 1) % 16; } else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; } else { f = c ^ (b | ~d); g = (7 * i) % 16; } f = (f + a + MD5_K[i]! + block.getInt32(g * 4, true)) | 0; const s = MD5_S[(i >> 4) * 4 + (i % 4)]!; a = d; d = c; c = b; b = (b + ((f << s) | (f >>> (32 - s)))) | 0; } state[0] = (state[0]! + a) | 0; state[1] = (state[1]! + b) | 0; state[2] = (state[2]! + c) | 0; state[3] = (state[3]! + d) | 0; }, };
// ---- SHA-1 ----
const rotl = (x: number, n: number): number => (x << n) | (x >>> (32 - n)); const sha1W = new Int32Array(80);
const sha1: Algorithm = { blockSize: 64, lengthBytes: 8, littleEndian: false, outputBytes: 20, init: () => Int32Array.of(0x67452301, 0xefcdab89 | 0, 0x98badcfe | 0, 0x10325476, 0xc3d2e1f0 | 0), compress(state, block) { const w = sha1W; for (let t = 0; t < 16; t++) w[t] = block.getInt32(t * 4); for (let t = 16; t < 80; t++) w[t] = rotl(w[t - 3]! ^ w[t - 8]! ^ w[t - 14]! ^ w[t - 16]!, 1); let [a, b, c, d, e] = state as unknown as [number, number, number, number, number]; for (let t = 0; t < 80; t++) { let f: number; let k: number; if (t < 20) { f = (b & c) | (~b & d); k = 0x5a827999; } else if (t < 40) { f = b ^ c ^ d; k = 0x6ed9eba1; } else if (t < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8f1bbcdc | 0; } else { f = b ^ c ^ d; k = 0xca62c1d6 | 0; } const temp = (rotl(a, 5) + f + e + k + w[t]!) | 0; e = d; d = c; c = rotl(b, 30); b = a; a = temp; } state[0] = (state[0]! + a) | 0; state[1] = (state[1]! + b) | 0; state[2] = (state[2]! + c) | 0; state[3] = (state[3]! + d) | 0; state[4] = (state[4]! + e) | 0; }, };
// ---- SHA-256 ----
const SHA256_K = Int32Array.from(fractionalRoots(64, 3, 32), toInt32); const SHA256_H = Int32Array.from(fractionalRoots(8, 2, 32), toInt32); const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n)); const sha256W = new Int32Array(64);
const sha256: Algorithm = { blockSize: 64, lengthBytes: 8, littleEndian: false, outputBytes: 32, init: () => SHA256_H.slice(), compress(state, block) { const w = sha256W; for (let t = 0; t < 16; t++) w[t] = block.getInt32(t * 4); for (let t = 16; t < 64; t++) { const x = w[t - 15]!; const y = w[t - 2]!; const s0 = rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3); const s1 = rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10); w[t] = (w[t - 16]! + s0 + w[t - 7]! + s1) | 0; } let [a, b, c, d, e, f, g, h] = state as unknown as number[] as [number, number, number, number, number, number, number, number]; for (let t = 0; t < 64; t++) { const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); const ch = (e & f) ^ (~e & g); const t1 = (h + S1 + ch + SHA256_K[t]! + w[t]!) | 0; const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); const maj = (a & b) ^ (a & c) ^ (b & c); const t2 = (S0 + maj) | 0; h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0; } const out = [a, b, c, d, e, f, g, h]; for (let i = 0; i < 8; i++) state[i] = (state[i]! + out[i]!) | 0; }, };
// ---- SHA-512, on 32-bit halves: state and constants are [hi, lo, hi, lo, ...] ----
const halves = (values: bigint[]): Int32Array => { const out = new Int32Array(values.length * 2); values.forEach((v, i) => { out[i * 2] = toInt32(v >> 32n); out[i * 2 + 1] = toInt32(v); }); return out; }; const SHA512_K = halves(fractionalRoots(80, 3, 64)); const SHA512_H = halves(fractionalRoots(8, 2, 64)); const sha512W = new Int32Array(160);
// 64-bit rotate right / shift right of (hi, lo), returning the high and low halves. const rotrHi = (hi: number, lo: number, n: number): number => n < 32 ? (hi >>> n) | (lo << (32 - n)) : (lo >>> (n - 32)) | (hi << (64 - n)); const rotrLo = (hi: number, lo: number, n: number): number => n < 32 ? (lo >>> n) | (hi << (32 - n)) : (hi >>> (n - 32)) | (lo << (64 - n)); const shrHi = (hi: number, _lo: number, n: number): number => hi >>> n; const shrLo = (hi: number, lo: number, n: number): number => (lo >>> n) | (hi << (32 - n));
/** Sum of 64-bit values given as parallel hi/lo lists; writes the result to out[at..at+1]. */ function add64(out: Int32Array, at: number, his: number[], los: number[]): void { let lo = 0; let hi = 0; for (let i = 0; i < los.length; i++) { lo += los[i]! >>> 0; hi += his[i]!; } out[at] = (hi + Math.floor(lo / 0x100000000)) | 0; out[at + 1] = lo | 0; }
const tmp = new Int32Array(2);
const sha512: Algorithm = { blockSize: 128, lengthBytes: 16, littleEndian: false, outputBytes: 64, init: () => SHA512_H.slice(), compress(state, block) { const w = sha512W; for (let t = 0; t < 32; t++) w[t] = block.getInt32(t * 4); for (let t = 16; t < 80; t++) { const xh = w[(t - 15) * 2]!; const xl = w[(t - 15) * 2 + 1]!; const yh = w[(t - 2) * 2]!; const yl = w[(t - 2) * 2 + 1]!; const s0h = rotrHi(xh, xl, 1) ^ rotrHi(xh, xl, 8) ^ shrHi(xh, xl, 7); const s0l = rotrLo(xh, xl, 1) ^ rotrLo(xh, xl, 8) ^ shrLo(xh, xl, 7); const s1h = rotrHi(yh, yl, 19) ^ rotrHi(yh, yl, 61) ^ shrHi(yh, yl, 6); const s1l = rotrLo(yh, yl, 19) ^ rotrLo(yh, yl, 61) ^ shrLo(yh, yl, 6); add64(w, t * 2, [w[(t - 16) * 2]!, s0h, w[(t - 7) * 2]!, s1h], [w[(t - 16) * 2 + 1]!, s0l, w[(t - 7) * 2 + 1]!, s1l]); } const v = state.slice(); // a..h as hi/lo pairs for (let t = 0; t < 80; t++) { const [ah, al, bh, bl, ch, cl, dh, dl, eh, el, fh, fl, gh, gl, hh, hl] = v as unknown as number[]; const S1h = rotrHi(eh!, el!, 14) ^ rotrHi(eh!, el!, 18) ^ rotrHi(eh!, el!, 41); const S1l = rotrLo(eh!, el!, 14) ^ rotrLo(eh!, el!, 18) ^ rotrLo(eh!, el!, 41); const chh = (eh! & fh!) ^ (~eh! & gh!); const chl = (el! & fl!) ^ (~el! & gl!); add64(tmp, 0, [hh!, S1h, chh, SHA512_K[t * 2]!, w[t * 2]!], [hl!, S1l, chl, SHA512_K[t * 2 + 1]!, w[t * 2 + 1]!]); const t1h = tmp[0]!; const t1l = tmp[1]!; const S0h = rotrHi(ah!, al!, 28) ^ rotrHi(ah!, al!, 34) ^ rotrHi(ah!, al!, 39); const S0l = rotrLo(ah!, al!, 28) ^ rotrLo(ah!, al!, 34) ^ rotrLo(ah!, al!, 39); const majh = (ah! & bh!) ^ (ah! & ch!) ^ (bh! & ch!); const majl = (al! & bl!) ^ (al! & cl!) ^ (bl! & cl!); v.copyWithin(2, 0, 14); // h=g, g=f, f=e, e=d, d=c, c=b, b=a add64(v, 8, [dh!, t1h], [dl!, t1l]); // e = d + t1 add64(v, 0, [t1h, S0h, majh], [t1l, S0l, majl]); // a = t1 + S0 + maj } for (let i = 0; i < 8; i++) add64(state, i * 2, [state[i * 2]!, v[i * 2]!], [state[i * 2 + 1]!, v[i * 2 + 1]!]); }, };
// ---- SHA-384: SHA-512's own compress, just a different IV (FIPS 180-4 5.3.3: the // "ninth through sixteenth prime" construction, at 64 bits) and the output // truncated to 48 bytes. ----
const SHA384_H = halves(fractionalRoots(8, 2, 64, 8));
const sha384: Algorithm = { ...sha512, outputBytes: 48, init: () => SHA384_H.slice(), };
// ---- SHA-224: SHA-256's own compress, truncated to 28 bytes, over the LOW 32 bits // of each of SHA-384's own 64-bit words above — not a fresh 32-bit sqrt-fraction // computation of its own. (Confirmed numerically: taking "the first 32 bits of the // fractional part" directly, the way SHA-256/384/512's own H and K are derived // here, gives SHA-384's high halves, not SHA-224's IV — SHA-224's words are the // *second* 32 bits of that same 64-bit fraction, i.e. exactly SHA-384's low // halves, which is what FIPS 180-4 5.3.2 actually specifies.) ----
const SHA224_H = Int32Array.from({ length: 8 }, (_, i) => SHA384_H[i * 2 + 1]!);
const sha224: Algorithm = { ...sha256, outputBytes: 28, init: () => SHA224_H.slice(), };
/** SHA-512/224 and SHA-512/256's IVs aren't prime-root constants like every other * variant's: FIPS 180-4 5.3.6's "SHA-512/t IV Generation Function" defines them as * SHA-512 itself, with its own IV XORed with a5a5...a5, hashing the ASCII label * ("SHA-512/224" or "SHA-512/256") as ordinary SHA-512 input. So rather than * transcribing yet another pair of pre-computed constants, this runs that * definition for real: one call to sha512's own compress, over one padded block * built the same way BlockDigest.digest() pads its tail. */ function sha512tInitialHash(label: string): Int32Array { const state = new Int32Array(SHA512_H.length); for (let i = 0; i < state.length; i++) state[i] = SHA512_H[i]! ^ 0xa5a5a5a5; const bytes = new TextEncoder().encode(label); // always well under one 128-byte block const block = new Uint8Array(sha512.blockSize); block.set(bytes); block[bytes.length] = 0x80; new DataView(block.buffer).setUint32(sha512.blockSize - 4, bytes.length * 8, false); sha512.compress(state, new DataView(block.buffer)); return state; }
const SHA512_224_H = sha512tInitialHash("SHA-512/224"); const SHA512_256_H = sha512tInitialHash("SHA-512/256");
const sha512_224: Algorithm = { ...sha512, outputBytes: 28, init: () => SHA512_224_H.slice() }; const sha512_256: Algorithm = { ...sha512, outputBytes: 32, init: () => SHA512_256_H.slice() };
return { md5, sha1, sha224, sha256, sha384, sha512, "sha512-224": sha512_224, "sha512-256": sha512_256, } satisfies Record<DigestName, Algorithm>;}
/** Node's names are case-insensitive and accept OpenSSL's RSA- and dashed aliases: * "sha-1"/"sha-224"/"sha-256"/"sha-384"/"sha-512" all dash the digit group, but * "sha512-224"/"sha512-256" (confirmed under Node 24.16.0) don't take a second * dash before the /t suffix — "sha-512-224" isn't recognized — so those two are * already their own canonical DIGEST_NAMES entries and need no rewriting here. */export function normalizeAlgorithm(name: string): string | undefined { const n = name.toLowerCase().replace(/^rsa-/, ""); const canonical = n === "sha-1" ? "sha1" : n === "sha-224" ? "sha224" : n === "sha-256" ? "sha256" : n === "sha-384" ? "sha384" : n === "sha-512" ? "sha512" : n; return (DIGEST_NAMES as readonly string[]).includes(canonical) ? canonical : undefined;}
export function createDigest(name: string): Digest | undefined { const algo = algorithms()[normalizeAlgorithm(name) as DigestName]; return algo ? new BlockDigest(algo) : undefined;}
export function blockSizeOf(name: string): number { return algorithms()[normalizeAlgorithm(name) as DigestName]!.blockSize;}
export function digestLengthOf(name: string): number { return algorithms()[normalizeAlgorithm(name) as DigestName]!.outputBytes;}
export const SUPPORTED_DIGESTS: readonly DigestName[] = DIGEST_NAMES;