Skip to main content

logicaffeine_compile/
crypto_accel.rs

1//! Native SHA-256 / HMAC-SHA256 / PBKDF2-HMAC-SHA256, byte-identical to the
2//! pure-LOGOS reference in `assets/std/crypto.lg`.
3//!
4//! The pure-LOGOS crypto is the SPEC — it is what the AOT tiers (Rust, C, WASM)
5//! compile to native code, and it is FIPS/RFC vector-locked. But the tree-walking
6//! interpreter INTERPRETS every `Word32` operation, so PBKDF2's mandated 4096
7//! iterations (16k SHA-256 compressions) take ~32s — far too slow for a real
8//! SCRAM handshake. This module accelerates exactly that hot primitive on the
9//! interpreted tier: the interpreter intercepts `pbkdf2Sha256` and runs this
10//! instead, differentially locked to produce the identical bytes as the pure
11//! reference (see `crypto_accel_lock.rs`). No corner is cut — the pure code is
12//! still the single source the compiled tiers use; this is the same "the
13//! reference implementation is native C" arrangement every language's `hashlib`
14//! uses.
15
16const H0: [u32; 8] = [
17    0x6a09_e667,
18    0xbb67_ae85,
19    0x3c6e_f372,
20    0xa54f_f53a,
21    0x510e_527f,
22    0x9b05_688c,
23    0x1f83_d9ab,
24    0x5be0_cd19,
25];
26
27const K: [u32; 64] = [
28    0x428a_2f98,
29    0x7137_4491,
30    0xb5c0_fbcf,
31    0xe9b5_dba5,
32    0x3956_c25b,
33    0x59f1_11f1,
34    0x923f_82a4,
35    0xab1c_5ed5,
36    0xd807_aa98,
37    0x1283_5b01,
38    0x2431_85be,
39    0x550c_7dc3,
40    0x72be_5d74,
41    0x80de_b1fe,
42    0x9bdc_06a7,
43    0xc19b_f174,
44    0xe49b_69c1,
45    0xefbe_4786,
46    0x0fc1_9dc6,
47    0x240c_a1cc,
48    0x2de9_2c6f,
49    0x4a74_84aa,
50    0x5cb0_a9dc,
51    0x76f9_88da,
52    0x983e_5152,
53    0xa831_c66d,
54    0xb003_27c8,
55    0xbf59_7fc7,
56    0xc6e0_0bf3,
57    0xd5a7_9147,
58    0x06ca_6351,
59    0x1429_2967,
60    0x27b7_0a85,
61    0x2e1b_2138,
62    0x4d2c_6dfc,
63    0x5338_0d13,
64    0x650a_7354,
65    0x766a_0abb,
66    0x81c2_c92e,
67    0x9272_2c85,
68    0xa2bf_e8a1,
69    0xa81a_664b,
70    0xc24b_8b70,
71    0xc76c_51a3,
72    0xd192_e819,
73    0xd699_0624,
74    0xf40e_3585,
75    0x106a_a070,
76    0x19a4_c116,
77    0x1e37_6c08,
78    0x2748_774c,
79    0x34b0_bcb5,
80    0x391c_0cb3,
81    0x4ed8_aa4a,
82    0x5b9c_ca4f,
83    0x682e_6ff3,
84    0x748f_82ee,
85    0x78a5_636f,
86    0x84c8_7814,
87    0x8cc7_0208,
88    0x90be_fffa,
89    0xa450_6ceb,
90    0xbef9_a3f7,
91    0xc671_78f2,
92];
93
94fn compress(h: &mut [u32; 8], block: &[u8; 64]) {
95    let mut w = [0u32; 64];
96    for i in 0..16 {
97        w[i] = u32::from_be_bytes([
98            block[i * 4],
99            block[i * 4 + 1],
100            block[i * 4 + 2],
101            block[i * 4 + 3],
102        ]);
103    }
104    for i in 16..64 {
105        let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
106        let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
107        w[i] = w[i - 16]
108            .wrapping_add(s0)
109            .wrapping_add(w[i - 7])
110            .wrapping_add(s1);
111    }
112    let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) =
113        (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
114    for i in 0..64 {
115        let big_s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
116        let ch = (e & f) ^ ((!e) & g);
117        let t1 = hh
118            .wrapping_add(big_s1)
119            .wrapping_add(ch)
120            .wrapping_add(K[i])
121            .wrapping_add(w[i]);
122        let big_s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
123        let maj = (a & b) ^ (a & c) ^ (b & c);
124        let t2 = big_s0.wrapping_add(maj);
125        hh = g;
126        g = f;
127        f = e;
128        e = d.wrapping_add(t1);
129        d = c;
130        c = b;
131        b = a;
132        a = t1.wrapping_add(t2);
133    }
134    h[0] = h[0].wrapping_add(a);
135    h[1] = h[1].wrapping_add(b);
136    h[2] = h[2].wrapping_add(c);
137    h[3] = h[3].wrapping_add(d);
138    h[4] = h[4].wrapping_add(e);
139    h[5] = h[5].wrapping_add(f);
140    h[6] = h[6].wrapping_add(g);
141    h[7] = h[7].wrapping_add(hh);
142}
143
144/// SHA-256 (FIPS 180-4) of an arbitrary byte string.
145pub fn sha256(msg: &[u8]) -> [u8; 32] {
146    let mut h = H0;
147    let mut block = [0u8; 64];
148    let mut chunks = msg.chunks_exact(64);
149    for chunk in chunks.by_ref() {
150        block.copy_from_slice(chunk);
151        compress(&mut h, &block);
152    }
153    // Final block(s): remainder + 0x80 + zero pad + 64-bit big-endian bit length.
154    let rem = chunks.remainder();
155    let mut tail = [0u8; 128];
156    tail[..rem.len()].copy_from_slice(rem);
157    tail[rem.len()] = 0x80;
158    let bit_len = (msg.len() as u64) * 8;
159    let pad_len = if rem.len() < 56 { 64 } else { 128 };
160    tail[pad_len - 8..pad_len].copy_from_slice(&bit_len.to_be_bytes());
161    block.copy_from_slice(&tail[..64]);
162    compress(&mut h, &block);
163    if pad_len == 128 {
164        block.copy_from_slice(&tail[64..128]);
165        compress(&mut h, &block);
166    }
167    let mut out = [0u8; 32];
168    for (i, word) in h.iter().enumerate() {
169        out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
170    }
171    out
172}
173
174/// HMAC-SHA256 (RFC 2104 / RFC 4231).
175pub fn hmac_sha256(key: &[u8], msg: &[u8]) -> [u8; 32] {
176    let mut k = [0u8; 64];
177    if key.len() > 64 {
178        k[..32].copy_from_slice(&sha256(key));
179    } else {
180        k[..key.len()].copy_from_slice(key);
181    }
182    let mut inner = Vec::with_capacity(64 + msg.len());
183    inner.extend(k.iter().map(|b| b ^ 0x36));
184    inner.extend_from_slice(msg);
185    let inner_hash = sha256(&inner);
186    let mut outer = Vec::with_capacity(64 + 32);
187    outer.extend(k.iter().map(|b| b ^ 0x5c));
188    outer.extend_from_slice(&inner_hash);
189    sha256(&outer)
190}
191
192/// PBKDF2-HMAC-SHA256 with dkLen fixed at 32 — exactly SCRAM's Hi()
193/// (RFC 5802 / RFC 7677): one output block, so U1 = HMAC(password, salt ‖ 0x00000001)
194/// and the derived key is the running XOR of the U chain over `iters` iterations.
195///
196/// HMAC over a FIXED key precomputes the inner/outer block midstates once, so each
197/// iteration costs two compressions instead of four — the pure LOGOS reference does
198/// the naive four; the result is byte-identical.
199pub fn pbkdf2_sha256(password: &[u8], salt: &[u8], iters: u32) -> [u8; 32] {
200    // Precompute the key-derived padded block and the two HMAC midstates.
201    let mut k = [0u8; 64];
202    if password.len() > 64 {
203        k[..32].copy_from_slice(&sha256(password));
204    } else {
205        k[..password.len()].copy_from_slice(password);
206    }
207    let mut ipad = [0u8; 64];
208    let mut opad = [0u8; 64];
209    for i in 0..64 {
210        ipad[i] = k[i] ^ 0x36;
211        opad[i] = k[i] ^ 0x5c;
212    }
213    let mut inner_state = H0;
214    compress(&mut inner_state, &ipad);
215    let mut outer_state = H0;
216    compress(&mut outer_state, &opad);
217
218    // U1 = HMAC(password, salt ‖ INT(1)). The salt may span multiple blocks;
219    // fall back to the straightforward HMAC for that first message, then use the
220    // midstate fast path (the per-iteration message is always a fixed 32 bytes).
221    let mut first_msg = Vec::with_capacity(salt.len() + 4);
222    first_msg.extend_from_slice(salt);
223    first_msg.extend_from_slice(&1u32.to_be_bytes());
224    let mut u = hmac_with_states(&inner_state, &outer_state, &first_msg);
225    let mut t = u;
226    for _ in 1..iters {
227        u = hmac_with_states(&inner_state, &outer_state, &u);
228        for j in 0..32 {
229            t[j] ^= u[j];
230        }
231    }
232    t
233}
234
235/// HMAC-SHA256 given precomputed inner/outer midstates (after absorbing the
236/// 64-byte key⊕ipad and key⊕opad blocks). `msg` is appended to each midstate.
237fn hmac_with_states(inner_state: &[u32; 8], outer_state: &[u32; 8], msg: &[u8]) -> [u8; 32] {
238    let inner_hash = finalize_from(inner_state, msg, 64);
239    finalize_from(outer_state, &inner_hash, 64)
240}
241
242/// Continue a SHA-256 that has already absorbed `prefix_len` bytes (whose state
243/// is `state`), hashing `tail` and finalizing. Only the tail's remaining blocks
244/// are compressed.
245fn finalize_from(state: &[u32; 8], tail: &[u8], prefix_len: u64) -> [u8; 32] {
246    let mut h = *state;
247    let total_bits = (prefix_len + tail.len() as u64) * 8;
248    let mut block = [0u8; 64];
249    let mut chunks = tail.chunks_exact(64);
250    for chunk in chunks.by_ref() {
251        block.copy_from_slice(chunk);
252        compress(&mut h, &block);
253    }
254    let rem = chunks.remainder();
255    let mut pad = [0u8; 128];
256    pad[..rem.len()].copy_from_slice(rem);
257    pad[rem.len()] = 0x80;
258    let pad_len = if rem.len() < 56 { 64 } else { 128 };
259    pad[pad_len - 8..pad_len].copy_from_slice(&total_bits.to_be_bytes());
260    block.copy_from_slice(&pad[..64]);
261    compress(&mut h, &block);
262    if pad_len == 128 {
263        block.copy_from_slice(&pad[64..128]);
264        compress(&mut h, &block);
265    }
266    let mut out = [0u8; 32];
267    for (i, word) in h.iter().enumerate() {
268        out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
269    }
270    out
271}
272
273#[cfg(test)]
274#[path = "../../logicaffeine_tests/unit/logicaffeine_compile/crypto_accel/tests.rs"]
275mod tests;