// crypto/sha256 — SHA-256 (FIPS 180-4) over the hash::hash interface. // Port of ref/hare/crypto/sha256/sha256.ha. The digest is computed // 64-byte block at a time; partial writes buffer in `x` until a full // block accumulates, and [[sum]] appends the 0x80/zero/bit-length // padding before reading out the eight state words big-endian. // // u32 WRAPPING. SHA-256 is defined over 32-bit modular arithmetic. ww's // `uint` is a 64-bit machine word, so this module uses `u32` throughout // (matching Hare) and relies on cgen truncating u32 add/shift/rotate to // 32 bits. The NIST vectors in sha256_test.ww are the oracle: a digest // mismatch would mean cgen promoted a u32 op to 64-bit without wrapping // (a cgen bug to STOP+report, NOT to mask here). // // DIVERGENCES from sha256.ha (all semantics-preserving spellings): // - `state` embeds [[hash.hash]], whose first field is an inline // io.vtable (not Hare's `stream: io::stream` pointer); see // lib/hash/hash.ww. The vtable slots are wired post-construction in // [[sha256]] rather than pointing at a module-level const vtable // (the base64/memio/io convention; ww has no const-vtable-of-fn-ptrs // idiom). // - slice-to-slice copy assignment (`h.x[a..b] = b[..n]`) is spelled // as a byte loop — ww has no slice-copy assignment (cf lib/memio's // flat-field note). Same observable effect. // - close() zeros the `h` state words with a loop instead of Hare's // `(s.h[..]: *[*]u8)[..len*size(u32)]` reinterpret + bytes::zero — // ww avoids the `*[*]u8` cast; bytes.zero still clears `x`. // - the eight beputu32 digest writes are a loop (stride routed through // size(u32) per CLAUDE.md rule 13) rather than Hare's unrolled 8. package sha256; import bytes; import crypto.math; import endian; import hash; import io; import os; // ref/hare/crypto/sha256/sha256.ha:11. export def SZ: size = 32; // ref/hare/crypto/sha256/sha256.ha:14. export def BLOCKSZ: size = 64; // ref/hare/crypto/sha256/sha256.ha:17-24. u32 suffixes: these scalar // defs don't ride #251 array-element narrowing, and four exceed 2^31. def init0: u32 = 0x6A09E667u32; def init1: u32 = 0xBB67AE85u32; def init2: u32 = 0x3C6EF372u32; def init3: u32 = 0xA54FF53Au32; def init4: u32 = 0x510E527Fu32; def init5: u32 = 0x9B05688Cu32; def init6: u32 = 0x1F83D9ABu32; def init7: u32 = 0x5BE0CD19u32; // ref/hare/crypto/sha256/sha256.ha:26-38. The round constants (first 32 // bits of the fractional parts of the cube roots of the first 64 // primes). Bare hex literals narrow to u32 via #251 array-element // narrowing (same as base64's decmap). def k: [64]u32 = [ 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, ]; // ref/hare/crypto/sha256/sha256.ha:40-46. Embeds [[hash.hash]] (whose // first field is the inline vtable) so `&st` casts to `*hash.hash` and // `&st.vt` to `io.stream`. `h` = the eight running state words, `x` = // the partial-block buffer, `nx` = bytes buffered in `x`, `ln` = total // bytes written. export type state = struct { hash.hash, h: [8]u32, x: [64]u8, // BLOCKSZ; array dims require an integer literal nx: size, ln: size, }; // sha256 — construct a SHA-256 hash. ref/hare/crypto/sha256/sha256.ha:58-69. // Wires the vtable + interface fn-ptr slots inline (base64/memio // convention) then resets to the initial state. Returned BY VALUE; the // caller passes `&st` (as `*hash.hash`) to the hash dispatchers. export fn sha256() state = { let sha: state; sha.vt.writer = (&writefn): *io.writer; sha.vt.closer = (&closefn): *io.closer; sha.sum = (&sumfn): *fn(h: *hash.hash, buf: []u8) void; sha.reset = (&resetfn): *fn(h: *hash.hash) void; sha.sz = SZ; sha.bsz = BLOCKSZ; hash.reset((&sha): *hash.hash); return sha; }; // ref/hare/crypto/sha256/sha256.ha:71-83. fn resetfn(h: *hash.hash) void = { let st: *state = h: *state; st.h[0] = init0; st.h[1] = init1; st.h[2] = init2; st.h[3] = init3; st.h[4] = init4; st.h[5] = init5; st.h[6] = init6; st.h[7] = init7; st.nx = 0: size; st.ln = 0: size; }; // writefn — the io.writer slot. ref/hare/crypto/sha256/sha256.ha:85-113. // Buffers a partial block in `x`, processes every whole block inline, // and stashes the trailing remainder. Returns the input byte count // (never errors). Slice copies are byte loops (header divergence). fn writefn(s: io.stream, buf: []u8) (size | io.error) = { let st: *state = s: *state; let b: []u8 = buf; let total: size = b.len: size; st.ln += total; let bs: i32 = BLOCKSZ: i32; if (st.nx > 0: size) { let avail: size = BLOCKSZ - st.nx; let take: size = avail; if ((b.len: size) <= avail) { take = b.len: size; }; let base: i32 = st.nx: i32; let ti: i32 = take: i32; let i: i32 = 0; for (i < ti) { st.x[base + i] = b[i]; i += 1; }; st.nx += take; if (st.nx == BLOCKSZ) { block(st, st.x[0:bs]); st.nx = 0: size; }; b = b[ti:b.len]; }; if ((b.len: size) >= BLOCKSZ) { let mask: i32 = bs - 1; let nfull: i32 = b.len & ~mask; block(st, b[0:nfull]); b = b[nfull:b.len]; }; if (b.len > 0) { let i: i32 = 0; for (i < b.len) { st.x[i] = b[i]; i += 1; }; st.nx = b.len: size; }; return total; }; // sumfn — finalize and emit the digest. ref/hare/crypto/sha256/sha256.ha:115-143. // // Re-entrant per Hare: snapshot the state (`let copy = *h; let h = ©`) // and pad+finalize the COPY, so the live hash is untouched and the caller // can keep writing or sum() again. The deref-copy of this array-containing // struct was blocked by a cgen bug (#265) — fold-1 (master 4d3f846) landed // the full-size aggregate copy for deref-rhs let-init, unblocking it. The // close() at the end wipes the copy (Hare's `defer hash::close(h)`), not // the live state. fn sumfn(h: *hash.hash, buf: []u8) void = { let live: *state = h: *state; let copy: state = *live; let st: *state = (©); let ln: size = st.ln; let tmp: [64]u8 = [0...]; tmp[0] = 0x80u8; // Pad so the message + 0x80 + zeros ends 8 bytes (the bit-length // field) shy of a block boundary. let lenbytes: size = size(u64); let thresh: size = BLOCKSZ - lenbytes; let m: size = ln % BLOCKSZ; let pad: size = thresh - m; if (m >= thresh) { pad = (BLOCKSZ + thresh) - m; }; let padn: i32 = pad: i32; match (writefn(&st.vt, tmp[0:padn])) { case let z: size => void; case let e: io.error => abort("sha256.sum: pad write errored"); }; ln <<= 3; // bytes -> bits endian.beputu64(tmp[0:8], ln: u64); let lb: i32 = lenbytes: i32; match (writefn(&st.vt, tmp[0:lb])) { case let z: size => void; case let e: io.error => abort("sha256.sum: length write errored"); }; os.assert(st.nx == 0: size, "sha256.sum: residual partial block"); let stride: i32 = size(u32): i32; let i: i32 = 0; for (i < 8) { let off: i32 = i * stride; endian.beputu32(buf[off:off + stride], st.h[i]); i += 1; }; hash.close((©): *hash.hash); }; // block — process every whole 64-byte block in `buf`. // ref/hare/crypto/sha256/sha256.ha:146-213. Param renamed `st` (Hare's // `h`) so the 8th compression working var can stay `hh` without // shadowing the state pointer. fn block(st: *state, buf: []u8) void = { let w: [64]u32 = [0...]; let h0: u32 = st.h[0]; let h1: u32 = st.h[1]; let h2: u32 = st.h[2]; let h3: u32 = st.h[3]; let h4: u32 = st.h[4]; let h5: u32 = st.h[5]; let h6: u32 = st.h[6]; let h7: u32 = st.h[7]; let bs: i32 = BLOCKSZ: i32; let b: []u8 = buf; for (b.len >= bs) { let i: i32 = 0; for (i < 16) { let j: i32 = i * 4; w[i] = ((b[j]: u32) << 24u32) | ((b[j + 1]: u32) << 16u32) | ((b[j + 2]: u32) << 8u32) | (b[j + 3]: u32); i += 1; }; i = 16; for (i < 64) { let v1: u32 = w[i - 2]; let t1: u32 = math.rotr32(v1, 17) ^ math.rotr32(v1, 19) ^ (v1 >> 10u32); let v2: u32 = w[i - 15]; let t2: u32 = math.rotr32(v2, 7) ^ math.rotr32(v2, 18) ^ (v2 >> 3u32); w[i] = t1 + w[i - 7] + t2 + w[i - 16]; i += 1; }; let a: u32 = h0; let bb: u32 = h1; let c: u32 = h2; let d: u32 = h3; let e: u32 = h4; let f: u32 = h5; let g: u32 = h6; let hh: u32 = h7; i = 0; for (i < 64) { let t1: u32 = hh + (math.rotr32(e, 6) ^ math.rotr32(e, 11) ^ math.rotr32(e, 25)) + ((e & f) ^ (~e & g)) + k[i] + w[i]; let t2: u32 = (math.rotr32(a, 2) ^ math.rotr32(a, 13) ^ math.rotr32(a, 22)) + ((a & bb) ^ (a & c) ^ (bb & c)); hh = g; g = f; f = e; e = d + t1; d = c; c = bb; bb = a; a = t1 + t2; i += 1; }; h0 += a; h1 += bb; h2 += c; h3 += d; h4 += e; h5 += f; h6 += g; h7 += hh; b = b[bs:b.len]; }; st.h[0] = h0; st.h[1] = h1; st.h[2] = h2; st.h[3] = h3; st.h[4] = h4; st.h[5] = h5; st.h[6] = h6; st.h[7] = h7; }; // closefn — wipe sensitive state. ref/hare/crypto/sha256/sha256.ha:215-219. // h zeroed via a loop (header divergence); x via bytes.zero array-decay. fn closefn(s: io.stream) (void | io.error) = { let st: *state = s: *state; let i: i32 = 0; for (i < 8) { st.h[i] = 0u32; i += 1; }; bytes.zero(st.x); return void; };