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>

//! HKDF — HMAC-based Extract-and-Expand Key Derivation Function (RFC 5869).
//!
//! Two steps, generic over any [`Digest`] `H` (used as `HMAC-H`):
//!
//! 1. **Extract**: `PRK = HMAC-H(salt, IKM)` — concentrates possibly
//!    non-uniform input keying material `IKM` into a pseudorandom key `PRK`
//!    of `H::OUTPUT_LEN` octets. An empty `salt` is, per RFC 5869 §2.2,
//!    equivalent to `HashLen` zero octets (HMAC zero-pads the key to the
//!    block, so the two coincide here).
//! 2. **Expand**: `OKM = T(1) ‖ T(2) ‖ …` where `T(i) = HMAC-H(PRK,
//!    T(i-1) ‖ info ‖ i)` and `T(0)` is empty — stretches `PRK` to the
//!    requested output length (at most `255 · H::OUTPUT_LEN` octets).
//!
//! [`derive`](fn@derive) runs both steps. Use HKDF to derive keys from a
//! *cryptographically strong* secret (a DH/KEM shared secret, a master
//! key); for **low-entropy passwords** use [`pbkdf2`](mod@crate::pbkdf2) /
//! scrypt / Argon2 instead — HKDF is fast by design and does not resist
//! brute force.
//!
//! # Side-channel posture
//!
//! Inherits [`crate::hmac`](mod@crate::hmac): the HMAC core is data-oblivious (no
//! secret-content branch/index). The only length-dependent branch is
//! HMAC's `key.len() > BLOCK_LEN`, so a `salt`/`PRK` at or below the block
//! size is constant per hash. The `PRK` and `IKM` are secret; the HMAC-key
//! DPA caveat on SHA-2 (see [`crate::hmac`](mod@crate::hmac)) applies. Loop counts and the
//! `info`/counter bytes are public.

use crate::Digest;
use crate::hmac::{hmac, hmac_multi};
use alloc::vec;
use alloc::vec::Vec;

/// The requested output keying material exceeds `255 · H::OUTPUT_LEN` octets,
/// the maximum HKDF can produce (RFC 5869 §2.3).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidLength;

/// HKDF-Extract (RFC 5869 §2.2): `PRK = HMAC-H(salt, IKM)`.
///
/// Returns the `H::OUTPUT_LEN`-octet pseudorandom key. An empty `salt`
/// yields the "`HashLen` zeros" default.
pub fn extract<H: Digest>(salt: &[u8], ikm: &[u8]) -> Vec<u8> {
    let mut prk = vec![0u8; H::OUTPUT_LEN];
    hmac::<H>(salt, ikm, &mut prk);
    prk
}

/// HKDF-Expand (RFC 5869 §2.3): fill `okm` from `prk` and context `info`.
///
/// `okm.len()` must not exceed `255 · H::OUTPUT_LEN`, else [`InvalidLength`].
/// `prk` should be an `H::OUTPUT_LEN`-octet key from [`extract`] (or an
/// equally strong key).
pub fn expand<H: Digest>(prk: &[u8], info: &[u8], okm: &mut [u8]) -> Result<(), InvalidLength> {
    let hlen = H::OUTPUT_LEN;
    if okm.len() > 255 * hlen {
        return Err(InvalidLength);
    }
    // T(0) is the empty string; T(i) = HMAC(PRK, T(i-1) ‖ info ‖ i).
    let mut t: Vec<u8> = Vec::new();
    let mut offset = 0;
    let mut block: usize = 1; // 1..=255; cast to the single counter octet
    while offset < okm.len() {
        let mut ti = vec![0u8; hlen];
        hmac_multi::<H>(prk, &[&t, info, &[block as u8]], &mut ti);
        let take = core::cmp::min(hlen, okm.len() - offset);
        okm[offset..offset + take].copy_from_slice(&ti[..take]);
        offset += take;
        t = ti;
        block += 1;
    }
    Ok(())
}

/// HKDF (RFC 5869): [`extract`] then [`expand`] in one call.
///
/// # Parameters
/// - `salt`: optional non-secret salt for the extract step (empty ⇒ the
///   `HashLen`-zeros default).
/// - `ikm`: the input keying material (the secret).
/// - `info`: optional context/application info binding the output.
/// - `okm`: destination for the output keying material; `okm.len()` must not
///   exceed `255 · H::OUTPUT_LEN`, else [`InvalidLength`].
pub fn derive<H: Digest>(salt: &[u8], ikm: &[u8], info: &[u8], okm: &mut [u8]) -> Result<(), InvalidLength> {
    let prk = extract::<H>(salt, ikm);
    expand::<H>(&prk, info, okm)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sha2::Sha256;

    fn hx(h: &str) -> Vec<u8> {
        (0..h.len())
            .step_by(2)
            .map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap())
            .collect()
    }

    // RFC 5869 Appendix A.1 (HKDF-SHA-256, basic). Full A.1–A.3 set lives in
    // the integration suite `tests/kdf.rs`.
    #[test]
    fn rfc5869_a1_sha256() {
        let ikm = hx("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
        let salt = hx("000102030405060708090a0b0c");
        let info = hx("f0f1f2f3f4f5f6f7f8f9");
        let prk = extract::<Sha256>(&salt, &ikm);
        assert_eq!(
            prk,
            hx("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5")
        );
        let mut okm = [0u8; 42];
        derive::<Sha256>(&salt, &ikm, &info, &mut okm).unwrap();
        assert_eq!(
            okm[..],
            hx("3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865")[..]
        );
    }

    #[test]
    fn output_too_long_errors() {
        let prk = extract::<Sha256>(b"salt", b"ikm");
        let mut too_long = vec![0u8; 255 * 32 + 1];
        assert_eq!(expand::<Sha256>(&prk, b"", &mut too_long), Err(InvalidLength));
        let mut ok = vec![0u8; 255 * 32];
        assert!(expand::<Sha256>(&prk, b"", &mut ok).is_ok());
    }
}