Skip to main content

dryoc/classic/
crypto_xof.rs

1//! # Extendable-output functions
2//!
3//! Implements libsodium's `crypto_xof_shake128_*`, `crypto_xof_shake256_*`,
4//! `crypto_xof_turboshake128_*` and `crypto_xof_turboshake256_*` functions.
5//!
6//! An extendable-output function (XOF) hashes input of any length into
7//! output of any length. SHAKE is specified in FIPS 202; TurboSHAKE (RFC
8//! 9861) uses 12 Keccak rounds instead of 24 and is about twice as fast.
9//!
10//! Absorb input with `update`, then call `squeeze` as many times as needed:
11//! the calls continue one output stream. Once squeezing has started, `update`
12//! returns an error and leaves the state unchanged. `init_with_domain`
13//! selects a custom domain byte in `0x01..=0x7f`.
14//!
15//! libsodium documents the same rules but does not enforce them: it accepts
16//! any domain byte, and an `update` after squeezing returns `-1` after
17//! resetting the state and absorbing the input anyway. dryoc returns an
18//! error in both cases instead.
19//!
20//! ```
21//! use dryoc::classic::crypto_xof::*;
22//!
23//! let mut digest = [0u8; 32];
24//! crypto_xof_turboshake128(&mut digest, b"Arbitrary data to hash");
25//!
26//! let mut state = crypto_xof_shake256_init();
27//! crypto_xof_shake256_update(&mut state, b"Arbitrary data to hash").expect("update failed");
28//! let (mut key1, mut key2) = ([0u8; 32], [0u8; 32]);
29//! crypto_xof_shake256_squeeze(&mut state, &mut key1);
30//! crypto_xof_shake256_squeeze(&mut state, &mut key2);
31//! assert_ne!(key1, key2);
32//! ```
33
34use crate::error::Error;
35use crate::keccak::{RATE_128, RATE_256, ROUNDS_FULL, ROUNDS_TURBO};
36use crate::xof::XofCore;
37
38/// Generates the `*State` struct and the one-shot, `init`,
39/// `init_with_domain`, `update` and `squeeze` functions for one XOF.
40macro_rules! crypto_xof {
41    (
42        $(#[$state_meta:meta])*
43        state: $state:ident($rate:expr, $rounds:expr),
44        rustaceous: $xof:ty,
45        algorithm: $algo:literal,
46        oneshot: $oneshot:ident,
47        init: $init:ident,
48        init_with_domain: $init_with_domain:ident,
49        update: $update:ident,
50        squeeze: $squeeze:ident $(,)?
51    ) => {
52        $(#[$state_meta])*
53        #[derive(Clone)]
54        pub struct $state {
55            core: XofCore<$rate, $rounds>,
56        }
57
58        #[doc = concat!("Computes ", $algo, " of `input`, filling `output`.")]
59        pub fn $oneshot(output: &mut [u8], input: &[u8]) {
60            <$xof>::compute_into_bytes(output, input)
61        }
62
63        #[doc = concat!("Initializes ", $algo, " with the standard domain.")]
64        #[must_use]
65        pub fn $init() -> $state {
66            $state {
67                core: XofCore::new(),
68            }
69        }
70
71        #[doc = concat!("Initializes ", $algo, " with a custom `domain` byte.")]
72        ///
73        /// # Errors
74        ///
75        /// Returns [`Error::InvalidValue`] unless `domain` is in `0x01..=0x7f`.
76        pub fn $init_with_domain(domain: u8) -> Result<$state, Error> {
77            Ok($state {
78                core: XofCore::with_domain(domain)?,
79            })
80        }
81
82        #[doc = concat!("Absorbs `input` into the ", $algo, " `state`.")]
83        ///
84        /// # Errors
85        ///
86        /// Returns [`Error::InvalidState`] if squeezing has started; the
87        /// state is left unchanged.
88        pub fn $update(state: &mut $state, input: &[u8]) -> Result<(), Error> {
89            state.core.update(input)
90        }
91
92        #[doc = concat!(
93            "Fills `output` with the next bytes of the ", $algo, " output stream,\n",
94            "finishing absorption on the first call."
95        )]
96        pub fn $squeeze(state: &mut $state, output: &mut [u8]) {
97            state.core.squeeze(output)
98        }
99    };
100}
101
102crypto_xof! {
103    /// Incremental SHAKE128 state.
104    state: Shake128State(RATE_128, ROUNDS_FULL),
105    rustaceous: crate::xof::Shake128,
106    algorithm: "SHAKE128",
107    oneshot: crypto_xof_shake128,
108    init: crypto_xof_shake128_init,
109    init_with_domain: crypto_xof_shake128_init_with_domain,
110    update: crypto_xof_shake128_update,
111    squeeze: crypto_xof_shake128_squeeze,
112}
113
114crypto_xof! {
115    /// Incremental SHAKE256 state.
116    state: Shake256State(RATE_256, ROUNDS_FULL),
117    rustaceous: crate::xof::Shake256,
118    algorithm: "SHAKE256",
119    oneshot: crypto_xof_shake256,
120    init: crypto_xof_shake256_init,
121    init_with_domain: crypto_xof_shake256_init_with_domain,
122    update: crypto_xof_shake256_update,
123    squeeze: crypto_xof_shake256_squeeze,
124}
125
126crypto_xof! {
127    /// Incremental TurboSHAKE128 state.
128    state: TurboShake128State(RATE_128, ROUNDS_TURBO),
129    rustaceous: crate::xof::TurboShake128,
130    algorithm: "TurboSHAKE128",
131    oneshot: crypto_xof_turboshake128,
132    init: crypto_xof_turboshake128_init,
133    init_with_domain: crypto_xof_turboshake128_init_with_domain,
134    update: crypto_xof_turboshake128_update,
135    squeeze: crypto_xof_turboshake128_squeeze,
136}
137
138crypto_xof! {
139    /// Incremental TurboSHAKE256 state.
140    state: TurboShake256State(RATE_256, ROUNDS_TURBO),
141    rustaceous: crate::xof::TurboShake256,
142    algorithm: "TurboSHAKE256",
143    oneshot: crypto_xof_turboshake256,
144    init: crypto_xof_turboshake256_init,
145    init_with_domain: crypto_xof_turboshake256_init_with_domain,
146    update: crypto_xof_turboshake256_update,
147    squeeze: crypto_xof_turboshake256_squeeze,
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::error::ErrorContext;
154    #[cfg(dryoc_native_tests)]
155    use crate::test_prelude::*;
156    use crate::xof::test_vectors::*;
157
158    /// Checks one XOF's Classic functions against its vectors: the one-shot
159    /// function for the standard domain, and `init_with_domain`, one byte
160    /// per `update` with empty updates around each, and odd-sized squeezes.
161    macro_rules! check_known_answers {
162        ($vectors:expr, $oneshot:ident, $init_with_domain:ident, $update:ident, $squeeze:ident) => {
163            for Vector {
164                message,
165                domain,
166                skip,
167                output,
168            } in $vectors
169            {
170                let len = message.len();
171                if domain == 0x1f && skip == 0 {
172                    let mut out = vec![0u8; output.len()];
173                    $oneshot(&mut out, &message);
174                    assert_eq!(out, output, "one-shot len {len}");
175                }
176
177                let mut state = $init_with_domain(domain).expect("valid domain");
178                if len <= 1024 {
179                    $update(&mut state, b"").expect("update failed");
180                    for byte in &message {
181                        $update(&mut state, core::slice::from_ref(byte)).expect("update failed");
182                        $update(&mut state, b"").expect("update failed");
183                    }
184                } else {
185                    $update(&mut state, &message).expect("update failed");
186                }
187                let mut all = vec![0u8; skip + output.len()];
188                for chunk in all.chunks_mut(7) {
189                    $squeeze(&mut state, chunk);
190                }
191                assert_eq!(&all[skip..], output, "streamed len {len}");
192            }
193        };
194    }
195
196    #[test]
197    fn test_crypto_xof_shake128_known_answers() {
198        check_known_answers!(
199            shake128(),
200            crypto_xof_shake128,
201            crypto_xof_shake128_init_with_domain,
202            crypto_xof_shake128_update,
203            crypto_xof_shake128_squeeze
204        );
205    }
206
207    #[test]
208    fn test_crypto_xof_shake256_known_answers() {
209        check_known_answers!(
210            shake256(),
211            crypto_xof_shake256,
212            crypto_xof_shake256_init_with_domain,
213            crypto_xof_shake256_update,
214            crypto_xof_shake256_squeeze
215        );
216    }
217
218    #[test]
219    fn test_crypto_xof_turboshake128_known_answers() {
220        check_known_answers!(
221            turboshake128(),
222            crypto_xof_turboshake128,
223            crypto_xof_turboshake128_init_with_domain,
224            crypto_xof_turboshake128_update,
225            crypto_xof_turboshake128_squeeze
226        );
227    }
228
229    #[test]
230    fn test_crypto_xof_turboshake256_known_answers() {
231        check_known_answers!(
232            turboshake256(),
233            crypto_xof_turboshake256,
234            crypto_xof_turboshake256_init_with_domain,
235            crypto_xof_turboshake256_update,
236            crypto_xof_turboshake256_squeeze
237        );
238    }
239
240    /// `update` after `squeeze` is rejected without disturbing the output
241    /// stream, and `init` equals `init_with_domain` with the standard domain.
242    #[test]
243    fn test_update_after_squeeze_is_rejected() {
244        let mut state = crypto_xof_turboshake256_init();
245        crypto_xof_turboshake256_update(&mut state, b"abc").expect("update failed");
246        let mut first = [0u8; 16];
247        crypto_xof_turboshake256_squeeze(&mut state, &mut first);
248        assert!(matches!(
249            crypto_xof_turboshake256_update(&mut state, b"more"),
250            Err(Error::InvalidState {
251                context: ErrorContext::Xof
252            })
253        ));
254        let mut rest = [0u8; 48];
255        crypto_xof_turboshake256_squeeze(&mut state, &mut rest);
256
257        let mut expected = [0u8; 64];
258        crypto_xof_turboshake256(&mut expected, b"abc");
259        assert_eq!([&first[..], &rest[..]].concat(), expected);
260
261        assert!(crypto_xof_shake128_init_with_domain(0x80).is_err());
262        assert!(crypto_xof_shake128_init_with_domain(0x00).is_err());
263    }
264
265    /// Each XOF against libsodium: the one-shot function over input lengths
266    /// around the rate for output lengths up to past two blocks; the same
267    /// input absorbed in odd-sized chunks (with empty updates between) and
268    /// squeezed in uneven pieces on both sides; and custom domains through
269    /// `init_with_domain`. (libsodium accepts any domain byte, where this
270    /// crate rejects those outside `0x01..=0x7f`, so only valid domains are
271    /// compared.)
272    #[cfg(dryoc_native_tests)]
273    #[test]
274    fn test_crypto_xof_matches_libsodium() {
275        use crate::keccak::{RATE_128, RATE_256};
276        use crate::native_test_util as sodium;
277
278        fn pattern(len: usize) -> Vec<u8> {
279            (0..len as u32).map(|i| (i * 31 % 251) as u8).collect()
280        }
281
282        macro_rules! check {
283            (
284                $rate:expr,
285                $oneshot:ident,
286                $init:ident,
287                $init_with_domain:ident,
288                $update:ident,
289                $squeeze:ident,
290                $theirs:ident
291            ) => {
292                let rate = $rate;
293                let input_lens = [0, 1, rate - 1, rate, rate + 1, 2 * rate + 3];
294                let output_lens = [1, 32, rate - 1, rate, rate + 1, 2 * rate + 5, 3 * rate + 1];
295                for len in input_lens {
296                    let message = pattern(len);
297                    for out_len in output_lens {
298                        let mut ours = vec![0u8; out_len];
299                        $oneshot(&mut ours, &message);
300                        assert_eq!(
301                            ours,
302                            sodium::$oneshot(&message, out_len),
303                            "one-shot len {len}, output {out_len}"
304                        );
305                    }
306
307                    let squeezes = [1, 7, rate - 1, rate + 3, 2 * rate + 1];
308                    for domain in [None, Some(0x01), Some(0x07), Some(0x7f)] {
309                        let (mut ours, mut theirs) = match domain {
310                            None => ($init(), sodium::$theirs::new()),
311                            Some(domain) => (
312                                $init_with_domain(domain).expect("valid domain"),
313                                sodium::$theirs::with_domain(domain).expect("valid domain"),
314                            ),
315                        };
316                        // Pieces of 7 bytes, and 5 at the end of every 61.
317                        for chunk in message.chunks(61) {
318                            for piece in chunk.chunks(7) {
319                                $update(&mut ours, piece).expect("update failed");
320                                theirs.update(piece);
321                                $update(&mut ours, b"").expect("update failed");
322                                theirs.update(b"");
323                            }
324                        }
325                        let mut stream = Vec::new();
326                        for squeeze_len in squeezes {
327                            let mut piece = vec![0u8; squeeze_len];
328                            $squeeze(&mut ours, &mut piece);
329                            assert_eq!(
330                                piece,
331                                theirs.squeeze(squeeze_len),
332                                "len {len}, domain {domain:?}, squeeze {squeeze_len}"
333                            );
334                            stream.extend_from_slice(&piece);
335                        }
336                        if domain.is_none() {
337                            assert_eq!(
338                                stream,
339                                sodium::$oneshot(&message, stream.len()),
340                                "len {len}: squeezes continue the one-shot stream"
341                            );
342                        }
343                    }
344                }
345            };
346        }
347
348        check!(
349            RATE_128,
350            crypto_xof_shake128,
351            crypto_xof_shake128_init,
352            crypto_xof_shake128_init_with_domain,
353            crypto_xof_shake128_update,
354            crypto_xof_shake128_squeeze,
355            XofShake128State
356        );
357        check!(
358            RATE_256,
359            crypto_xof_shake256,
360            crypto_xof_shake256_init,
361            crypto_xof_shake256_init_with_domain,
362            crypto_xof_shake256_update,
363            crypto_xof_shake256_squeeze,
364            XofShake256State
365        );
366        check!(
367            RATE_128,
368            crypto_xof_turboshake128,
369            crypto_xof_turboshake128_init,
370            crypto_xof_turboshake128_init_with_domain,
371            crypto_xof_turboshake128_update,
372            crypto_xof_turboshake128_squeeze,
373            XofTurboShake128State
374        );
375        check!(
376            RATE_256,
377            crypto_xof_turboshake256,
378            crypto_xof_turboshake256_init,
379            crypto_xof_turboshake256_init_with_domain,
380            crypto_xof_turboshake256_update,
381            crypto_xof_turboshake256_squeeze,
382            XofTurboShake256State
383        );
384    }
385}