Files
ww/lib/crypto/sha256/sha256.ww

326 lines
9.9 KiB
Plaintext

// 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).
// - array dimensions are integer literals (`[64]u8`, `[8]u32`,
// `[64]u32`) where Hare uses the `BLOCKSZ` def / `[_]u32`: ww rejects
// a def in array-dimension position ("array length must be an integer
// literal"). Retained language divergence — filed as #269.
// - 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;
// 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; def array dims rejected (#269)
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 = &copy;`)
// 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 = (&copy);
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");
};
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((&copy): *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;
};