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>

//! Argon2 — memory-hard password hashing (RFC 9106 / PHC winner).
//!
//! Three variants: [`Variant::D`] (data-dependent addressing, max GPU
//! resistance), [`Variant::I`] (data-independent, side-channel resistant),
//! and [`Variant::Id`] (hybrid — the RFC 9106 §4 recommended default). Built
//! on this crate's BLAKE2b: the initial hash `H_0` and the variable-length
//! hash `H'` are BLAKE2b, and the 1024-byte compression `G` uses the BLAKE2b
//! round (the BlaMka `GB`).
//!
//! `argon2(variant, password, salt, secret, ad, m_kib, t_cost, lanes, out)`
//! fills `m_kib · 1024` bytes over `t_cost` passes with `lanes`-way
//! parallelism and writes `out.len()` tag octets. `secret` (a keyed pepper)
//! and `ad` (associated data) may be empty.
//!
//! # Side-channel posture
//!
//! [`Variant::I`] (and the first half of pass 0 for [`Variant::Id`]) uses
//! **data-independent** memory addressing and is the side-channel-safe
//! choice. [`Variant::D`] (and the rest of [`Variant::Id`]) reads memory at a
//! **password-derived index** — a data-dependent access that is a
//! cache-timing target on shared hardware, inherent to Argon2d's design (RFC
//! 9106 §4). Use `Id` (or `I`) when a co-resident attacker may observe the
//! cache. The `GB`/BLAKE2b cores are data-oblivious.

use crate::Digest;
use crate::blake2::Blake2b;
use alloc::vec;
use alloc::vec::Vec;

const BLOCK_WORDS: usize = 128; // 1024 bytes / 8
const SYNC_POINTS: usize = 4; // slices per pass

type Block = [u64; BLOCK_WORDS];

/// Argon2 variant selector.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Variant {
    /// Argon2d — data-dependent addressing (no side-channel resistance).
    D = 0,
    /// Argon2i — data-independent addressing (side-channel resistant).
    I = 1,
    /// Argon2id — hybrid; the RFC 9106 recommended default.
    Id = 2,
}

/// Argon2 parameter error (RFC 9106 §3.1).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
    /// A required minimum is not met: `lanes == 0`, `t_cost == 0` (iteration
    /// count below 1), the tag is below 4 bytes, or `m_kib < 8 · lanes`
    /// (memory below the RFC 9106 §3.1 floor). No upper-range or salt-length
    /// validation is performed.
    InvalidParams,
}

// ---- BLAKE2b helpers -----------------------------------------------------

/// BLAKE2b with a caller-chosen output length `n ≤ 64`, over concatenated parts.
fn blake2b(n: usize, parts: &[&[u8]]) -> Vec<u8> {
    let mut h = Blake2b::with_output_len(n);
    for p in parts {
        h.update(p);
    }
    let mut out = vec![0u8; n];
    h.finalize(&mut out);
    out
}

/// H' variable-length hash (RFC 9106 §3.3): output exactly `out_len` octets.
fn h_prime(out_len: usize, input: &[u8]) -> Vec<u8> {
    let len_le = (out_len as u32).to_le_bytes();
    if out_len <= 64 {
        return blake2b(out_len, &[&len_le, input]);
    }
    // r = ceil(out_len/32) - 2 full 64-byte blocks, take 32 bytes of each.
    let r = out_len.div_ceil(32) - 2;
    let mut out = Vec::with_capacity(out_len);
    let mut v = blake2b(64, &[&len_le, input]); // V_1
    out.extend_from_slice(&v[..32]);
    for _ in 1..r {
        v = blake2b(64, &[&v]); // V_2 .. V_r
        out.extend_from_slice(&v[..32]);
    }
    let last = out_len - 32 * r; // V_{r+1}, length out_len - 32r
    let vlast = blake2b(last, &[&v]);
    out.extend_from_slice(&vlast);
    out
}

// ---- Compression function G ---------------------------------------------

