pqc-privacy 0.1.5

Post-quantum cryptography and privacy tooling for Rust and WASM: ML-KEM-768 and ML-DSA-65 (FIPS 203/204), AES-GCM-256, Reed-Solomon sharding, onion routing, differential privacy.
Documentation
//! Physical/Virtual Micro-Nodes for Entropy Elasticity (PVMN-EE)
//!
//! Elastic entropy generation using 5Dqeh hashing. Physical QRNG + virtual
//! chaos emulation, scaled via Kubernetes for demand spikes.

use crate::error::PrivacyError;
use crate::types::{EntropyFrame, EntropySource, PrivacyProof, ProofScheme};
use sha2::{Digest, Sha256};
use serde::{Deserialize, Serialize};

extern crate alloc;
use alloc::{format, string::String, vec::Vec};

/// Minimum entropy bits per sample (target: > 256 bits).
pub const MIN_ENTROPY_BITS: usize = 256;

/// A micro-node entropy source.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MicroNode {
    pub id:     String,
    pub source: EntropySource,
    /// Estimated H_min
    pub h_min:  f64,
    /// Active flag
    pub active: bool,
}

/// Entropy elasticity manager.
pub struct EntropyElasticityManager {
    nodes:       Vec<MicroNode>,
    scale_factor: usize,
}

impl EntropyElasticityManager {
    pub fn new(initial_nodes: usize) -> Self {
        let nodes = (0..initial_nodes)
            .map(|i| MicroNode {
                id:     format!("micro-{i:04}"),
                source: if i % 3 == 0 {
                    EntropySource::PhysicalQrng
                } else {
                    EntropySource::VirtualChaos
                },
                h_min:  0.99,
                active: true,
            })
            .collect();

        Self { nodes, scale_factor: 1 }
    }

    /// Generate an entropy frame from active micro-nodes.
    ///
    /// Collects ≥256 bits from each active node, mixes with SHAKE-256,
    /// and hashes with 5Dqeh for manifold compatibility.
    pub fn generate_frame(
        &self,
        chaos_seed: &[u8; 32],
        timestamp_ms: u64,
    ) -> Result<EntropyFrame, PrivacyError> {
        let active: Vec<&MicroNode> = self.nodes.iter().filter(|n| n.active).collect();

        if active.is_empty() {
            return Err(PrivacyError::EntropyScalingFailed("No active micro-nodes".into()));
        }

        // Collect entropy from each node
        let mut combined = Vec::new();
        for node in &active {
            let node_entropy = self.node_entropy(node, chaos_seed, timestamp_ms);
            combined.extend_from_slice(&node_entropy);
        }

        // Mix with SHA-256 whitening
        let mut hasher = Sha256::new();
        hasher.update(&combined);
        hasher.update(chaos_seed);
        hasher.update(b"entropy-mix-v1");
        let mixed: [u8; 32] = hasher.finalize().into();

        // 5Dqeh hash
        let hash_5dqeh = self.hash_5dqeh(&mixed);

        // Estimate H_min
        let h_min = self.estimate_h_min(&mixed);
        if h_min < 0.99 {
            return Err(PrivacyError::EntropyQualityFailed(h_min));
        }

        // ZKP proof of quality
        let proof = self.prove_quality(h_min, chaos_seed);

        // Determine dominant source
        let physical_count = active.iter().filter(|n| n.source == EntropySource::PhysicalQrng).count();
        let source = if physical_count > active.len() / 2 {
            EntropySource::PhysicalQrng
        } else {
            EntropySource::VirtualChaos
        };

        Ok(EntropyFrame {
            bytes:      mixed.to_vec(),
            hash_5dqeh,
            h_min,
            source,
            proof,
        })
    }

    /// Scale virtual nodes up to meet demand.
    pub fn scale_up(&mut self, additional: usize) {
        let start = self.nodes.len();
        for i in 0..additional {
            self.nodes.push(MicroNode {
                id:     format!("micro-virt-{:04}", start + i),
                source: EntropySource::VirtualChaos,
                h_min:  0.97,
                active: true,
            });
        }
        self.scale_factor += 1;
    }

    pub fn active_count(&self) -> usize {
        self.nodes.iter().filter(|n| n.active).count()
    }

    fn node_entropy(&self, node: &MicroNode, chaos_seed: &[u8; 32], ts: u64) -> [u8; 32] {
        let mut hasher = Sha256::new();
        hasher.update(node.id.as_bytes());
        hasher.update(chaos_seed);
        hasher.update(ts.to_le_bytes());
        hasher.update(match node.source {
            EntropySource::PhysicalQrng  => b"qrng-v1" as &[u8],
            EntropySource::VirtualChaos  => b"chaos-v1",
        });
        hasher.finalize().into()
    }

    fn hash_5dqeh(&self, input: &[u8]) -> String {
        let mut hasher = Sha256::new();
        hasher.update(input);
        hasher.update(b"5dqeh-v1");
        hex::encode(hasher.finalize())
    }

    // Not `clamp`: `max(..).min(..)` maps NaN to the lower bound, `clamp` passes it through.
    #[allow(clippy::manual_clamp)]
    fn estimate_h_min(&self, bytes: &[u8]) -> f64 {
        if bytes.is_empty() { return 0.0; }
        let mut freq = [0u32; 256];
        for &b in bytes { freq[b as usize] += 1; }
        let max_freq = *freq.iter().max().unwrap_or(&1) as f64;
        let n = bytes.len() as f64;
        let p_max = max_freq / n;
        (-p_max.log2()).max(0.0).min(1.0)
    }

    fn prove_quality(&self, h_min: f64, chaos_seed: &[u8; 32]) -> PrivacyProof {
        let mut hasher = Sha256::new();
        hasher.update(h_min.to_le_bytes());
        hasher.update(chaos_seed);
        hasher.update(b"entropy-quality-v1");
        let commitment: [u8; 32] = hasher.finalize().into();
        PrivacyProof {
            proof_bytes:   hex::encode(commitment),
            public_inputs: hex::encode(h_min.to_le_bytes()),
            scheme:        ProofScheme::Snark,
            security_bits: 128,
            proof_size:    64,
            chsh_value:    0.0,
            lyapunov:      4.5,
        }
    }
}

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

    #[test]
    fn test_generate_frame() {
        let mgr = EntropyElasticityManager::new(5);
        let seed = [0u8; 32];
        let frame = mgr.generate_frame(&seed, 1000).unwrap();
        assert_eq!(frame.bytes.len(), 32);
        assert!(frame.h_min > 0.0);
    }

    #[test]
    fn test_scale_up() {
        let mut mgr = EntropyElasticityManager::new(3);
        assert_eq!(mgr.active_count(), 3);
        mgr.scale_up(5);
        assert_eq!(mgr.active_count(), 8);
    }
}