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}