/// The BlaMka mixing `GB` (RFC 9106 §3.5) on four words of `v`.
macro_rules! gb {
    ($v:expr, $a:expr, $b:expr, $c:expr, $d:expr) => {{
        let (mut a, mut b, mut c, mut d) = ($v[$a], $v[$b], $v[$c], $v[$d]);
        let m = |x: u64, y: u64| {
            x.wrapping_add(y)
                .wrapping_add(2u64.wrapping_mul((x & 0xffff_ffff).wrapping_mul(y & 0xffff_ffff)))
        };
        a = m(a, b);
        d = (d ^ a).rotate_right(32);
        c = m(c, d);
        b = (b ^ c).rotate_right(24);
        a = m(a, b);
        d = (d ^ a).rotate_right(16);
        c = m(c, d);
        b = (b ^ c).rotate_right(63);
        $v[$a] = a;
        $v[$b] = b;
        $v[$c] = c;
        $v[$d] = d;
    }};
}

/// Permutation P (RFC 9106 §3.6) over 16 words (8 registers).
fn permute(v: &mut [u64]) {
    gb!(v, 0, 4, 8, 12);
    gb!(v, 1, 5, 9, 13);
    gb!(v, 2, 6, 10, 14);
    gb!(v, 3, 7, 11, 15);
    gb!(v, 0, 5, 10, 15);
    gb!(v, 1, 6, 11, 12);
    gb!(v, 2, 7, 8, 13);
    gb!(v, 3, 4, 9, 14);
}

/// `out = out_xor ? out ^ G(x, y) : G(x, y)`, with `G` the Argon2 compression.
fn g(x: &Block, y: &Block, out: &mut Block, xor_prev: bool) {
    let mut r = [0u64; BLOCK_WORDS];
    for i in 0..BLOCK_WORDS {
        r[i] = x[i] ^ y[i];
    }
    let mut q = r;
    // Row-wise P: 8 rows of 16 words.
    for row in 0..8 {
        permute(&mut q[row * 16..row * 16 + 16]);
    }
    // Column-wise P: 8 columns of 16 words (2 words from each of 8 rows).
    for col in 0..8 {
        let mut tmp = [0u64; 16];
        for k in 0..8 {
            tmp[2 * k] = q[16 * k + 2 * col];
            tmp[2 * k + 1] = q[16 * k + 2 * col + 1];
        }
        permute(&mut tmp);
        for k in 0..8 {
            q[16 * k + 2 * col] = tmp[2 * k];
            q[16 * k + 2 * col + 1] = tmp[2 * k + 1];
        }
    }
    for i in 0..BLOCK_WORDS {
        let z = q[i] ^ r[i];
        out[i] = if xor_prev { out[i] ^ z } else { z };
    }
}

fn block_from_bytes(b: &[u8]) -> Block {
    let mut out = [0u64; BLOCK_WORDS];
    for (i, w) in out.iter_mut().enumerate() {
        *w = u64::from_le_bytes(b[8 * i..8 * i + 8].try_into().unwrap());
    }
    out
}

fn block_to_bytes(b: &Block) -> [u8; 1024] {
    let mut out = [0u8; 1024];
    for i in 0..BLOCK_WORDS {
        out[8 * i..8 * i + 8].copy_from_slice(&b[i].to_le_bytes());
    }
    out
}

// ---- Main driver ---------------------------------------------------------

