krypteia-tessera 0.2.0

Shared pure-Rust hash, XOF, MAC and KDF primitives for the krypteia workspace: SHA-1/2/3, Keccak, SHAKE/cSHAKE, RIPEMD-160, BLAKE2/BLAKE3, HMAC, SP 800-185 (KMAC/TupleHash/ParallelHash), the KDF family (HKDF, PBKDF2, SP 800-108, scrypt, Argon2), MGF1 and CRC-16/32. no_std, zero runtime dependencies.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Cédric Mesnil <cslashm@pm.me>

//! Keyed and derived hash functions from NIST SP 800-185: KMAC,
//! TupleHash and ParallelHash (and their XOF variants).
//!
//! All three families are thin constructions layered on cSHAKE128 /
//! cSHAKE256 ([`crate::sha3`]). No Keccak or cSHAKE code is
//! re-implemented here — this module only performs the SP 800-185
//! input encoding (`encode_string`, `left_encode`, `right_encode`,
//! `bytepad`) and dispatches to the shared sponge.
//!
//! # Side-channel posture
//!
//! Keccak-f is data-oblivious (no data-dependent branch, index, or
//! memory access), so the whole sponge is constant-time by
//! construction. Key absorption in KMAC is a straight `bytepad ‖
//! absorb`: the key length changes the number of absorbed lanes, but
//! that count is a *public* parameter of the API, not secret-derived,
//! and there is no secret-dependent branch or table index anywhere in
//! this module.
//!
//! The MAC-tag **comparison** is deliberately **not** performed here:
//! [`kmac128`] & friends only *produce* the tag into a caller buffer.
//! Verifying a tag against an expected value must go through
//! `silentops::ct_eq` in the caller — a byte-wise `==` on the tag is a
//! timing oracle and is out of scope for this crate.

use crate::sha3::{CShake128, CShake256, bytepad, left_encode, right_encode};
use alloc::vec::Vec;

// ============================================================
// KMAC (NIST SP 800-185 §4)
// ============================================================
//
// KMAC128(K, X, L, S) =
//     cSHAKE128( bytepad(encode_string(K), 168)
//                || X || right_encode(L),
//                L, "KMAC", S )
// KMAC256 is identical over cSHAKE256 (rate 136).
// The *XOF* variants replace right_encode(L) with right_encode(0) and
// squeeze an arbitrary (caller-chosen) number of octets.

/// Absorb `bytepad(encode_string(K), RATE)` into a fresh cSHAKE keyed
/// with `N = "KMAC"` and customization `S`, then absorb `data`.
macro_rules! kmac_impl {
    ($fn:ident, $xof:ident, $cshake:ty, $doc:literal, $xdoc:literal) => {
        #[doc = $doc]
        ///
        /// `out.len()` is the requested output length `L` (in octets);
        /// this fixed-length variant binds `L` into the input via
        /// `right_encode(L*8)`. NIST SP 800-185 §4.3.
        pub fn $fn(key: &[u8], data: &[u8], custom: &[u8], out: &mut [u8]) {
            let mut c = <$cshake>::new(b"KMAC", custom);
            let key_block = bytepad(&encode_string(key), <$cshake>::RATE);
            c.update(&key_block);
            c.update(data);
            // right_encode is over the output length in BITS.
            c.update(&right_encode((out.len() as u64) * 8));
            c.squeeze(out);
        }

        #[doc = $xdoc]
        ///
        /// XOF variant: binds `right_encode(0)` instead of the output
        /// length, so `out.len()` does not affect earlier bytes.
        /// NIST SP 800-185 §4.3.1.
        pub fn $xof(key: &[u8], data: &[u8], custom: &[u8], out: &mut [u8]) {
            let mut c = <$cshake>::new(b"KMAC", custom);
            let key_block = bytepad(&encode_string(key), <$cshake>::RATE);
            c.update(&key_block);
            c.update(data);
            c.update(&right_encode(0));
            c.squeeze(out);
        }
    };
}

kmac_impl!(
    kmac128,
    kmac128_xof,
    CShake128,
    "KMAC128 keyed MAC (NIST SP 800-185 §4).",
    "KMAC128 in XOF mode (KMACXOF128, NIST SP 800-185 §4)."
);
kmac_impl!(
    kmac256,
    kmac256_xof,
    CShake256,
    "KMAC256 keyed MAC (NIST SP 800-185 §4).",
    "KMAC256 in XOF mode (KMACXOF256, NIST SP 800-185 §4)."
);

