Skip to main content

ic_hash/
sha3.rs

1//! FIPS 202 SHA-3 and SHAKE, built on the Keccak-f[1600] permutation.
2
3use ic_core::traits::{Algorithm, Digest, SelfTest, Xof};
4use ic_core::{ensure, Result, Zeroize};
5
6const ROUNDS: usize = 24;
7
8const RC: [u64; ROUNDS] = [
9    0x0000000000000001,
10    0x0000000000008082,
11    0x800000000000808a,
12    0x8000000080008000,
13    0x000000000000808b,
14    0x0000000080000001,
15    0x8000000080008081,
16    0x8000000000008009,
17    0x000000000000008a,
18    0x0000000000000088,
19    0x0000000080008009,
20    0x000000008000000a,
21    0x000000008000808b,
22    0x800000000000008b,
23    0x8000000000008089,
24    0x8000000000008003,
25    0x8000000000008002,
26    0x8000000000000080,
27    0x000000000000800a,
28    0x800000008000000a,
29    0x8000000080008081,
30    0x8000000000008080,
31    0x0000000080000001,
32    0x8000000080008008,
33];
34
35/// Rotation amounts for rho, in lane-cycle order.
36///
37/// Only the tests use these now: `keccak_f1600` has the rotations written out.
38/// They are kept because they, with `PI`, are the compact statement of what rho
39/// and pi do, and `rho_and_pi_agree_with_the_lane_cycle` checks the written-out
40/// form against them rather than against a stored answer.
41#[cfg(test)]
42const RHO: [u32; 24] = [
43    1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44,
44];
45
46/// Lane-cycle order for pi. See [`RHO`].
47#[cfg(test)]
48const PI: [usize; 24] = [
49    10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1,
50];
51
52/// The Keccak-f[1600] permutation over a 25-lane state.
53///
54/// Straight-line rather than table-driven. The rho and pi steps used to walk
55/// the lane cycle one position at a time, carrying a lane through a
56/// twenty-four step chain of load, rotate and scattered store -- each
57/// iteration waiting on the one before it, for no reason other than that the
58/// cycle is a convenient way to write the permutation down. The rotations do
59/// not depend on each other, so they are written out and the scheduler is free
60/// to overlap them.
61///
62/// Theta's D is folded into the same expressions. The lanes had been xored
63/// with it in a pass of their own, written back to the state, and read out
64/// again by rho; applying it where rho reads removes a pass over all
65/// twenty-five lanes per round.
66///
67/// The assignments below were generated from RHO and PI rather than
68/// transcribed, and `rho_and_pi_agree_with_the_lane_cycle` checks them against
69/// the cycle walk this replaced. Those tables are kept, under `cfg(test)`, so
70/// the compact statement of what rho and pi do stays in the file and stays
71/// checked against the version that runs.
72fn keccak_f1600(a: &mut [u64; 25]) {
73    for round in RC.iter().take(ROUNDS) {
74        // theta
75        let c0 = a[0] ^ a[5] ^ a[10] ^ a[15] ^ a[20];
76        let c1 = a[1] ^ a[6] ^ a[11] ^ a[16] ^ a[21];
77        let c2 = a[2] ^ a[7] ^ a[12] ^ a[17] ^ a[22];
78        let c3 = a[3] ^ a[8] ^ a[13] ^ a[18] ^ a[23];
79        let c4 = a[4] ^ a[9] ^ a[14] ^ a[19] ^ a[24];
80        let d = [
81            c4 ^ c1.rotate_left(1),
82            c0 ^ c2.rotate_left(1),
83            c1 ^ c3.rotate_left(1),
84            c2 ^ c4.rotate_left(1),
85            c3 ^ c0.rotate_left(1),
86        ];
87
88        // rho and pi, with theta's D applied as each lane is read
89        let mut b = [0u64; 25];
90        b[0] = a[0] ^ d[0];
91        b[1] = (a[6] ^ d[1]).rotate_left(44);
92        b[2] = (a[12] ^ d[2]).rotate_left(43);
93        b[3] = (a[18] ^ d[3]).rotate_left(21);
94        b[4] = (a[24] ^ d[4]).rotate_left(14);
95        b[5] = (a[3] ^ d[3]).rotate_left(28);
96        b[6] = (a[9] ^ d[4]).rotate_left(20);
97        b[7] = (a[10] ^ d[0]).rotate_left(3);
98        b[8] = (a[16] ^ d[1]).rotate_left(45);
99        b[9] = (a[22] ^ d[2]).rotate_left(61);
100        b[10] = (a[1] ^ d[1]).rotate_left(1);
101        b[11] = (a[7] ^ d[2]).rotate_left(6);
102        b[12] = (a[13] ^ d[3]).rotate_left(25);
103        b[13] = (a[19] ^ d[4]).rotate_left(8);
104        b[14] = (a[20] ^ d[0]).rotate_left(18);
105        b[15] = (a[4] ^ d[4]).rotate_left(27);
106        b[16] = (a[5] ^ d[0]).rotate_left(36);
107        b[17] = (a[11] ^ d[1]).rotate_left(10);
108        b[18] = (a[17] ^ d[2]).rotate_left(15);
109        b[19] = (a[23] ^ d[3]).rotate_left(56);
110        b[20] = (a[2] ^ d[2]).rotate_left(62);
111        b[21] = (a[8] ^ d[3]).rotate_left(55);
112        b[22] = (a[14] ^ d[4]).rotate_left(39);
113        b[23] = (a[15] ^ d[0]).rotate_left(41);
114        b[24] = (a[21] ^ d[1]).rotate_left(2);
115
116        // chi
117        a[0] = b[0] ^ (!b[1] & b[2]);
118        a[1] = b[1] ^ (!b[2] & b[3]);
119        a[2] = b[2] ^ (!b[3] & b[4]);
120        a[3] = b[3] ^ (!b[4] & b[0]);
121        a[4] = b[4] ^ (!b[0] & b[1]);
122        a[5] = b[5] ^ (!b[6] & b[7]);
123        a[6] = b[6] ^ (!b[7] & b[8]);
124        a[7] = b[7] ^ (!b[8] & b[9]);
125        a[8] = b[8] ^ (!b[9] & b[5]);
126        a[9] = b[9] ^ (!b[5] & b[6]);
127        a[10] = b[10] ^ (!b[11] & b[12]);
128        a[11] = b[11] ^ (!b[12] & b[13]);
129        a[12] = b[12] ^ (!b[13] & b[14]);
130        a[13] = b[13] ^ (!b[14] & b[10]);
131        a[14] = b[14] ^ (!b[10] & b[11]);
132        a[15] = b[15] ^ (!b[16] & b[17]);
133        a[16] = b[16] ^ (!b[17] & b[18]);
134        a[17] = b[17] ^ (!b[18] & b[19]);
135        a[18] = b[18] ^ (!b[19] & b[15]);
136        a[19] = b[19] ^ (!b[15] & b[16]);
137        a[20] = b[20] ^ (!b[21] & b[22]);
138        a[21] = b[21] ^ (!b[22] & b[23]);
139        a[22] = b[22] ^ (!b[23] & b[24]);
140        a[23] = b[23] ^ (!b[24] & b[20]);
141        a[24] = b[24] ^ (!b[20] & b[21]);
142
143        // iota
144        a[0] ^= *round;
145    }
146}
147
148/// A sponge over Keccak-f[1600] with a configurable rate and domain separator.
149#[derive(Clone)]
150pub(crate) struct Sponge {
151    state: [u64; 25],
152    rate: usize,
153    pos: usize,
154    pad: u8,
155}
156
157impl Sponge {
158    pub(crate) const fn new(rate: usize, pad: u8) -> Self {
159        Self {
160            state: [0u64; 25],
161            rate,
162            pos: 0,
163            pad,
164        }
165    }
166
167    /// Absorb a whole block at a time where the input allows it.
168    ///
169    /// This used to walk the input byte by byte, and a byte cost a division, a
170    /// remainder, a shift and a read-modify-write of the state. At the SHA3-256
171    /// rate that is 136 trips round the loop to feed one permutation -- about
172    /// as much work as the permutation itself, so roughly half of hashing was
173    /// spent getting the bytes into the state rather than mixing them.
174    ///
175    /// A block-aligned run is now xored a lane at a time, which is 17 of those
176    /// operations instead of 136. The byte-wise path stays for whatever
177    /// straddles the ends, since callers may hand over any lengths they like
178    /// and the result must not depend on how the input was split up.
179    pub(crate) fn absorb(&mut self, mut data: &[u8]) {
180        // Every SHA-3 and SHAKE rate is a whole number of lanes, but the
181        // sponge takes the rate as a parameter, so the fast path checks rather
182        // than assumes. A rate that is not lane-aligned simply keeps the old
183        // behaviour.
184        let lane_aligned = self.rate.is_multiple_of(8);
185
186        while !data.is_empty() {
187            if lane_aligned && self.pos == 0 && data.len() >= self.rate {
188                let (block, rest) = data.split_at(self.rate);
189                for (lane, chunk) in self.state.iter_mut().zip(block.chunks_exact(8)) {
190                    let mut b = [0u8; 8];
191                    b.copy_from_slice(chunk);
192                    *lane ^= u64::from_le_bytes(b);
193                }
194                keccak_f1600(&mut self.state);
195                data = rest;
196                continue;
197            }
198
199            let byte = data[0];
200            let lane = self.pos / 8;
201            let shift = 8 * (self.pos % 8);
202            self.state[lane] ^= (byte as u64) << shift;
203            self.pos += 1;
204            if self.pos == self.rate {
205                keccak_f1600(&mut self.state);
206                self.pos = 0;
207            }
208            data = &data[1..];
209        }
210    }
211
212    pub(crate) fn finish(&mut self) {
213        let lane = self.pos / 8;
214        let shift = 8 * (self.pos % 8);
215        self.state[lane] ^= (self.pad as u64) << shift;
216        let last = self.rate - 1;
217        self.state[last / 8] ^= 0x80u64 << (8 * (last % 8));
218        keccak_f1600(&mut self.state);
219        self.pos = 0;
220    }
221
222    pub(crate) fn squeeze(&mut self, out: &mut [u8]) {
223        let mut produced = 0;
224        while produced < out.len() {
225            if self.pos == self.rate {
226                keccak_f1600(&mut self.state);
227                self.pos = 0;
228            }
229            let lane = self.pos / 8;
230            let shift = 8 * (self.pos % 8);
231            out[produced] = (self.state[lane] >> shift) as u8;
232            self.pos += 1;
233            produced += 1;
234        }
235    }
236}
237
238impl Drop for Sponge {
239    fn drop(&mut self) {
240        self.state.zeroize();
241    }
242}
243
244macro_rules! sha3_hash {
245    ($name:ident, $id:literal, $disp:literal, $out:literal, $kat:literal) => {
246        #[doc = concat!("FIPS 202 ", $disp, ".")]
247        #[derive(Clone)]
248        pub struct $name(Sponge);
249
250        impl Default for $name {
251            fn default() -> Self {
252                Self(Sponge::new(200 - 2 * $out, 0x06))
253            }
254        }
255
256        impl Algorithm for $name {
257            const ID: &'static str = $id;
258            const NAME: &'static str = $disp;
259        }
260
261        impl Digest for $name {
262            type Output = [u8; $out];
263            const OUTPUT_LEN: usize = $out;
264            const BLOCK_LEN: usize = 200 - 2 * $out;
265
266            fn update(&mut self, data: &[u8]) {
267                self.0.absorb(data);
268            }
269
270            fn finalize(mut self) -> Self::Output {
271                let mut out = [0u8; $out];
272                self.0.finish();
273                self.0.squeeze(&mut out);
274                out
275            }
276        }
277
278        impl SelfTest for $name {
279            fn self_test() -> Result<()> {
280                let got = <Self as Digest>::digest(b"abc");
281                let mut want = [0u8; $out];
282                ic_core::codec::hex_decode($kat.as_bytes(), &mut want)?;
283                ensure!(
284                    ic_core::ct::verify(&want, got.as_ref()),
285                    SelfTestFailed,
286                    $id
287                );
288                Ok(())
289            }
290        }
291    };
292}
293
294/// A finished sponge that can be squeezed repeatedly.
295///
296/// [`Xof::finalize_xof`] consumes the hasher and produces a fixed number of
297/// bytes, which is the right shape for most callers. Rejection sampling is the
298/// exception: it cannot know in advance how much output it needs, because that
299/// depends on how many candidates it throws away. ML-KEM's `SampleNTT` is
300/// exactly this case.
301///
302/// Reading is continuous — reading 32 bytes twice gives the same stream as
303/// reading 64 once — which is what makes the sampler's output independent of
304/// the chunk size it happens to ask for.
305pub struct XofReader {
306    sponge: Sponge,
307}
308
309impl XofReader {
310    /// Squeeze the next `out.len()` bytes.
311    pub fn read(&mut self, out: &mut [u8]) {
312        self.sponge.squeeze(out);
313    }
314}
315
316macro_rules! shake {
317    ($name:ident, $id:literal, $disp:literal, $cap:literal, $kat:literal) => {
318        #[doc = concat!("FIPS 202 ", $disp, " extendable-output function.")]
319        #[derive(Clone)]
320        pub struct $name(Sponge);
321
322        impl Default for $name {
323            fn default() -> Self {
324                Self(Sponge::new(200 - $cap / 4, 0x1f))
325            }
326        }
327
328        impl Algorithm for $name {
329            const ID: &'static str = $id;
330            const NAME: &'static str = $disp;
331        }
332
333        impl Xof for $name {
334            const BLOCK_LEN: usize = 200 - $cap / 4;
335
336            fn update(&mut self, data: &[u8]) {
337                self.0.absorb(data);
338            }
339
340            fn finalize_xof(mut self, out: &mut [u8]) {
341                self.0.finish();
342                self.0.squeeze(out);
343            }
344        }
345
346        impl $name {
347            /// Finish absorbing and return a reader for an unbounded stream.
348            ///
349            /// For callers that cannot size their output in advance; see
350            /// [`XofReader`].
351            pub fn finalize_reader(mut self) -> XofReader {
352                self.0.finish();
353                XofReader { sponge: self.0 }
354            }
355
356            /// One-shot: absorb `data` and squeeze `out.len()` bytes.
357            pub fn xof(data: &[u8], out: &mut [u8]) {
358                let mut x = Self::default();
359                <Self as Xof>::update(&mut x, data);
360                x.finalize_xof(out);
361            }
362        }
363
364        impl SelfTest for $name {
365            fn self_test() -> Result<()> {
366                let mut got = [0u8; 32];
367                Self::xof(b"abc", &mut got);
368                let mut want = [0u8; 32];
369                ic_core::codec::hex_decode($kat.as_bytes(), &mut want)?;
370                ensure!(ic_core::ct::verify(&want, &got), SelfTestFailed, $id);
371                Ok(())
372            }
373        }
374    };
375}
376
377sha3_hash!(
378    Sha3_224,
379    "sha3-224",
380    "SHA3-224",
381    28,
382    "e642824c3f8cf24ad09234ee7d3c766fc9a3a5168d0c94ad73b46fdf"
383);
384sha3_hash!(
385    Sha3_256,
386    "sha3-256",
387    "SHA3-256",
388    32,
389    "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"
390);
391sha3_hash!(
392    Sha3_384,
393    "sha3-384",
394    "SHA3-384",
395    48,
396    "ec01498288516fc926459f58e2c6ad8df9b473cb0fc08c2596da7cf0e49be4b298d88cea927ac7f539f1edf228376d25"
397);
398sha3_hash!(
399    Sha3_512,
400    "sha3-512",
401    "SHA3-512",
402    64,
403    "b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0"
404);
405
406shake!(
407    Shake128,
408    "shake128",
409    "SHAKE128",
410    128,
411    "5881092dd818bf5cf8a3ddb793fbcba74097d5c526a6d35f97b83351940f2cc8"
412);
413shake!(
414    Shake256,
415    "shake256",
416    "SHAKE256",
417    256,
418    "483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739"
419);
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use ic_core::codec::hex;
425
426    #[test]
427    fn sha3_abc_vectors() {
428        assert_eq!(
429            hex(Sha3_224::digest(b"abc").as_ref()),
430            "e642824c3f8cf24ad09234ee7d3c766fc9a3a5168d0c94ad73b46fdf"
431        );
432        assert_eq!(
433            hex(Sha3_256::digest(b"abc").as_ref()),
434            "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"
435        );
436        assert_eq!(
437            hex(Sha3_384::digest(b"abc").as_ref()),
438            "ec01498288516fc926459f58e2c6ad8df9b473cb0fc08c2596da7cf0e49be4b298d88cea927ac7f539f1edf228376d25"
439        );
440        assert_eq!(
441            hex(Sha3_512::digest(b"abc").as_ref()),
442            "b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0"
443        );
444    }
445
446    #[test]
447    fn sha3_empty_vectors() {
448        assert_eq!(
449            hex(Sha3_256::digest(b"").as_ref()),
450            "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"
451        );
452        assert_eq!(
453            hex(Sha3_512::digest(b"").as_ref()),
454            "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
455        );
456    }
457
458    #[test]
459    fn shake_vectors() {
460        let mut out = [0u8; 32];
461        Shake128::xof(b"", &mut out);
462        assert_eq!(
463            hex(&out),
464            "7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26"
465        );
466        Shake256::xof(b"", &mut out);
467        assert_eq!(
468            hex(&out),
469            "46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f"
470        );
471    }
472
473    /// A long squeeze must agree with a short one on its prefix, proving the
474    /// sponge rate boundary is handled correctly.
475    #[test]
476    fn shake_long_squeeze_is_prefix_consistent() {
477        let mut short = [0u8; 16];
478        let mut long = [0u8; 512];
479        Shake128::xof(b"agentic", &mut short);
480        Shake128::xof(b"agentic", &mut long);
481        assert_eq!(&long[..16], &short[..]);
482    }
483
484    #[test]
485    fn streaming_matches_one_shot() {
486        let data: [u8; 400] = core::array::from_fn(|i| (i * 7) as u8);
487        for split in [0usize, 1, 135, 136, 137, 200, 400] {
488            let mut h = Sha3_256::new();
489            h.update(&data[..split]);
490            h.update(&data[split..]);
491            assert_eq!(h.finalize(), Sha3_256::digest(&data), "split at {split}");
492        }
493    }
494
495    #[test]
496    fn all_self_tests_pass() {
497        Sha3_224::self_test().unwrap();
498        Sha3_256::self_test().unwrap();
499        Sha3_384::self_test().unwrap();
500        Sha3_512::self_test().unwrap();
501        Shake128::self_test().unwrap();
502        Shake256::self_test().unwrap();
503    }
504
505    /// The table-driven permutation the straight-line one replaced.
506    ///
507    /// Transcribed unchanged from the previous revision, and the reason the
508    /// rewrite is checkable: it derives rho and pi by walking the lane cycle
509    /// out of RHO and PI, so agreeing with it means the twenty-four written-out
510    /// assignments say what those tables say.
511    fn keccak_f1600_by_lane_cycle(a: &mut [u64; 25]) {
512        for round in RC.iter().take(ROUNDS) {
513            let mut c = [0u64; 5];
514            for x in 0..5 {
515                c[x] = a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20];
516            }
517            for x in 0..5 {
518                let d = c[(x + 4) % 5] ^ c[(x + 1) % 5].rotate_left(1);
519                for y in 0..5 {
520                    a[x + 5 * y] ^= d;
521                }
522            }
523            let mut last = a[1];
524            for i in 0..24 {
525                let j = PI[i];
526                let tmp = a[j];
527                a[j] = last.rotate_left(RHO[i]);
528                last = tmp;
529            }
530            for y in 0..5 {
531                let row = [
532                    a[5 * y],
533                    a[5 * y + 1],
534                    a[5 * y + 2],
535                    a[5 * y + 3],
536                    a[5 * y + 4],
537                ];
538                for x in 0..5 {
539                    a[5 * y + x] = row[x] ^ ((!row[(x + 1) % 5]) & row[(x + 2) % 5]);
540                }
541            }
542            a[0] ^= *round;
543        }
544    }
545
546    /// The straight-line rho and pi say what RHO and PI say.
547    ///
548    /// Arbitrary states, not just the ones a sponge reaches: a padded sponge
549    /// never presents a full-entropy state to the permutation, so testing only
550    /// through `Sha3_256` would leave most lane patterns unexercised, and a
551    /// misplaced rotation is exactly the kind of error that hides in the lanes
552    /// nobody drives.
553    #[test]
554    fn rho_and_pi_agree_with_the_lane_cycle() {
555        let mut state = 0x0123_4567_89ab_cdefu64;
556        let mut next = || {
557            state ^= state >> 12;
558            state ^= state << 25;
559            state ^= state >> 27;
560            state.wrapping_mul(0x2545_f491_4f6c_dd1d)
561        };
562        for case in 0..2_000 {
563            let mut a = [0u64; 25];
564            for lane in a.iter_mut() {
565                *lane = next();
566            }
567            // One lane at a time as well, so a rotation landing on the wrong
568            // lane cannot be masked by every other lane also being non-zero.
569            if case < 25 {
570                a = [0u64; 25];
571                a[case] = 0x8000_0000_0000_0001;
572            }
573            let mut want = a;
574            keccak_f1600_by_lane_cycle(&mut want);
575            let mut got = a;
576            keccak_f1600(&mut got);
577            assert_eq!(got, want, "permutations disagree on state {a:?}");
578        }
579    }
580
581    /// Straight-line against lane-cycle, in one process, alternating.
582    ///
583    /// Ignored: a measurement, not an assertion. Run it with
584    /// `cargo test -p ic-hash --release -- --ignored --nocapture permutation_ab`.
585    ///
586    /// Both forms are called from the same binary in the same loop, taking the
587    /// best of many alternating batches, because this machine has other work on
588    /// it and two separate benchmark runs minutes apart measure the other work
589    /// as much as this code. An earlier attempt to compare across runs had
590    /// RustCrypto's own SHA3 moving 616 -> 425 MiB/s between them, untouched.
591    #[test]
592    #[ignore = "diagnostic, not a test"]
593    fn permutation_ab() {
594        use std::time::Instant;
595        let mut seed = 0x1234_5678_9abc_def0u64;
596        let mut a = [0u64; 25];
597        for lane in a.iter_mut() {
598            seed ^= seed >> 12;
599            seed ^= seed << 25;
600            seed ^= seed >> 27;
601            *lane = seed.wrapping_mul(0x2545_f491_4f6c_dd1d);
602        }
603
604        let n = 20_000;
605        let (mut best_new, mut best_old) = (f64::INFINITY, f64::INFINITY);
606        for _ in 0..40 {
607            let mut s1 = a;
608            let t = Instant::now();
609            for _ in 0..n {
610                keccak_f1600(core::hint::black_box(&mut s1));
611            }
612            let e = t.elapsed().as_secs_f64() / n as f64 * 1e9;
613            best_new = best_new.min(e);
614
615            let mut s2 = a;
616            let t = Instant::now();
617            for _ in 0..n {
618                keccak_f1600_by_lane_cycle(core::hint::black_box(&mut s2));
619            }
620            let e = t.elapsed().as_secs_f64() / n as f64 * 1e9;
621            best_old = best_old.min(e);
622        }
623        println!(
624            "
625  keccak-f[1600] straight-line   {best_new:>8.1} ns"
626        );
627        println!("  keccak-f[1600] lane cycle     {best_old:>8.1} ns");
628        println!(
629            "  ratio                         {:>8.2}x",
630            best_old / best_new
631        );
632    }
633
634    /// The digest does not depend on how the input was handed over.
635    ///
636    /// `absorb` now has a block-aligned fast path and a byte-wise one, and
637    /// which of them runs depends entirely on the caller's chunking: the same
638    /// message delivered whole, in single bytes, or in awkward pieces has to
639    /// cross between them at different points. Feeding one buffer in every
640    /// chunking pattern below and demanding one answer is what holds the two
641    /// paths together. Lengths either side of the 136-byte rate, and either
642    /// side of two rates, so a block boundary falls inside a chunk as well as
643    /// on one.
644    #[test]
645    fn chunking_the_input_does_not_change_the_digest() {
646        use ic_core::traits::Digest;
647
648        for len in [0usize, 1, 7, 8, 9, 135, 136, 137, 271, 272, 273, 400] {
649            let msg: Vec<u8> = (0..len).map(|i| (i * 31 + 7) as u8).collect();
650            let want = Sha3_256::digest(&msg);
651
652            for chunk in [1usize, 2, 3, 7, 8, 17, 64, 135, 136, 137, 200] {
653                let mut h = Sha3_256::default();
654                for piece in msg.chunks(chunk.max(1)) {
655                    h.update(piece);
656                }
657                assert_eq!(
658                    h.finalize().as_ref(),
659                    want.as_ref(),
660                    "len {len} split into {chunk}-byte pieces"
661                );
662            }
663
664            // An uneven split, so the boundary is not at a regular stride.
665            if len > 3 {
666                let mut h = Sha3_256::default();
667                h.update(&msg[..1]);
668                h.update(&msg[1..len - 2]);
669                h.update(&msg[len - 2..]);
670                assert_eq!(
671                    h.finalize().as_ref(),
672                    want.as_ref(),
673                    "len {len} split unevenly"
674                );
675            }
676        }
677    }
678
679    /// The same, for the extendable-output side and its different rate.
680    #[test]
681    fn chunking_the_input_does_not_change_the_xof_output() {
682        use ic_core::traits::Xof;
683
684        for len in [0usize, 1, 167, 168, 169, 337, 500] {
685            let msg: Vec<u8> = (0..len).map(|i| (i * 17 + 3) as u8).collect();
686            let mut want = [0u8; 137];
687            {
688                let mut x = Shake128::default();
689                x.update(&msg);
690                x.finalize_xof(&mut want);
691            }
692            for chunk in [1usize, 5, 8, 64, 167, 168, 169] {
693                let mut x = Shake128::default();
694                for piece in msg.chunks(chunk) {
695                    x.update(piece);
696                }
697                let mut got = [0u8; 137];
698                x.finalize_xof(&mut got);
699                assert_eq!(got, want, "xof len {len} split into {chunk}-byte pieces");
700            }
701        }
702    }
703}