rust_checker 1.0.0

A modular Rust code validation tool with HTML, JSON, SVG badge, and JUnit XML report export. Includes optional web dashboard and PQC guardrails via plugins.
Documentation
//! Hybrid PQC example using a production crypto library for KDF/AEAD.
//!
//! - Post-quantum KEM (Kyber/ML-KEM) via `pqcrypto-kyber`
//! - HKDF-SHA256 and ChaCha20-Poly1305 via `ring`
//! - OS CSPRNG via `rand::rngs::OsRng`
//! - Constant-time equality via `subtle`
//! - Zeroization of sensitive buffers via `zeroize`
//!
//! NOTE: This is a minimal demo. Real systems need robust nonce management,
//! AAD, versioning, error handling, etc.

use pqcrypto_kyber::kyber1024::SharedSecret as KyberSharedSecret;
use pqcrypto_kyber::kyber1024::{decapsulate, encapsulate, keypair};
use pqcrypto_traits::kem::SharedSecret as _; // brings .as_bytes() into scope

use rand::rngs::OsRng;
use rand_core::TryRngCore; // <-- rand_core 0.9: OsRng implements TryRngCore

use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305};
use ring::hkdf::{self, Prk, Salt};

use subtle::ConstantTimeEq;
use zeroize::{Zeroize, Zeroizing};

/// Constant-time compare for Kyber shared secrets (avoid non-CT `PartialEq`).
fn ct_eq_shared(a: &KyberSharedSecret, b: &KyberSharedSecret) -> bool {
    a.as_bytes().ct_eq(b.as_bytes()).unwrap_u8() == 1
}

/// Derive a symmetric key from PQC shared secret with HKDF-SHA256.
fn hkdf_derive_key(ss: &[u8]) -> Zeroizing<[u8; 32]> {
    // Use a domain-separated salt in real systems (protocol/version/etc.).
    let salt = Salt::new(hkdf::HKDF_SHA256, b"rust_checker.pqc.hybrid.hkdf.v1");
    let prk: Prk = salt.extract(ss);
    let mut out = Zeroizing::new([0u8; 32]);
    prk.expand(&[b"chacha20poly1305.key"], hkdf::HKDF_SHA256)
        .expect("HKDF expand")
        .fill(&mut *out)
        .expect("HKDF fill");
    out
}

/// Encrypt `plaintext` with ChaCha20-Poly1305 using `key_bytes`. Returns (nonce, ciphertext).
fn aead_encrypt(key_bytes: &[u8; 32], plaintext: &[u8]) -> (Zeroizing<[u8; 12]>, Vec<u8>) {
    let unbound = UnboundKey::new(&CHACHA20_POLY1305, key_bytes).expect("key");
    let key = LessSafeKey::new(unbound);

    // 96-bit nonce; ensure uniqueness per key.
    let mut nonce_bytes = Zeroizing::new([0u8; 12]);
    let mut rng = OsRng;
    rng.try_fill_bytes(&mut *nonce_bytes).expect("OsRng"); // <-- changed

    let nonce = Nonce::assume_unique_for_key(*nonce_bytes);
    let mut in_out = plaintext.to_vec();
    key.seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out)
        .expect("seal");
    (nonce_bytes, in_out)
}

/// Decrypt `ciphertext` with ChaCha20-Poly1305 using `key_bytes` and `nonce`.
fn aead_decrypt(key_bytes: &[u8; 32], nonce_bytes: &[u8; 12], ciphertext: &[u8]) -> Vec<u8> {
    let unbound = UnboundKey::new(&CHACHA20_POLY1305, key_bytes).expect("key");
    let key = LessSafeKey::new(unbound);

    let nonce = Nonce::assume_unique_for_key(*nonce_bytes);
    let mut in_out = ciphertext.to_vec();
    key.open_in_place(nonce, Aad::empty(), &mut in_out)
        .expect("open")
        .to_vec()
}

/// Demonstrate ML-KEM (Kyber) encapsulation + ring HKDF + ring AEAD.
pub fn run_hybrid_demo() -> usize {
    // --- Kyber keygen ---
    let (pk, sk) = keypair();

    // --- Encapsulate / Decapsulate ---
    // NOTE: For this crate, encapsulate returns (SharedSecret, Ciphertext).
    let (ss_sender, ct) = encapsulate(&pk);
    let ss_receiver = decapsulate(&ct, &sk);

    // --- Constant-time compare of shared secrets ---
    assert!(
        ct_eq_shared(&ss_sender, &ss_receiver),
        "PQC shared secrets do not match"
    );

    // --- Derive a symmetric key via HKDF-SHA256 (ring) ---
    let key = hkdf_derive_key(ss_sender.as_bytes());

    // --- Encrypt with ChaCha20-Poly1305 (ring) ---
    let plaintext = b"pqc-hybrid: hello, world";
    let (nonce, ciphertext) = aead_encrypt(&key, plaintext);

    // --- Decrypt and verify ---
    let decrypted = aead_decrypt(&key, &nonce, &ciphertext);
    assert_eq!(decrypted, plaintext);

    // Explicit zeroization example (optional; Zeroizing clears on drop anyway)
    let mut another_secret = Zeroizing::new(vec![0u8; 48]);
    let mut rng = OsRng;
    rng.try_fill_bytes(&mut another_secret[..]).expect("OsRng"); // <-- changed
    another_secret.zeroize();

    ss_sender.as_bytes().len() // 32 bytes for Kyber-1024
}

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

    #[test]
    fn hybrid_demo_roundtrips() {
        let len = run_hybrid_demo();
        assert_eq!(len, 32);
    }
}