use alloc::vec::Vec;
use oxicrypto_core::CryptoError;
use crate::xof::{left_encode, right_encode};
use crate::{cshake128, cshake256, shake128, shake256};
const PARALLEL_HASH_NAME: &[u8] = b"ParallelHash";
const CV_LEN_128: usize = 32;
const CV_LEN_256: usize = 64;
fn parallel_hash_z(
data: &[u8],
block_size: usize,
out_bits: u64,
cv_len: usize,
) -> Result<Vec<u8>, CryptoError> {
if block_size == 0 {
return Err(CryptoError::BadInput);
}
let n_blocks = data.len() / block_size + usize::from(!data.len().is_multiple_of(block_size));
let mut z = left_encode(block_size as u64);
let mut cv = alloc::vec![0u8; cv_len];
for block in data.chunks(block_size) {
match cv_len {
CV_LEN_128 => shake128(block, &mut cv),
CV_LEN_256 => shake256(block, &mut cv),
_ => return Err(CryptoError::Internal("invalid ParallelHash CV length")),
}
z.extend_from_slice(&cv);
}
z.extend_from_slice(&right_encode(n_blocks as u64));
z.extend_from_slice(&right_encode(out_bits));
Ok(z)
}
fn out_len_bits(out: &[u8]) -> Result<u64, CryptoError> {
(out.len() as u64)
.checked_mul(8)
.ok_or(CryptoError::BadInput)
}
pub fn parallel_hash128(
data: &[u8],
block_size: usize,
customization: &[u8],
out: &mut [u8],
) -> Result<(), CryptoError> {
let out_bits = out_len_bits(out)?;
let z = parallel_hash_z(data, block_size, out_bits, CV_LEN_128)?;
cshake128(&z, PARALLEL_HASH_NAME, customization, out);
Ok(())
}
pub fn parallel_hash256(
data: &[u8],
block_size: usize,
customization: &[u8],
out: &mut [u8],
) -> Result<(), CryptoError> {
let out_bits = out_len_bits(out)?;
let z = parallel_hash_z(data, block_size, out_bits, CV_LEN_256)?;
cshake256(&z, PARALLEL_HASH_NAME, customization, out);
Ok(())
}
pub fn parallel_hash128_xof(
data: &[u8],
block_size: usize,
customization: &[u8],
out: &mut [u8],
) -> Result<(), CryptoError> {
let z = parallel_hash_z(data, block_size, 0, CV_LEN_128)?;
cshake128(&z, PARALLEL_HASH_NAME, customization, out);
Ok(())
}
pub fn parallel_hash256_xof(
data: &[u8],
block_size: usize,
customization: &[u8],
out: &mut [u8],
) -> Result<(), CryptoError> {
let z = parallel_hash_z(data, block_size, 0, CV_LEN_256)?;
cshake256(&z, PARALLEL_HASH_NAME, customization, out);
Ok(())
}
#[derive(Debug, Clone)]
pub struct ParallelHash128 {
block_size: usize,
customization: Vec<u8>,
}
impl ParallelHash128 {
pub fn new(block_size: usize, customization: &[u8]) -> Result<Self, CryptoError> {
if block_size == 0 {
return Err(CryptoError::BadInput);
}
Ok(Self {
block_size,
customization: customization.to_vec(),
})
}
pub fn hash(&self, data: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
parallel_hash128(data, self.block_size, &self.customization, out)
}
pub fn hash_xof(&self, data: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
parallel_hash128_xof(data, self.block_size, &self.customization, out)
}
}
#[derive(Debug, Clone)]
pub struct ParallelHash256 {
block_size: usize,
customization: Vec<u8>,
}
impl ParallelHash256 {
pub fn new(block_size: usize, customization: &[u8]) -> Result<Self, CryptoError> {
if block_size == 0 {
return Err(CryptoError::BadInput);
}
Ok(Self {
block_size,
customization: customization.to_vec(),
})
}
pub fn hash(&self, data: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
parallel_hash256(data, self.block_size, &self.customization, out)
}
pub fn hash_xof(&self, data: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
parallel_hash256_xof(data, self.block_size, &self.customization, out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parallel_hash128_block_size_zero_rejected() {
let mut out = [0u8; 32];
assert_eq!(
parallel_hash128(b"data", 0, b"", &mut out).unwrap_err(),
CryptoError::BadInput
);
}
#[test]
fn parallel_hash256_block_size_zero_rejected() {
let mut out = [0u8; 64];
assert_eq!(
parallel_hash256(b"data", 0, b"", &mut out).unwrap_err(),
CryptoError::BadInput
);
}
#[test]
fn struct_new_block_size_zero_rejected() {
assert_eq!(
ParallelHash128::new(0, b"").unwrap_err(),
CryptoError::BadInput
);
assert_eq!(
ParallelHash256::new(0, b"").unwrap_err(),
CryptoError::BadInput
);
}
#[test]
fn struct_matches_free_function_128() {
let data = b"some parallel hash payload spanning multiple blocks";
let mut a = [0u8; 32];
let mut b = [0u8; 32];
parallel_hash128(data, 8, b"cust", &mut a).unwrap();
ParallelHash128::new(8, b"cust")
.unwrap()
.hash(data, &mut b)
.unwrap();
assert_eq!(a, b, "struct API must equal free function (PH128)");
}
#[test]
fn struct_matches_free_function_256() {
let data = b"some parallel hash payload spanning multiple blocks";
let mut a = [0u8; 64];
let mut b = [0u8; 64];
parallel_hash256(data, 16, b"cust", &mut a).unwrap();
ParallelHash256::new(16, b"cust")
.unwrap()
.hash(data, &mut b)
.unwrap();
assert_eq!(a, b, "struct API must equal free function (PH256)");
}
#[test]
fn customization_changes_output_128() {
let data = b"abcdefghijklmnop";
let mut a = [0u8; 32];
let mut b = [0u8; 32];
parallel_hash128(data, 8, b"A", &mut a).unwrap();
parallel_hash128(data, 8, b"B", &mut b).unwrap();
assert_ne!(a, b, "different customization must change PH128 output");
}
#[test]
fn block_size_changes_output_128() {
let data = b"abcdefghijklmnopqrstuvwx";
let mut a = [0u8; 32];
let mut b = [0u8; 32];
parallel_hash128(data, 8, b"", &mut a).unwrap();
parallel_hash128(data, 12, b"", &mut b).unwrap();
assert_ne!(a, b, "different block size must change PH128 output");
}
#[test]
fn xof_prefix_consistency_128() {
let data = b"xof prefix consistency check payload";
let mut short = [0u8; 32];
let mut long = [0u8; 80];
parallel_hash128_xof(data, 8, b"cust", &mut short).unwrap();
parallel_hash128_xof(data, 8, b"cust", &mut long).unwrap();
assert_eq!(
short,
long[..32],
"PH128 XOF: short output must prefix the longer one"
);
}
#[test]
fn xof_prefix_consistency_256() {
let data = b"xof prefix consistency check payload";
let mut short = [0u8; 64];
let mut long = [0u8; 160];
parallel_hash256_xof(data, 16, b"cust", &mut short).unwrap();
parallel_hash256_xof(data, 16, b"cust", &mut long).unwrap();
assert_eq!(
short,
long[..64],
"PH256 XOF: short output must prefix the longer one"
);
}
#[test]
fn xof_differs_from_fixed_128() {
let data = b"some data here";
let mut fixed = [0u8; 32];
let mut xof = [0u8; 32];
parallel_hash128(data, 8, b"", &mut fixed).unwrap();
parallel_hash128_xof(data, 8, b"", &mut xof).unwrap();
assert_ne!(fixed, xof, "fixed-output and XOF variants must differ");
}
#[test]
fn empty_input_is_defined_128() {
let mut out = [0u8; 32];
parallel_hash128(b"", 8, b"", &mut out).unwrap();
assert!(
out.iter().any(|&x| x != 0),
"PH128 of empty input must be non-zero"
);
}
#[test]
fn partial_final_block_is_handled_128() {
let data: [u8; 20] = core::array::from_fn(|i| i as u8);
let mut out = [0u8; 32];
parallel_hash128(&data, 8, b"", &mut out).unwrap();
let mut out2 = [0u8; 32];
parallel_hash128(&data, 8, b"", &mut out2).unwrap();
assert_eq!(out, out2, "PH128 with partial final block must be stable");
}
}