Skip to main content

quantica/
sha3.rs

1//! Shared `Keccak-f[1600]` permutation and sponge core (FIPS 202).
2//!
3//! This module provides the single, deduplicated implementation of the
4//! Keccak permutation and the absorb/squeeze sponge state used by all
5//! three quantica algorithms (ML-KEM, ML-DSA, SLH-DSA). Each algorithm
6//! still ships its own thin `sha3` wrapper module exposing the
7//! algorithm-specific high-level functions (`h`, `g`, `j`, `prf`,
8//! `Xof` for ML-KEM; `sha3_256`, `sha3_512`, `shake128`, `shake256`,
9//! … for ML-DSA; `Shake256`, `shake256_into`, … for SLH-DSA), but they
10//! all build on top of the [`KeccakState`] defined here.
11//!
12//! Pure Rust, no external dependencies.
13
14const KECCAK_ROUNDS: usize = 24;
15
16const RC: [u64; 24] = [
17    0x0000000000000001,
18    0x0000000000008082,
19    0x800000000000808A,
20    0x8000000080008000,
21    0x000000000000808B,
22    0x0000000080000001,
23    0x8000000080008081,
24    0x8000000000008009,
25    0x000000000000008A,
26    0x0000000000000088,
27    0x0000000080008009,
28    0x000000008000000A,
29    0x000000008000808B,
30    0x800000000000008B,
31    0x8000000000008089,
32    0x8000000000008003,
33    0x8000000000008002,
34    0x8000000000000080,
35    0x000000000000800A,
36    0x800000008000000A,
37    0x8000000080008081,
38    0x8000000000008080,
39    0x0000000080000001,
40    0x8000000080008008,
41];
42
43const ROTC: [u32; 24] = [
44    1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44,
45];
46
47const PI: [usize; 24] = [
48    10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1,
49];
50
51#[inline(always)]
52fn keccak_f(state: &mut [u64; 25]) {
53    for round in 0..KECCAK_ROUNDS {
54        // θ step
55        let mut c = [0u64; 5];
56        for x in 0..5 {
57            c[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20];
58        }
59        let mut d = [0u64; 5];
60        for x in 0..5 {
61            d[x] = c[(x + 4) % 5] ^ c[(x + 1) % 5].rotate_left(1);
62        }
63        for i in 0..25 {
64            state[i] ^= d[i % 5];
65        }
66
67        // ρ and π steps
68        let mut last = state[1];
69        for i in 0..24 {
70            let j = PI[i];
71            let temp = state[j];
72            state[j] = last.rotate_left(ROTC[i]);
73            last = temp;
74        }
75
76        // χ step
77        for y in (0..25).step_by(5) {
78            let t0 = state[y];
79            let t1 = state[y + 1];
80            let t2 = state[y + 2];
81            let t3 = state[y + 3];
82            let t4 = state[y + 4];
83            state[y] = t0 ^ (!t1 & t2);
84            state[y + 1] = t1 ^ (!t2 & t3);
85            state[y + 2] = t2 ^ (!t3 & t4);
86            state[y + 3] = t3 ^ (!t4 & t0);
87            state[y + 4] = t4 ^ (!t0 & t1);
88        }
89
90        // ι step
91        state[0] ^= RC[round];
92    }
93}
94
95/// Keccak sponge state for SHA-3 and SHAKE constructions.
96///
97/// Implements the sponge construction over `Keccak-f[1600]` with
98/// configurable rate and domain-separation suffix. Supports the
99/// absorb-then-squeeze paradigm; once squeezing has begun, further
100/// absorption is not permitted.
101pub struct KeccakState {
102    state: [u64; 25],
103    /// Current byte offset in the rate portion.
104    offset: usize,
105    /// Rate in bytes.
106    rate: usize,
107    /// Domain separation / padding byte.
108    suffix: u8,
109    /// Whether we've already switched to squeezing.
110    squeezing: bool,
111}
112
113impl KeccakState {
114    /// Create a new Keccak sponge state.
115    ///
116    /// * `rate`   — the sponge rate in bytes
117    ///   (e.g., 168 for SHAKE128, 136 for SHA3-256/SHAKE256, 72 for SHA3-512).
118    /// * `suffix` — the domain-separation/padding byte (0x06 for SHA-3, 0x1f for SHAKE).
119    pub fn new(rate: usize, suffix: u8) -> Self {
120        Self {
121            state: [0u64; 25],
122            offset: 0,
123            rate,
124            suffix,
125            squeezing: false,
126        }
127    }
128
129    /// Absorb input bytes into the sponge.
130    ///
131    /// May be called multiple times before squeezing to incrementally
132    /// feed data. XORs input into the rate portion of the state and
133    /// applies the Keccak-f permutation whenever a full rate block is
134    /// filled.
135    ///
136    /// # Panics
137    ///
138    /// Debug-panics if called after squeezing has begun.
139    pub fn absorb(&mut self, data: &[u8]) {
140        debug_assert!(!self.squeezing, "Cannot absorb after squeezing");
141        let mut pos = 0;
142        while pos < data.len() {
143            let block_remaining = self.rate - self.offset;
144            let to_copy = block_remaining.min(data.len() - pos);
145
146            // XOR input into state bytes.
147            let state_bytes = state_as_bytes_mut(&mut self.state);
148            for i in 0..to_copy {
149                state_bytes[self.offset + i] ^= data[pos + i];
150            }
151            self.offset += to_copy;
152            pos += to_copy;
153
154            if self.offset == self.rate {
155                keccak_f(&mut self.state);
156                self.offset = 0;
157            }
158        }
159    }
160
161    /// Finalize absorption and switch to squeezing.
162    fn finalize(&mut self) {
163        if !self.squeezing {
164            let state_bytes = state_as_bytes_mut(&mut self.state);
165            // Padding: suffix byte then final bit.
166            state_bytes[self.offset] ^= self.suffix;
167            state_bytes[self.rate - 1] ^= 0x80;
168            keccak_f(&mut self.state);
169            self.offset = 0;
170            self.squeezing = true;
171        }
172    }
173
174    /// Squeeze output bytes from the sponge.
175    ///
176    /// On the first call, finalizes absorption by applying padding and
177    /// running Keccak-f. Subsequent calls continue squeezing, applying
178    /// Keccak-f each time the rate buffer is exhausted. May be called
179    /// multiple times to produce arbitrary-length output (XOF mode).
180    pub fn squeeze(&mut self, out: &mut [u8]) {
181        self.finalize();
182        let mut pos = 0;
183        while pos < out.len() {
184            if self.offset == self.rate {
185                keccak_f(&mut self.state);
186                self.offset = 0;
187            }
188            let available = self.rate - self.offset;
189            let to_copy = available.min(out.len() - pos);
190
191            // Fast path: read by u64 lanes when aligned.
192            let mut i = 0;
193            while i + 8 <= to_copy && (self.offset + i) % 8 == 0 {
194                let lane = (self.offset + i) / 8;
195                let bytes = self.state[lane].to_le_bytes();
196                out[pos + i..pos + i + 8].copy_from_slice(&bytes);
197                i += 8;
198            }
199            // Slow path: remaining bytes.
200            let state_bytes = state_as_bytes(&self.state);
201            while i < to_copy {
202                out[pos + i] = state_bytes[self.offset + i];
203                i += 1;
204            }
205
206            self.offset += to_copy;
207            pos += to_copy;
208        }
209    }
210}
211
212#[inline]
213fn state_as_bytes(state: &[u64; 25]) -> &[u8; 200] {
214    unsafe { &*(state.as_ptr() as *const [u8; 200]) }
215}
216
217#[inline]
218fn state_as_bytes_mut(state: &mut [u64; 25]) -> &mut [u8; 200] {
219    unsafe { &mut *(state.as_mut_ptr() as *mut [u8; 200]) }
220}
221
222// ============================================================
223// Standard FIPS 202 rates (in bytes).
224// ============================================================
225
226/// SHA3-256: rate = (1600 − 2·256) / 8 = 136 bytes, suffix = 0x06.
227pub const SHA3_256_RATE: usize = 136;
228/// SHA3-512: rate = (1600 − 2·512) / 8 = 72 bytes, suffix = 0x06.
229pub const SHA3_512_RATE: usize = 72;
230/// SHAKE128: rate = (1600 − 2·128) / 8 = 168 bytes, suffix = 0x1f.
231pub const SHAKE128_RATE: usize = 168;
232/// SHAKE256: rate = (1600 − 2·256) / 8 = 136 bytes, suffix = 0x1f.
233pub const SHAKE256_RATE: usize = 136;
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    fn sha3_256(input: &[u8]) -> [u8; 32] {
240        let mut s = KeccakState::new(SHA3_256_RATE, 0x06);
241        s.absorb(input);
242        let mut out = [0u8; 32];
243        s.squeeze(&mut out);
244        out
245    }
246
247    fn sha3_512(input: &[u8]) -> [u8; 64] {
248        let mut s = KeccakState::new(SHA3_512_RATE, 0x06);
249        s.absorb(input);
250        let mut out = [0u8; 64];
251        s.squeeze(&mut out);
252        out
253    }
254
255    #[test]
256    fn sha3_256_empty_kat() {
257        // SHA3-256("") = a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a
258        let out = sha3_256(b"");
259        let expected = [
260            0xa7, 0xff, 0xc6, 0xf8, 0xbf, 0x1e, 0xd7, 0x66, 0x51, 0xc1, 0x47, 0x56, 0xa0, 0x61, 0xd6, 0x62, 0xf5, 0x80,
261            0xff, 0x4d, 0xe4, 0x3b, 0x49, 0xfa, 0x82, 0xd8, 0x0a, 0x4b, 0x80, 0xf8, 0x43, 0x4a,
262        ];
263        assert_eq!(out, expected);
264    }
265
266    #[test]
267    fn sha3_512_empty_kat_first_32() {
268        let out = sha3_512(b"");
269        let expected_first_32 = [
270            0xa6, 0x9f, 0x73, 0xcc, 0xa2, 0x3a, 0x9a, 0xc5, 0xc8, 0xb5, 0x67, 0xdc, 0x18, 0x5a, 0x75, 0x6e, 0x97, 0xc9,
271            0x82, 0x16, 0x4f, 0xe2, 0x58, 0x59, 0xe0, 0xd1, 0xdc, 0xc1, 0x47, 0x5c, 0x80, 0xa6,
272        ];
273        assert_eq!(&out[..32], &expected_first_32);
274    }
275}