Skip to main content

oxicrypto_hash/
parallelhash.rs

1// ── ParallelHash128 / ParallelHash256 (NIST SP 800-185 §6) ──────────────────
2//
3//! ParallelHash128 and ParallelHash256, the SHA-3 derived parallelisable hash
4//! functions defined in NIST SP 800-185 §6.
5//!
6//! ParallelHash is designed so that long messages can be hashed by processing
7//! fixed-size blocks independently. This module implements the construction
8//! **sequentially** (pure Rust, no threading dependency); the per-block chaining
9//! values are independent, so a future `parallel` feature could compute them on
10//! multiple threads while producing byte-identical output.
11//!
12//! # Construction (SP 800-185 §6.1 / §6.2)
13//!
14//! Given message `X`, block size `B` (bytes), customization `S`, and output
15//! length `L` (bits), split `X` into `n = ceil(len(X) / B)` blocks
16//! `X_0 ‖ X_1 ‖ … ‖ X_{n-1}` (the final block may be shorter than `B`). Then:
17//!
18//! ```text
19//! z = left_encode(B)
20//!   ‖ SHAKE128(X_0, 256) ‖ … ‖ SHAKE128(X_{n-1}, 256)   (256-bit CVs for 128)
21//!   ‖ right_encode(n)
22//!   ‖ right_encode(L)
23//! ParallelHash128(X, B, L, S) = cSHAKE128(z, L, "ParallelHash", S)
24//! ```
25//!
26//! ParallelHash256 is identical with SHAKE256 producing 512-bit (64-byte)
27//! chaining values and cSHAKE256 as the final compression. The XOF variants set
28//! `L = 0` in `right_encode(L)` and stream arbitrary-length output.
29//!
30//! The per-block hash is `cSHAKE128(X_i, 256, "", "")`, which (with empty
31//! function-name and empty customization) equals `SHAKE128(X_i, 256)`; this
32//! implementation uses [`crate::shake128`] / [`crate::shake256`] directly for
33//! those chaining values.
34//!
35//! # Examples
36//!
37//! ```
38//! use oxicrypto_hash::parallel_hash128;
39//!
40//! // NIST SP 800-185 ParallelHash128 Sample #1: B = 8, S = "", L = 256.
41//! let data: [u8; 24] = [
42//!     0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
43//!     0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
44//!     0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
45//! ];
46//! let mut out = [0u8; 32];
47//! parallel_hash128(&data, 8, b"", &mut out).unwrap();
48//! assert_eq!(
49//!     out,
50//!     [
51//!         0xBA, 0x8D, 0xC1, 0xD1, 0xD9, 0x79, 0x33, 0x1D,
52//!         0x3F, 0x81, 0x36, 0x03, 0xC6, 0x7F, 0x72, 0x60,
53//!         0x9A, 0xB5, 0xE4, 0x4B, 0x94, 0xA0, 0xB8, 0xF9,
54//!         0xAF, 0x46, 0x51, 0x44, 0x54, 0xA2, 0xB4, 0xF5,
55//!     ]
56//! );
57//! ```
58
59use alloc::vec::Vec;
60
61use oxicrypto_core::CryptoError;
62
63use crate::xof::{left_encode, right_encode};
64use crate::{cshake128, cshake256, shake128, shake256};
65
66/// Function-name string `N` used by every ParallelHash variant (SP 800-185 §6).
67const PARALLEL_HASH_NAME: &[u8] = b"ParallelHash";
68
69/// Chaining-value length for ParallelHash128 (256 bits = 32 bytes).
70const CV_LEN_128: usize = 32;
71
72/// Chaining-value length for ParallelHash256 (512 bits = 64 bytes).
73const CV_LEN_256: usize = 64;
74
75/// Build the `z` preimage (the cSHAKE input) shared by all ParallelHash variants.
76///
77/// `cv_len` selects the per-block chaining-value width (32 for the 128-bit
78/// strength, 64 for the 256-bit strength). `out_bits` is the encoded output
79/// length `L`: a positive value for the fixed-output variants, or `0` for the
80/// XOF variants per SP 800-185.
81///
82/// # Errors
83///
84/// Returns [`CryptoError::BadInput`] if `block_size` is `0`, or if a length
85/// computation overflows a `u64` (unreachable in practice).
86fn parallel_hash_z(
87    data: &[u8],
88    block_size: usize,
89    out_bits: u64,
90    cv_len: usize,
91) -> Result<Vec<u8>, CryptoError> {
92    if block_size == 0 {
93        return Err(CryptoError::BadInput);
94    }
95
96    // n = ceil(len(data) / block_size). For empty data, n = 0 (SP 800-185).
97    let n_blocks = data.len() / block_size + usize::from(!data.len().is_multiple_of(block_size));
98
99    // `left_encode(B)` encodes the block size in *bytes* (SP 800-185 §6), unlike
100    // `encode_string`, which encodes a bit length.
101    let mut z = left_encode(block_size as u64);
102
103    // Each block contributes a chaining value CV_i of `cv_len` bytes.
104    let mut cv = alloc::vec![0u8; cv_len];
105    for block in data.chunks(block_size) {
106        match cv_len {
107            CV_LEN_128 => shake128(block, &mut cv),
108            CV_LEN_256 => shake256(block, &mut cv),
109            // Unreachable: only the two constants above are ever passed in.
110            _ => return Err(CryptoError::Internal("invalid ParallelHash CV length")),
111        }
112        z.extend_from_slice(&cv);
113    }
114
115    z.extend_from_slice(&right_encode(n_blocks as u64));
116    z.extend_from_slice(&right_encode(out_bits));
117    Ok(z)
118}
119
120/// Compute the encoded output-length value `right_encode(out.len() * 8)`.
121///
122/// # Errors
123///
124/// Returns [`CryptoError::BadInput`] if `out.len() * 8` overflows a `u64`.
125fn out_len_bits(out: &[u8]) -> Result<u64, CryptoError> {
126    (out.len() as u64)
127        .checked_mul(8)
128        .ok_or(CryptoError::BadInput)
129}
130
131// ── ParallelHash128 (fixed output) ──────────────────────────────────────────
132
133/// ParallelHash128 with fixed output length (NIST SP 800-185 §6.1).
134///
135/// Hashes `data` in `block_size`-byte blocks with optional `customization`
136/// string `S`; the output length is `out.len()` bytes.
137///
138/// # Errors
139///
140/// Returns [`CryptoError::BadInput`] if `block_size` is `0` or a length
141/// computation overflows (unreachable in practice).
142pub fn parallel_hash128(
143    data: &[u8],
144    block_size: usize,
145    customization: &[u8],
146    out: &mut [u8],
147) -> Result<(), CryptoError> {
148    let out_bits = out_len_bits(out)?;
149    let z = parallel_hash_z(data, block_size, out_bits, CV_LEN_128)?;
150    cshake128(&z, PARALLEL_HASH_NAME, customization, out);
151    Ok(())
152}
153
154/// ParallelHash256 with fixed output length (NIST SP 800-185 §6.2).
155///
156/// Hashes `data` in `block_size`-byte blocks with optional `customization`
157/// string `S`; the output length is `out.len()` bytes.
158///
159/// # Errors
160///
161/// Returns [`CryptoError::BadInput`] if `block_size` is `0` or a length
162/// computation overflows (unreachable in practice).
163pub fn parallel_hash256(
164    data: &[u8],
165    block_size: usize,
166    customization: &[u8],
167    out: &mut [u8],
168) -> Result<(), CryptoError> {
169    let out_bits = out_len_bits(out)?;
170    let z = parallel_hash_z(data, block_size, out_bits, CV_LEN_256)?;
171    cshake256(&z, PARALLEL_HASH_NAME, customization, out);
172    Ok(())
173}
174
175// ── ParallelHashXOF variants ────────────────────────────────────────────────
176
177/// ParallelHash128 in extendable-output (XOF) mode (NIST SP 800-185 §6.3).
178///
179/// Identical to [`parallel_hash128`] except the encoded output length is `0`
180/// (`right_encode(0)`), making the output an arbitrary-length stream rather than
181/// being domain-separated by a fixed length. `out.len()` bytes are produced.
182///
183/// # Errors
184///
185/// Returns [`CryptoError::BadInput`] if `block_size` is `0`.
186pub fn parallel_hash128_xof(
187    data: &[u8],
188    block_size: usize,
189    customization: &[u8],
190    out: &mut [u8],
191) -> Result<(), CryptoError> {
192    let z = parallel_hash_z(data, block_size, 0, CV_LEN_128)?;
193    cshake128(&z, PARALLEL_HASH_NAME, customization, out);
194    Ok(())
195}
196
197/// ParallelHash256 in extendable-output (XOF) mode (NIST SP 800-185 §6.3).
198///
199/// Identical to [`parallel_hash256`] except the encoded output length is `0`.
200///
201/// # Errors
202///
203/// Returns [`CryptoError::BadInput`] if `block_size` is `0`.
204pub fn parallel_hash256_xof(
205    data: &[u8],
206    block_size: usize,
207    customization: &[u8],
208    out: &mut [u8],
209) -> Result<(), CryptoError> {
210    let z = parallel_hash_z(data, block_size, 0, CV_LEN_256)?;
211    cshake256(&z, PARALLEL_HASH_NAME, customization, out);
212    Ok(())
213}
214
215// ── Convenience struct wrappers ─────────────────────────────────────────────
216
217/// ParallelHash128 configured with a block size and customization string.
218///
219/// A small convenience wrapper over [`parallel_hash128`] /
220/// [`parallel_hash128_xof`] holding the parameters that stay fixed across calls.
221#[derive(Debug, Clone)]
222pub struct ParallelHash128 {
223    block_size: usize,
224    customization: Vec<u8>,
225}
226
227impl ParallelHash128 {
228    /// Create a ParallelHash128 with the given `block_size` (bytes) and
229    /// `customization` string `S`.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`CryptoError::BadInput`] if `block_size` is `0`.
234    pub fn new(block_size: usize, customization: &[u8]) -> Result<Self, CryptoError> {
235        if block_size == 0 {
236            return Err(CryptoError::BadInput);
237        }
238        Ok(Self {
239            block_size,
240            customization: customization.to_vec(),
241        })
242    }
243
244    /// Hash `data` with fixed output length `out.len()` bytes.
245    ///
246    /// # Errors
247    ///
248    /// Propagates errors from [`parallel_hash128`].
249    pub fn hash(&self, data: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
250        parallel_hash128(data, self.block_size, &self.customization, out)
251    }
252
253    /// Hash `data` in XOF mode, producing `out.len()` bytes.
254    ///
255    /// # Errors
256    ///
257    /// Propagates errors from [`parallel_hash128_xof`].
258    pub fn hash_xof(&self, data: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
259        parallel_hash128_xof(data, self.block_size, &self.customization, out)
260    }
261}
262
263/// ParallelHash256 configured with a block size and customization string.
264///
265/// A small convenience wrapper over [`parallel_hash256`] /
266/// [`parallel_hash256_xof`].
267#[derive(Debug, Clone)]
268pub struct ParallelHash256 {
269    block_size: usize,
270    customization: Vec<u8>,
271}
272
273impl ParallelHash256 {
274    /// Create a ParallelHash256 with the given `block_size` (bytes) and
275    /// `customization` string `S`.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`CryptoError::BadInput`] if `block_size` is `0`.
280    pub fn new(block_size: usize, customization: &[u8]) -> Result<Self, CryptoError> {
281        if block_size == 0 {
282            return Err(CryptoError::BadInput);
283        }
284        Ok(Self {
285            block_size,
286            customization: customization.to_vec(),
287        })
288    }
289
290    /// Hash `data` with fixed output length `out.len()` bytes.
291    ///
292    /// # Errors
293    ///
294    /// Propagates errors from [`parallel_hash256`].
295    pub fn hash(&self, data: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
296        parallel_hash256(data, self.block_size, &self.customization, out)
297    }
298
299    /// Hash `data` in XOF mode, producing `out.len()` bytes.
300    ///
301    /// # Errors
302    ///
303    /// Propagates errors from [`parallel_hash256_xof`].
304    pub fn hash_xof(&self, data: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
305        parallel_hash256_xof(data, self.block_size, &self.customization, out)
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn parallel_hash128_block_size_zero_rejected() {
315        let mut out = [0u8; 32];
316        assert_eq!(
317            parallel_hash128(b"data", 0, b"", &mut out).unwrap_err(),
318            CryptoError::BadInput
319        );
320    }
321
322    #[test]
323    fn parallel_hash256_block_size_zero_rejected() {
324        let mut out = [0u8; 64];
325        assert_eq!(
326            parallel_hash256(b"data", 0, b"", &mut out).unwrap_err(),
327            CryptoError::BadInput
328        );
329    }
330
331    #[test]
332    fn struct_new_block_size_zero_rejected() {
333        assert_eq!(
334            ParallelHash128::new(0, b"").unwrap_err(),
335            CryptoError::BadInput
336        );
337        assert_eq!(
338            ParallelHash256::new(0, b"").unwrap_err(),
339            CryptoError::BadInput
340        );
341    }
342
343    #[test]
344    fn struct_matches_free_function_128() {
345        let data = b"some parallel hash payload spanning multiple blocks";
346        let mut a = [0u8; 32];
347        let mut b = [0u8; 32];
348        parallel_hash128(data, 8, b"cust", &mut a).unwrap();
349        ParallelHash128::new(8, b"cust")
350            .unwrap()
351            .hash(data, &mut b)
352            .unwrap();
353        assert_eq!(a, b, "struct API must equal free function (PH128)");
354    }
355
356    #[test]
357    fn struct_matches_free_function_256() {
358        let data = b"some parallel hash payload spanning multiple blocks";
359        let mut a = [0u8; 64];
360        let mut b = [0u8; 64];
361        parallel_hash256(data, 16, b"cust", &mut a).unwrap();
362        ParallelHash256::new(16, b"cust")
363            .unwrap()
364            .hash(data, &mut b)
365            .unwrap();
366        assert_eq!(a, b, "struct API must equal free function (PH256)");
367    }
368
369    #[test]
370    fn customization_changes_output_128() {
371        let data = b"abcdefghijklmnop";
372        let mut a = [0u8; 32];
373        let mut b = [0u8; 32];
374        parallel_hash128(data, 8, b"A", &mut a).unwrap();
375        parallel_hash128(data, 8, b"B", &mut b).unwrap();
376        assert_ne!(a, b, "different customization must change PH128 output");
377    }
378
379    #[test]
380    fn block_size_changes_output_128() {
381        // ParallelHash output depends on the block size (different CV layout).
382        let data = b"abcdefghijklmnopqrstuvwx";
383        let mut a = [0u8; 32];
384        let mut b = [0u8; 32];
385        parallel_hash128(data, 8, b"", &mut a).unwrap();
386        parallel_hash128(data, 12, b"", &mut b).unwrap();
387        assert_ne!(a, b, "different block size must change PH128 output");
388    }
389
390    #[test]
391    fn xof_prefix_consistency_128() {
392        // A longer XOF output must extend a shorter one (same parameters).
393        let data = b"xof prefix consistency check payload";
394        let mut short = [0u8; 32];
395        let mut long = [0u8; 80];
396        parallel_hash128_xof(data, 8, b"cust", &mut short).unwrap();
397        parallel_hash128_xof(data, 8, b"cust", &mut long).unwrap();
398        assert_eq!(
399            short,
400            long[..32],
401            "PH128 XOF: short output must prefix the longer one"
402        );
403    }
404
405    #[test]
406    fn xof_prefix_consistency_256() {
407        let data = b"xof prefix consistency check payload";
408        let mut short = [0u8; 64];
409        let mut long = [0u8; 160];
410        parallel_hash256_xof(data, 16, b"cust", &mut short).unwrap();
411        parallel_hash256_xof(data, 16, b"cust", &mut long).unwrap();
412        assert_eq!(
413            short,
414            long[..64],
415            "PH256 XOF: short output must prefix the longer one"
416        );
417    }
418
419    #[test]
420    fn xof_differs_from_fixed_128() {
421        // The fixed-output and XOF variants differ (right_encode(L) vs right_encode(0)).
422        let data = b"some data here";
423        let mut fixed = [0u8; 32];
424        let mut xof = [0u8; 32];
425        parallel_hash128(data, 8, b"", &mut fixed).unwrap();
426        parallel_hash128_xof(data, 8, b"", &mut xof).unwrap();
427        assert_ne!(fixed, xof, "fixed-output and XOF variants must differ");
428    }
429
430    #[test]
431    fn empty_input_is_defined_128() {
432        // n = 0 for empty input: z = left_encode(B) ‖ right_encode(0) ‖ right_encode(L).
433        let mut out = [0u8; 32];
434        parallel_hash128(b"", 8, b"", &mut out).unwrap();
435        assert!(
436            out.iter().any(|&x| x != 0),
437            "PH128 of empty input must be non-zero"
438        );
439    }
440
441    #[test]
442    fn partial_final_block_is_handled_128() {
443        // 20 bytes with B = 8 => blocks of 8, 8, 4 (last is partial).
444        let data: [u8; 20] = core::array::from_fn(|i| i as u8);
445        let mut out = [0u8; 32];
446        // Must not panic and must be deterministic.
447        parallel_hash128(&data, 8, b"", &mut out).unwrap();
448        let mut out2 = [0u8; 32];
449        parallel_hash128(&data, 8, b"", &mut out2).unwrap();
450        assert_eq!(out, out2, "PH128 with partial final block must be stable");
451    }
452}