The port shipped sum() single-shot — mutating the live hash state —
because Hare's state snapshot `let copy = *h; let h = ©` (a
deref-rhs aggregate let-init of an array-containing struct) miscompiled
in cgen. #265 fold-1 (master 4d3f846) landed the full-size aggregate
copy for that axis, so restore the faithful form: pad+finalize the
snapshot, leave the live state untouched, close() the copy.
sum() is now non-destructive — summing twice yields the same digest and
writing after a sum() continues the stream. Pinned by a new reentrant()
@test (sum-twice identical + write-after-sum continuity). NIST vectors
unchanged.
323 lines
9.6 KiB
Plaintext
323 lines
9.6 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).
|
|
// - 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;
|
|
};
|