// ============================================================
// TupleHash (NIST SP 800-185 §5)
// ============================================================
//
// TupleHash128(X, L, S) =
//     cSHAKE128( encode_string(x_1) || ... || encode_string(x_n)
//                || right_encode(L),
//                L, "TupleHash", S )
// The encode_string wrapper on each element is what makes the hash
// unambiguous over the tuple structure (concatenation-attack safe).

macro_rules! tuplehash_impl {
    ($fn:ident, $xof:ident, $cshake:ty, $doc:literal, $xdoc:literal) => {
        #[doc = $doc]
        ///
        /// Each element of `tuple` is length-prefixed with
        /// `encode_string`, so the digest is unambiguous over the tuple
        /// structure. NIST SP 800-185 §5.3.
        pub fn $fn(tuple: &[&[u8]], custom: &[u8], out: &mut [u8]) {
            let mut c = <$cshake>::new(b"TupleHash", custom);
            for x in tuple {
                c.update(&encode_string(x));
            }
            c.update(&right_encode((out.len() as u64) * 8));
            c.squeeze(out);
        }

        #[doc = $xdoc]
        ///
        /// XOF variant: `right_encode(0)` in place of the output
        /// length. NIST SP 800-185 §5.3.1.
        pub fn $xof(tuple: &[&[u8]], custom: &[u8], out: &mut [u8]) {
            let mut c = <$cshake>::new(b"TupleHash", custom);
            for x in tuple {
                c.update(&encode_string(x));
            }
            c.update(&right_encode(0));
            c.squeeze(out);
        }
    };
}

tuplehash_impl!(
    tuplehash128,
    tuplehash128_xof,
    CShake128,
    "TupleHash128 (NIST SP 800-185 §5).",
    "TupleHashXOF128 (NIST SP 800-185 §5)."
);
tuplehash_impl!(
    tuplehash256,
    tuplehash256_xof,
    CShake256,
    "TupleHash256 (NIST SP 800-185 §5).",
    "TupleHashXOF256 (NIST SP 800-185 §5)."
);

// ============================================================
// ParallelHash (NIST SP 800-185 §6)
// ============================================================
//
// ParallelHash128(X, B, L, S):
//   1. split X into n blocks of B octets (last block may be short);
//   2. for each block b_i compute a chaining value
//         CV_i = cSHAKE128(b_i, 256, "", "")   [= SHAKE128, 32 octets]
//      (ParallelHash256 uses 512-bit / 64-octet CVs from cSHAKE256);
//   3. z = left_encode(B) || CV_1 || ... || CV_n
//            || right_encode(n) || right_encode(L);
//   4. output = cSHAKE128(z, L, "ParallelHash", S).
// The per-block hash uses cSHAKE with empty N and S — i.e. plain
// SHAKE of the matching width. The XOF variant uses right_encode(0)
// for the final length field. NIST SP 800-185 §6.3.

/// Chaining-value length in octets: 256 bits for the 128 variant,
/// 512 bits for the 256 variant.
const PARALLELHASH_CV_128: usize = 32;
const PARALLELHASH_CV_256: usize = 64;

macro_rules! parallelhash_impl {
    ($fn:ident, $xof:ident, $cshake:ty, $cv:expr, $doc:literal, $xdoc:literal) => {
        #[doc = $doc]
        ///
        /// `block_size` is `B` in octets and must be non-zero. Each
        /// `B`-octet block is compressed to a
        #[doc = concat!(stringify!($cv), "-octet chaining value.")]
        /// NIST SP 800-185 §6.3.
        pub fn $fn(data: &[u8], block_size: usize, custom: &[u8], out: &mut [u8]) {
            let z = parallelhash_inner::<$cshake>(data, block_size, $cv);
            let mut c = <$cshake>::new(b"ParallelHash", custom);
            c.update(&z);
            c.update(&right_encode((out.len() as u64) * 8));
            c.squeeze(out);
        }

        #[doc = $xdoc]
        ///
        /// XOF variant: `right_encode(0)` for the final length field.
        /// NIST SP 800-185 §6.3.1.
        pub fn $xof(data: &[u8], block_size: usize, custom: &[u8], out: &mut [u8]) {
            let z = parallelhash_inner::<$cshake>(data, block_size, $cv);
            let mut c = <$cshake>::new(b"ParallelHash", custom);
            c.update(&z);
            c.update(&right_encode(0));
            c.squeeze(out);
        }
    };
}

