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>

//! PBKDF2 — Password-Based Key Derivation Function 2 (RFC 2898 / PKCS#5 v2.1,
//! NIST SP 800-132).
//!
//! `DK = T(1) ‖ T(2) ‖ … ‖ T(dkLen/hLen)` where each block
//! `T(i) = U(1) ⊕ U(2) ⊕ … ⊕ U(c)`, `U(1) = HMAC-H(P, S ‖ INT_BE(i))` and
//! `U(j) = HMAC-H(P, U(j-1))`. The iteration count `c` deliberately makes
//! derivation slow to raise the cost of password guessing. Generic over any
//! [`Digest`] `H` (as `HMAC-H`); the classic instantiation is
//! `PBKDF2-HMAC-SHA-256`.
//!
//! Choose `iterations` per current guidance (OWASP 2023: ≥ 600 000 for
//! HMAC-SHA-256). PBKDF2 is only *iteration*-hard, not *memory*-hard — for
//! stronger password hashing prefer the memory-hard
//! [`scrypt`](fn@crate::scrypt) (RFC 7914) or [`argon2`](mod@crate::argon2)
//! (RFC 9106), both provided by this crate. `iterations` is a
//! [`NonZeroU32`], so `c ≥ 1` is enforced at the type level (RFC 2898
//! requires `c ≥ 1`).
//!
//! # Side-channel posture
//!
//! Inherits [`crate::hmac`](mod@crate::hmac): the HMAC core is data-oblivious. The password
//! `P` is secret and used as the HMAC key; HMAC's single length branch
//! (`key.len() > BLOCK_LEN`) therefore leaks the **password length class**
//! (short vs. longer than the block), not its content — a standard property
//! of HMAC-based PBKDF2. The iteration count, salt, block index and lengths
//! are public; the per-block `U(j)` XOR accumulation is fixed-length. The
//! HMAC-key DPA caveat on SHA-2 (see [`crate::hmac`](mod@crate::hmac)) applies to `P`.

use crate::Digest;
use crate::hmac::{hmac, hmac_multi};
use alloc::vec;
use core::num::NonZeroU32;

/// Derive `dk.len()` octets from `password` and `salt` with `iterations`
/// rounds of `HMAC-H` (PBKDF2, RFC 2898 §5.2).
///
/// Fills the whole of `dk`; `dk.len()` is the requested derived-key length
/// (`dkLen`). The RFC's `dkLen ≤ (2³² − 1) · hLen` ceiling is far beyond any
/// practical buffer and is not separately checked.
pub fn pbkdf2<H: Digest>(password: &[u8], salt: &[u8], iterations: NonZeroU32, dk: &mut [u8]) {
    let hlen = H::OUTPUT_LEN;
    let c = iterations.get();
    let mut block_index: u32 = 1;
    let mut offset = 0;

    while offset < dk.len() {
        // U(1) = HMAC(P, S ‖ INT_BE(block_index)); T = U(1).
        let mut u = vec![0u8; hlen];
        hmac_multi::<H>(password, &[salt, &block_index.to_be_bytes()], &mut u);
        let mut t = u.clone();

        // U(j) = HMAC(P, U(j-1)); T ^= U(j) for j = 2..=c.
        let mut next = vec![0u8; hlen];
        for _ in 1..c {
            hmac::<H>(password, &u, &mut next);
            for k in 0..hlen {
                t[k] ^= next[k];
            }
            u.copy_from_slice(&next);
        }

        let take = core::cmp::min(hlen, dk.len() - offset);
        dk[offset..offset + take].copy_from_slice(&t[..take]);
        offset += take;
        block_index += 1;
    }
}

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

    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()
    }

    fn nz(n: u32) -> NonZeroU32 {
        NonZeroU32::new(n).unwrap()
    }

    // RFC 6070 test vector 1 (PBKDF2-HMAC-SHA-1). The full RFC 6070 set and
    // HMAC-SHA-256 vectors live in the integration suite `tests/kdf.rs`.
    #[test]
    fn rfc6070_case1_sha1() {
        let mut dk = [0u8; 20];
        pbkdf2::<Sha1>(b"password", b"salt", nz(1), &mut dk);
        assert_eq!(dk[..], hx("0c60c80f961f0e71f3a9b524af6012062fe037a6")[..]);
    }

    #[test]
    fn rfc6070_case2_sha1() {
        let mut dk = [0u8; 20];
        pbkdf2::<Sha1>(b"password", b"salt", nz(2), &mut dk);
        assert_eq!(dk[..], hx("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957")[..]);
    }
}