/** * Synchronous SHA-256 / HMAC / HKDF so markdown rendering (which is synchronous all the way down through * markdown-it and the HTML sanitizer) can sign image proxy URLs without becoming async. WebCrypto is used * everywhere else; this is a textbook FIPS 180-4 implementation for short inputs only. */ const encoder = new TextEncoder(); const BLOCK_BYTES = 64; const DIGEST_BYTES = 32; const WORD_BITS = 32; const BYTE_BITS = 8; const LENGTH_FIELD_BYTES = 8; const PADDING_BYTE = 0x80; const IPAD = 0x36; const OPAD = 0x5c; const ROUNDS = 64; const SCHEDULE_WORDS = 64; const BLOCK_WORDS = 16; const HEX_RADIX = 16; const BYTE_MASK = 0xff; const INITIAL_STATE = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, ] as const; const ROUND_CONSTANTS = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, ] as const; function rotr(value: number, bits: number): number { return (value >>> bits) | (value << (WORD_BITS - bits)); } export function sha256(message: Uint8Array): Uint8Array { const bitLength = message.byteLength * BYTE_BITS; const paddedLength = Math.ceil((message.byteLength + 1 + LENGTH_FIELD_BYTES) / BLOCK_BYTES) * BLOCK_BYTES; const padded = new Uint8Array(paddedLength); padded.set(message); padded[message.byteLength] = PADDING_BYTE; const view = new DataView(padded.buffer); view.setUint32(paddedLength - 4, bitLength >>> 0); view.setUint32(paddedLength - LENGTH_FIELD_BYTES, Math.floor(bitLength / 2 ** WORD_BITS)); const state: number[] = [...INITIAL_STATE]; const schedule = new Uint32Array(SCHEDULE_WORDS); for (let offset = 0; offset < paddedLength; offset += BLOCK_BYTES) { for (let index = 0; index < BLOCK_WORDS; index++) schedule[index] = view.getUint32(offset + index * 4); for (let index = BLOCK_WORDS; index < SCHEDULE_WORDS; index++) { const w15 = schedule[index - 15]; const w2 = schedule[index - 2]; const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3); const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10); schedule[index] = (schedule[index - 16] + s0 + schedule[index - 7] + s1) >>> 0; } let [a, b, c, d, e, f, g, h] = state; for (let round = 0; round < ROUNDS; round++) { const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); const choose = (e & f) ^ (~e & g); const temp1 = (h + s1 + choose + ROUND_CONSTANTS[round] + schedule[round]) >>> 0; const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); const majority = (a & b) ^ (a & c) ^ (b & c); const temp2 = (s0 + majority) >>> 0; h = g; g = f; f = e; e = (d + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 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; state[4] = (state[4] + e) >>> 0; state[5] = (state[5] + f) >>> 0; state[6] = (state[6] + g) >>> 0; state[7] = (state[7] + h) >>> 0; } const out = new Uint8Array(DIGEST_BYTES); const outView = new DataView(out.buffer); state.forEach((word, index) => outView.setUint32(index * 4, word)); return out; } function concat(...parts: Uint8Array[]): Uint8Array { const out = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0)); let offset = 0; for (const part of parts) { out.set(part, offset); offset += part.byteLength; } return out; } export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array { const normalizedKey = key.byteLength > BLOCK_BYTES ? sha256(key) : key; const inner = new Uint8Array(BLOCK_BYTES).fill(IPAD); const outer = new Uint8Array(BLOCK_BYTES).fill(OPAD); normalizedKey.forEach((byte, index) => { inner[index] ^= byte; outer[index] ^= byte; }); return sha256(concat(outer, sha256(concat(inner, message)))); } /** RFC 5869 HKDF-SHA256, single-block output (32 bytes) which is all a MAC key needs. */ export function hkdfSha256(secret: Uint8Array, salt: string, info: string): Uint8Array { const prk = hmacSha256(encoder.encode(salt), secret); return hmacSha256(prk, concat(encoder.encode(info), new Uint8Array([1]))); } export function bytesToHex(bytes: Uint8Array): string { let out = ""; for (const byte of bytes) out += byte.toString(HEX_RADIX).padStart(2, "0"); return out; } export function hexToBytes(hex: string): Uint8Array | null { if (hex.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(hex)) return null; const out = new Uint8Array(hex.length / 2); for (let index = 0; index < out.length; index++) { out[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), HEX_RADIX) & BYTE_MASK; } return out; } export function utf8(text: string): Uint8Array { return encoder.encode(text); }