parallelhash_impl!(
    parallelhash128,
    parallelhash128_xof,
    CShake128,
    PARALLELHASH_CV_128,
    "ParallelHash128 (NIST SP 800-185 §6).",
    "ParallelHashXOF128 (NIST SP 800-185 §6)."
);
parallelhash_impl!(
    parallelhash256,
    parallelhash256_xof,
    CShake256,
    PARALLELHASH_CV_256,
    "ParallelHash256 (NIST SP 800-185 §6).",
    "ParallelHashXOF256 (NIST SP 800-185 §6)."
);

// ============================================================
// Encoding helpers
// ============================================================

/// `encode_string(S)` = `left_encode(len_in_bits(S)) || S`
/// (NIST SP 800-185 §2.3.2). Returned as an owned buffer so the
/// caller can `update` it in one shot.
fn encode_string(s: &[u8]) -> Vec<u8> {
    let mut out = left_encode((s.len() as u64) * 8);
    out.extend_from_slice(s);
    out
}

/// Build the ParallelHash intermediate string `z` (NIST SP 800-185
/// §6.3, steps 1-3). Generic over the cShake width used for the
/// per-block chaining values, which is instantiated with empty
/// `N`/`S` (i.e. plain SHAKE).
///
/// `block_size` must be non-zero; `cv_len` is 32 (128) or 64 (256).
fn parallelhash_inner<C: CShakeBlock>(data: &[u8], block_size: usize, cv_len: usize) -> Vec<u8> {
    assert!(block_size != 0, "ParallelHash block size must be non-zero");
    let mut z = left_encode(block_size as u64);
    let n_blocks = data.len().div_ceil(block_size);
    for chunk in data.chunks(block_size) {
        // CV_i = SHAKE(chunk, cv_len) via cSHAKE with empty N, S.
        let mut cv = alloc::vec![0u8; cv_len];
        let mut c = C::new_bare();
        c.update_bare(chunk);
        c.squeeze_bare(&mut cv);
        z.extend_from_slice(&cv);
    }
    z.extend_from_slice(&right_encode(n_blocks as u64));
    z
}

/// Minimal trait to reuse the two cSHAKE widths for the per-block
/// chaining-value hash without duplicating the loop. `new_bare` builds
/// a cSHAKE with empty `N`/`S`, which reduces to plain SHAKE of the
/// matching width (NIST SP 800-185 §6.3).
trait CShakeBlock: Sized {
    fn new_bare() -> Self;
    fn update_bare(&mut self, data: &[u8]);
    fn squeeze_bare(&mut self, out: &mut [u8]);
}

macro_rules! cshake_block {
    ($t:ty) => {
        impl CShakeBlock for $t {
            fn new_bare() -> Self {
                <$t>::new(b"", b"")
            }
            fn update_bare(&mut self, data: &[u8]) {
                self.update(data);
            }
            fn squeeze_bare(&mut self, out: &mut [u8]) {
                self.squeeze(out);
            }
        }
    };
}

cshake_block!(CShake128);
cshake_block!(CShake256);

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::vec;

    // Smoke KAT: KMAC128 Sample #1 (NIST SP 800-185 KMAC_samples).
    // Key = 0x40..0x5F, Data = 00010203, S = "", L = 256. The exhaustive
    // SP 800-185 sample set and the NIST ACVP corpora (cSHAKE, KMAC incl.
    // over-rate keys, TupleHash, ParallelHash) live in the integration
    // suite `tests/sp800_185.rs`.
    #[test]
    fn kmac128_smoke() {
        let key: Vec<u8> = (0x40u8..=0x5F).collect();
        let data = [0x00u8, 0x01, 0x02, 0x03];
        let expected = [
            0xe5, 0x78, 0x0b, 0x0d, 0x3e, 0xa6, 0xf7, 0xd3, 0xa4, 0x29, 0xc5, 0x70, 0x6a, 0xa4, 0x3a, 0x00, 0xfa, 0xdb,
            0xd7, 0xd4, 0x96, 0x28, 0x83, 0x9e, 0x31, 0x87, 0x24, 0x3f, 0x45, 0x6e, 0xe1, 0x4e,
        ];
        let mut out = vec![0u8; 32];
        kmac128(&key, &data, b"", &mut out);
        assert_eq!(out, expected);
    }
}