/// Argon2 password-hashing / KDF (RFC 9106 §3.2).
///
/// # Parameters
/// - `variant`: [`Variant::D`] (data-dependent), [`Variant::I`]
///   (data-independent, side-channel resistant) or [`Variant::Id`]
///   (hybrid, the RFC-recommended default).
/// - `password`: the secret message `P` (RFC 9106 §3.1). Its length is a
///   secret; see the module `# Side-channel posture`.
/// - `salt`: the nonce `S`; per RFC 9106 §3.1 it **should** be ≥ 8 bytes
///   (16 recommended), but this minimum is **not** enforced here — the
///   caller is responsible for choosing an adequate salt.
/// - `secret`: the optional keyed-hashing secret `K` (empty slice if unused).
/// - `ad`: the optional associated data `X` (empty slice if unused).
/// - `m_kib`: memory size `m` in kibibytes; must be ≥ `8 * lanes`.
/// - `t_cost`: number of passes `t` over memory (≥ 1).
/// - `lanes`: degree of parallelism `p` (≥ 1); also the lane count.
/// - `out`: destination for the tag; its length is the requested tag size
///   `T` (≥ 4 bytes).
///
/// Returns [`Error::InvalidParams`] if `lanes == 0`, `t_cost == 0`, the tag
/// is < 4 bytes, or `m_kib < 8 · lanes`. No upper-range or salt-length check
/// is performed.
///
/// The 9-argument signature mirrors the RFC 9106 §3.1 input list
/// (`P, S, K, X, m, t, p, T`) one-to-one; splitting it would obscure that
/// mapping, hence the `clippy::too_many_arguments` waiver.
#[allow(clippy::too_many_arguments)]
pub fn argon2(
    variant: Variant,
    password: &[u8],
    salt: &[u8],
    secret: &[u8],
    ad: &[u8],
    m_kib: u32,
    t_cost: u32,
    lanes: u32,
    out: &mut [u8],
) -> Result<(), Error> {
    let p = lanes as usize;
    let tag_len = out.len();
    if p == 0 || t_cost == 0 || tag_len < 4 || m_kib < 8 * lanes {
        return Err(Error::InvalidParams);
    }

    // H_0 = BLAKE2b-64( LE32(p)‖LE32(T)‖LE32(m)‖LE32(t)‖LE32(v)‖LE32(y)
    //                   ‖LE32(|P|)‖P‖LE32(|S|)‖S‖LE32(|K|)‖K‖LE32(|X|)‖X ).
    let le = |x: u32| x.to_le_bytes();
    let h0 = blake2b(
        64,
        &[
            &le(lanes),
            &le(tag_len as u32),
            &le(m_kib),
            &le(t_cost),
            &le(0x13),
            &le(variant as u32),
            &le(password.len() as u32),
            password,
            &le(salt.len() as u32),
            salt,
            &le(secret.len() as u32),
            secret,
            &le(ad.len() as u32),
            ad,
        ],
    );

    // Memory layout: m' = 4·p·floor(m / 4p) blocks, q columns per lane.
    let m_prime = 4 * p * (m_kib as usize / (4 * p));
    let q = m_prime / p; // columns per lane
    let seg_len = q / SYNC_POINTS;
    let mut mem: Vec<Block> = vec![[0u64; BLOCK_WORDS]; m_prime];

    // First two columns of every lane.
    for i in 0..p {
        let b0 = h_prime(1024, &[&h0[..], &le(0), &le(i as u32)].concat());
        let b1 = h_prime(1024, &[&h0[..], &le(1), &le(i as u32)].concat());
        mem[i * q] = block_from_bytes(&b0);
        mem[i * q + 1] = block_from_bytes(&b1);
    }

    let zero: Block = [0u64; BLOCK_WORDS];
    for pass in 0..t_cost as usize {
        for slice in 0..SYNC_POINTS {
            for lane in 0..p {
                let data_independent = match variant {
                    Variant::D => false,
                    Variant::I => true,
                    Variant::Id => pass == 0 && slice < 2,
                };

                // Address-block state for data-independent (Argon2i) addressing.
                let mut input_block: Block = [0u64; BLOCK_WORDS];
                input_block[0] = pass as u64;
                input_block[1] = lane as u64;
                input_block[2] = slice as u64;
                input_block[3] = m_prime as u64;
                input_block[4] = t_cost as u64;
                input_block[5] = variant as u64;
                let mut addr: Block = [0u64; BLOCK_WORDS];
                let gen_addresses = |ctr: &mut u64, input_block: &mut Block, addr: &mut Block| {
                    *ctr += 1;
                    input_block[6] = *ctr;
                    let mut t = [0u64; BLOCK_WORDS];
                    g(&zero, input_block, &mut t, false);
                    g(&zero, &t, addr, false);
                };
                let mut ctr: u64 = 0;
                if data_independent && pass == 0 && slice == 0 {
                    gen_addresses(&mut ctr, &mut input_block, &mut addr);
                }

                let start = if pass == 0 && slice == 0 { 2 } else { 0 };
                for index in start..seg_len {
                    let col = slice * seg_len + index; // current column j
                    let prev_col = if col == 0 { q - 1 } else { col - 1 };

                    // (J_1, J_2)
                    let rand = if data_independent {
                        if index % BLOCK_WORDS == 0 {
                            gen_addresses(&mut ctr, &mut input_block, &mut addr);
                        }
                        addr[index % BLOCK_WORDS]
                    } else {
                        mem[lane * q + prev_col][0]
                    };
                    let j1 = rand & 0xffff_ffff;
                    let j2 = rand >> 32;

                    // Reference lane.
                    let ref_lane = if pass == 0 && slice == 0 {
                        lane
                    } else {
                        (j2 as usize) % p
                    };

                    // Reference area size (RFC 9106 §3.4.1.2).
                    let same_lane = ref_lane == lane;
                    let ref_area: usize = if pass == 0 {
                        if slice == 0 {
                            index - 1
                        } else if same_lane {
                            slice * seg_len + index - 1
                        } else {
                            slice * seg_len - usize::from(index == 0)
                        }
                    } else if same_lane {
                        (SYNC_POINTS - 1) * seg_len + index - 1
                    } else {
                        (SYNC_POINTS - 1) * seg_len - usize::from(index == 0)
                    };

                    // Relative → absolute position.
                    let x = (j1 * j1) >> 32;
                    let y = ((ref_area as u64) * x) >> 32;
                    let z = ref_area - 1 - (y as usize);
                    let ref_start = if pass == 0 || slice == SYNC_POINTS - 1 {
                        0
                    } else {
                        (slice + 1) * seg_len
                    };
                    let ref_col = (ref_start + z) % q;

                    let prev = mem[lane * q + prev_col];
                    let refb = mem[ref_lane * q + ref_col];
                    let mut cur = mem[lane * q + col];
                    g(&prev, &refb, &mut cur, pass != 0);
                    mem[lane * q + col] = cur;
                }
            }
        }
    }

    // Final: C = XOR of the last column of every lane; Tag = H'(C).
    let mut c = mem[q - 1];
    for i in 1..p {
        let b = mem[i * q + q - 1];
        for k in 0..BLOCK_WORDS {
            c[k] ^= b[k];
        }
    }
    let tag = h_prime(tag_len, &block_to_bytes(&c));
    out.copy_from_slice(&tag);
    Ok(())
}

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

    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 9106 §5 test vectors (P=32×01, S=16×02, K=8×03, X=12×04, m=32, t=3,
    // p=4, taglen=32, v=0x13). All three variants; gold from the RFC.
    #[test]
    fn rfc9106_vectors() {
        let (p, s, k, x) = ([1u8; 32], [2u8; 16], [3u8; 8], [4u8; 12]);
        let mut out = [0u8; 32];
        argon2(Variant::D, &p, &s, &k, &x, 32, 3, 4, &mut out).unwrap();
        assert_eq!(
            out[..],
            hx("512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb")[..]
        );
        argon2(Variant::I, &p, &s, &k, &x, 32, 3, 4, &mut out).unwrap();
        assert_eq!(
            out[..],
            hx("c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8")[..]
        );
        argon2(Variant::Id, &p, &s, &k, &x, 32, 3, 4, &mut out).unwrap();
        assert_eq!(
            out[..],
            hx("0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659")[..]
        );
    }
}