Skip to main content

qv_core/
mutation.rs

1use crate::crypto::sha3_256;
2use crate::error::{QVError, QVResult};
3
4/// HYDRA mutation chain — each token mutation ratchets a SHA3-256 hash chain.
5/// This provides Post-Compromise Security (PCS): a stolen token becomes
6/// invalid once the legitimate holder advances the chain.
7pub struct MutationChain {
8    state: [u8; 32],
9    counter: u64,
10}
11
12impl MutationChain {
13    /// Initialise from a random 32-byte seed (should be CSPRNG output).
14    pub fn new(seed: [u8; 32]) -> Self {
15        MutationChain { state: seed, counter: 0 }
16    }
17
18    /// Restore from persisted state.
19    pub fn from_state(state: [u8; 32], counter: u64) -> Self {
20        MutationChain { state, counter }
21    }
22
23    /// Advance the chain once and return the new epoch tag.
24    /// The new state is SHA3-256(old_state || counter_bytes).
25    pub fn advance(&mut self) -> [u8; 32] {
26        let mut input = [0u8; 40];
27        input[..32].copy_from_slice(&self.state);
28        input[32..].copy_from_slice(&self.counter.to_be_bytes());
29        self.state = sha3_256(&input);
30        self.counter += 1;
31        self.state
32    }
33
34    pub fn current_counter(&self) -> u64 { self.counter }
35    pub fn current_state(&self) -> &[u8; 32] { &self.state }
36
37    /// Verify a token's mutation counter is strictly greater than the stored chain counter.
38    /// Enforces monotonicity — replay of any previous token is immediately rejected.
39    pub fn check_token_counter(&self, token_ctr: u64) -> QVResult<()> {
40        if token_ctr <= self.counter {
41            Err(QVError::ReplayDetected { token: token_ctr, chain: self.counter })
42        } else {
43            Ok(())
44        }
45    }
46}
47
48/// KOLMOGOROV entropy certification: verify that a byte slice is "sufficiently random"
49/// by checking that no 4-byte pattern repeats more than a statistical threshold.
50/// Real randomness test: run-length compression ratio must be > 0.85.
51pub fn certify_entropy(data: &[u8]) -> QVResult<()> {
52    if data.len() < 8 {
53        return Ok(()); // too short to test
54    }
55    // Naive LZ-like: count unique 4-grams vs total 4-grams.
56    let total = data.len() - 3;
57    let mut seen = std::collections::HashSet::new();
58    for i in 0..total {
59        let gram: [u8; 4] = data[i..i + 4].try_into().unwrap();
60        seen.insert(gram);
61    }
62    let ratio = seen.len() as f64 / total as f64;
63    if ratio < 0.85 {
64        Err(QVError::LowEntropy(ratio))
65    } else {
66        Ok(())
67    }
68}