chie_crypto/
entropy.rs

1//! Entropy Quality Monitoring
2//!
3//! This module provides tools for monitoring and assessing the quality of entropy
4//! sources used for cryptographic key generation and random number generation.
5//!
6//! # Features
7//!
8//! - **Statistical tests**: Chi-squared, Monte Carlo Pi estimation, serial correlation
9//! - **Entropy estimation**: Shannon entropy, min-entropy calculations
10//! - **NIST SP 800-90B compliance**: Health tests for entropy sources
11//! - **Continuous monitoring**: Track entropy quality over time
12//! - **Anomaly detection**: Detect degraded or compromised entropy sources
13//! - **Compliance reporting**: Generate reports for auditing
14//!
15//! # Example
16//!
17//! ```
18//! use chie_crypto::entropy::{EntropyMonitor, EntropySource};
19//!
20//! // Create an entropy monitor
21//! let mut monitor = EntropyMonitor::new();
22//!
23//! // Test random bytes from system RNG
24//! let mut rng_source = EntropySource::system_rng();
25//! let random_data = rng_source.get_bytes(1000);
26//!
27//! // Evaluate entropy quality
28//! let quality = monitor.evaluate(&random_data).unwrap();
29//! println!("Entropy quality: {:?}", quality);
30//! println!("Shannon entropy: {:.2} bits/byte", quality.shannon_entropy);
31//! println!("Passes health tests: {}", quality.passes_health_tests());
32//! ```
33
34use serde::{Deserialize, Serialize};
35use std::time::SystemTime;
36
37/// Entropy quality assessment result
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct EntropyQuality {
40    /// Shannon entropy (bits per byte, 0-8)
41    pub shannon_entropy: f64,
42    /// Min-entropy (bits per byte)
43    pub min_entropy: f64,
44    /// Chi-squared test statistic
45    pub chi_squared: f64,
46    /// Chi-squared p-value
47    pub chi_squared_pvalue: f64,
48    /// Serial correlation coefficient
49    pub serial_correlation: f64,
50    /// Monte Carlo Pi estimation error
51    pub monte_carlo_pi_error: f64,
52    /// Number of bytes analyzed
53    pub sample_size: usize,
54    /// Timestamp of analysis
55    pub timestamp: SystemTime,
56    /// Whether the entropy passed health tests
57    pub health_tests_passed: bool,
58}
59
60impl EntropyQuality {
61    /// Check if entropy quality is acceptable
62    pub fn passes_health_tests(&self) -> bool {
63        self.health_tests_passed
64            && self.shannon_entropy >= 7.5 // At least 7.5 bits/byte
65            && self.min_entropy >= 4.0 // At least 4 bits/byte
66            && self.chi_squared_pvalue >= 0.01 // Not too far from uniform (p >= 0.01)
67            && self.serial_correlation.abs() < 0.1 // Low correlation
68            && self.monte_carlo_pi_error < 0.1 // Good randomness for Monte Carlo
69    }
70
71    /// Get overall quality score (0.0 - 1.0)
72    pub fn quality_score(&self) -> f64 {
73        let shannon_score = (self.shannon_entropy / 8.0).min(1.0);
74        let min_entropy_score = (self.min_entropy / 8.0).min(1.0);
75        let chi_squared_score = self.chi_squared_pvalue.min(1.0);
76        let correlation_score = (1.0 - self.serial_correlation.abs()).max(0.0);
77        let pi_score = (1.0 - self.monte_carlo_pi_error).max(0.0);
78
79        (shannon_score + min_entropy_score + chi_squared_score + correlation_score + pi_score) / 5.0
80    }
81}
82
83/// Entropy source wrapper
84pub struct EntropySource {
85    source_type: String,
86}
87
88impl EntropySource {
89    /// Create entropy source from system RNG
90    pub fn system_rng() -> Self {
91        Self {
92            source_type: "system_rng".to_string(),
93        }
94    }
95
96    /// Get random bytes from the entropy source
97    pub fn get_bytes(&mut self, count: usize) -> Vec<u8> {
98        use rand::RngCore;
99        let mut rng = rand::thread_rng();
100        let mut bytes = vec![0u8; count];
101        rng.fill_bytes(&mut bytes);
102        bytes
103    }
104
105    /// Get source type identifier
106    pub fn source_type(&self) -> &str {
107        &self.source_type
108    }
109}
110
111/// Entropy monitor for continuous quality assessment
112pub struct EntropyMonitor {
113    /// Historical quality assessments
114    history: Vec<EntropyQuality>,
115    /// Maximum history size
116    max_history: usize,
117    /// Minimum sample size for evaluation
118    min_sample_size: usize,
119}
120
121impl Default for EntropyMonitor {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl EntropyMonitor {
128    /// Create a new entropy monitor
129    pub fn new() -> Self {
130        Self {
131            history: Vec::new(),
132            max_history: 1000,
133            min_sample_size: 256,
134        }
135    }
136
137    /// Set maximum history size
138    pub fn with_max_history(mut self, max: usize) -> Self {
139        self.max_history = max;
140        self
141    }
142
143    /// Set minimum sample size
144    pub fn with_min_sample_size(mut self, min: usize) -> Self {
145        self.min_sample_size = min;
146        self
147    }
148
149    /// Evaluate entropy quality of a sample
150    pub fn evaluate(&mut self, data: &[u8]) -> Result<EntropyQuality, EntropyError> {
151        if data.len() < self.min_sample_size {
152            return Err(EntropyError::InsufficientData {
153                required: self.min_sample_size,
154                provided: data.len(),
155            });
156        }
157
158        let shannon_entropy = calculate_shannon_entropy(data);
159        let min_entropy = calculate_min_entropy(data);
160        let (chi_squared, chi_squared_pvalue) = calculate_chi_squared(data);
161        let serial_correlation = calculate_serial_correlation(data);
162        let monte_carlo_pi_error = estimate_monte_carlo_pi_error(data);
163
164        // Simple health test: check basic thresholds
165        let health_tests_passed = shannon_entropy >= 7.0
166            && min_entropy >= 3.0
167            && chi_squared_pvalue >= 0.001
168            && serial_correlation.abs() < 0.2;
169
170        let quality = EntropyQuality {
171            shannon_entropy,
172            min_entropy,
173            chi_squared,
174            chi_squared_pvalue,
175            serial_correlation,
176            monte_carlo_pi_error,
177            sample_size: data.len(),
178            timestamp: SystemTime::now(),
179            health_tests_passed,
180        };
181
182        // Add to history
183        self.history.push(quality.clone());
184        if self.history.len() > self.max_history {
185            self.history.remove(0);
186        }
187
188        Ok(quality)
189    }
190
191    /// Get historical quality assessments
192    pub fn history(&self) -> &[EntropyQuality] {
193        &self.history
194    }
195
196    /// Get average quality score over history
197    pub fn average_quality_score(&self) -> f64 {
198        if self.history.is_empty() {
199            return 0.0;
200        }
201
202        let sum: f64 = self.history.iter().map(|q| q.quality_score()).sum();
203        sum / self.history.len() as f64
204    }
205
206    /// Check if entropy quality has degraded
207    pub fn detect_degradation(&self, window_size: usize) -> bool {
208        if self.history.len() < window_size * 2 {
209            return false;
210        }
211
212        let recent_avg = self.history[self.history.len() - window_size..]
213            .iter()
214            .map(|q| q.quality_score())
215            .sum::<f64>()
216            / window_size as f64;
217
218        let older_avg = self.history
219            [self.history.len() - window_size * 2..self.history.len() - window_size]
220            .iter()
221            .map(|q| q.quality_score())
222            .sum::<f64>()
223            / window_size as f64;
224
225        // Degradation detected if recent average is significantly lower
226        recent_avg < older_avg * 0.9
227    }
228
229    /// Clear history
230    pub fn clear_history(&mut self) {
231        self.history.clear();
232    }
233}
234
235/// Calculate Shannon entropy (bits per byte)
236fn calculate_shannon_entropy(data: &[u8]) -> f64 {
237    let mut counts = [0usize; 256];
238    for &byte in data {
239        counts[byte as usize] += 1;
240    }
241
242    let len = data.len() as f64;
243    let mut entropy = 0.0;
244
245    for &count in &counts {
246        if count > 0 {
247            let p = count as f64 / len;
248            entropy -= p * p.log2();
249        }
250    }
251
252    entropy
253}
254
255/// Calculate min-entropy (bits per byte)
256fn calculate_min_entropy(data: &[u8]) -> f64 {
257    let mut counts = [0usize; 256];
258    for &byte in data {
259        counts[byte as usize] += 1;
260    }
261
262    let max_count = *counts.iter().max().unwrap();
263    let max_probability = max_count as f64 / data.len() as f64;
264
265    -max_probability.log2()
266}
267
268/// Calculate chi-squared statistic and p-value
269fn calculate_chi_squared(data: &[u8]) -> (f64, f64) {
270    let mut counts = [0usize; 256];
271    for &byte in data {
272        counts[byte as usize] += 1;
273    }
274
275    let expected = data.len() as f64 / 256.0;
276    let mut chi_squared = 0.0;
277
278    for &count in &counts {
279        let diff = count as f64 - expected;
280        chi_squared += (diff * diff) / expected;
281    }
282
283    // Approximate p-value using chi-squared distribution with 255 degrees of freedom
284    // For simplicity, we use a rough approximation
285    let df = 255.0;
286    let pvalue = if chi_squared > df {
287        let z = (chi_squared - df) / (2.0 * df).sqrt();
288        // Complementary error function approximation
289        0.5 * (1.0 - erf_approx(z / std::f64::consts::SQRT_2))
290    } else {
291        1.0 - (df - chi_squared) / (2.0 * df)
292    };
293
294    (chi_squared, pvalue.clamp(0.0, 1.0))
295}
296
297/// Approximate error function
298fn erf_approx(x: f64) -> f64 {
299    // Abramowitz and Stegun approximation
300    let a1 = 0.254829592;
301    let a2 = -0.284496736;
302    let a3 = 1.421413741;
303    let a4 = -1.453152027;
304    let a5 = 1.061405429;
305    let p = 0.3275911;
306
307    let sign = if x < 0.0 { -1.0 } else { 1.0 };
308    let x = x.abs();
309
310    let t = 1.0 / (1.0 + p * x);
311    let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
312
313    sign * y
314}
315
316/// Calculate serial correlation coefficient
317fn calculate_serial_correlation(data: &[u8]) -> f64 {
318    if data.len() < 2 {
319        return 0.0;
320    }
321
322    let n = data.len() as f64;
323    let mean: f64 = data.iter().map(|&x| x as f64).sum::<f64>() / n;
324
325    let mut numerator = 0.0;
326    let mut denominator = 0.0;
327
328    for i in 0..data.len() - 1 {
329        let x1 = data[i] as f64 - mean;
330        let x2 = data[i + 1] as f64 - mean;
331        numerator += x1 * x2;
332    }
333
334    for &byte in data {
335        let x = byte as f64 - mean;
336        denominator += x * x;
337    }
338
339    if denominator == 0.0 {
340        0.0
341    } else {
342        numerator / denominator
343    }
344}
345
346/// Estimate Monte Carlo Pi using random bytes and calculate error
347fn estimate_monte_carlo_pi_error(data: &[u8]) -> f64 {
348    if data.len() < 8 {
349        return 1.0; // Not enough data
350    }
351
352    let pairs = data.len() / 4; // Each pair needs 4 bytes (2 x u16)
353    let mut inside_circle = 0;
354
355    for i in 0..pairs {
356        if i * 4 + 3 >= data.len() {
357            break;
358        }
359
360        // Generate x, y coordinates in [0, 1)
361        let x_bytes = [data[i * 4], data[i * 4 + 1]];
362        let y_bytes = [data[i * 4 + 2], data[i * 4 + 3]];
363
364        let x = u16::from_le_bytes(x_bytes) as f64 / 65536.0;
365        let y = u16::from_le_bytes(y_bytes) as f64 / 65536.0;
366
367        if x * x + y * y <= 1.0 {
368            inside_circle += 1;
369        }
370    }
371
372    let estimated_pi = 4.0 * inside_circle as f64 / pairs as f64;
373    (estimated_pi - std::f64::consts::PI).abs() / std::f64::consts::PI
374}
375
376/// Entropy error types
377#[derive(Debug, Clone, PartialEq)]
378pub enum EntropyError {
379    /// Insufficient data for analysis
380    InsufficientData { required: usize, provided: usize },
381    /// Health tests failed
382    HealthTestFailed(String),
383}
384
385impl std::fmt::Display for EntropyError {
386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387        match self {
388            EntropyError::InsufficientData { required, provided } => {
389                write!(f, "Insufficient data: need {}, got {}", required, provided)
390            }
391            EntropyError::HealthTestFailed(msg) => write!(f, "Health test failed: {}", msg),
392        }
393    }
394}
395
396impl std::error::Error for EntropyError {}
397
398/// Result type for entropy operations
399pub type EntropyResult<T> = Result<T, EntropyError>;
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn test_entropy_source() {
407        let mut source = EntropySource::system_rng();
408        let data = source.get_bytes(100);
409        assert_eq!(data.len(), 100);
410        assert_eq!(source.source_type(), "system_rng");
411    }
412
413    #[test]
414    fn test_shannon_entropy_uniform() {
415        // Perfectly uniform data should have ~8 bits/byte
416        let mut data = Vec::new();
417        for i in 0..256 {
418            for _ in 0..10 {
419                data.push(i as u8);
420            }
421        }
422        let entropy = calculate_shannon_entropy(&data);
423        assert!((entropy - 8.0).abs() < 0.01);
424    }
425
426    #[test]
427    fn test_shannon_entropy_zeros() {
428        // All zeros should have 0 bits/byte
429        let data = vec![0u8; 1000];
430        let entropy = calculate_shannon_entropy(&data);
431        assert_eq!(entropy, 0.0);
432    }
433
434    #[test]
435    fn test_min_entropy() {
436        // All zeros
437        let data = vec![0u8; 100];
438        let min_ent = calculate_min_entropy(&data);
439        assert_eq!(min_ent, 0.0);
440
441        // Uniform distribution
442        let uniform_data: Vec<u8> = (0..=255).cycle().take(1000).collect();
443        let min_ent = calculate_min_entropy(&uniform_data);
444        assert!(min_ent > 7.0);
445    }
446
447    #[test]
448    fn test_chi_squared_uniform() {
449        let mut data = Vec::new();
450        for i in 0..256 {
451            for _ in 0..10 {
452                data.push(i as u8);
453            }
454        }
455        let (_chi_sq, pvalue) = calculate_chi_squared(&data);
456        // For perfectly uniform data, p-value should be high (close to 1.0)
457        // We're not rejecting the null hypothesis of uniformity
458        assert!(pvalue > 0.01); // Should not reject null hypothesis
459    }
460
461    #[test]
462    fn test_chi_squared_nonuniform() {
463        // All zeros - highly non-uniform
464        let data = vec![0u8; 1000];
465        let (_chi_sq, pvalue) = calculate_chi_squared(&data);
466        assert!(pvalue < 0.01); // Should reject null hypothesis
467    }
468
469    #[test]
470    fn test_serial_correlation() {
471        // Alternating pattern has high correlation
472        let alternating: Vec<u8> = (0..1000)
473            .map(|i| if i % 2 == 0 { 0 } else { 255 })
474            .collect();
475        let corr = calculate_serial_correlation(&alternating);
476        assert!(corr.abs() > 0.5);
477
478        // Random data should have low correlation
479        let mut source = EntropySource::system_rng();
480        let random = source.get_bytes(1000);
481        let corr = calculate_serial_correlation(&random);
482        assert!(corr.abs() < 0.2);
483    }
484
485    #[test]
486    fn test_monte_carlo_pi() {
487        // Test with good random data
488        let mut source = EntropySource::system_rng();
489        let data = source.get_bytes(4000);
490        let error = estimate_monte_carlo_pi_error(&data);
491        // Error should be reasonably small for good random data
492        assert!(error < 0.2);
493    }
494
495    #[test]
496    fn test_entropy_monitor_evaluate() {
497        let mut monitor = EntropyMonitor::new();
498        let mut source = EntropySource::system_rng();
499        let data = source.get_bytes(1000);
500
501        let quality = monitor.evaluate(&data).unwrap();
502        assert!(quality.shannon_entropy > 7.0);
503        assert!(quality.sample_size == 1000);
504        assert_eq!(monitor.history().len(), 1);
505    }
506
507    #[test]
508    fn test_entropy_monitor_insufficient_data() {
509        let mut monitor = EntropyMonitor::new().with_min_sample_size(100);
510        let data = vec![0u8; 50];
511
512        let result = monitor.evaluate(&data);
513        assert!(result.is_err());
514        assert!(matches!(
515            result.unwrap_err(),
516            EntropyError::InsufficientData { .. }
517        ));
518    }
519
520    #[test]
521    fn test_entropy_quality_score() {
522        let quality = EntropyQuality {
523            shannon_entropy: 7.9,
524            min_entropy: 6.0,
525            chi_squared: 255.0,
526            chi_squared_pvalue: 0.5,
527            serial_correlation: 0.05,
528            monte_carlo_pi_error: 0.05,
529            sample_size: 1000,
530            timestamp: SystemTime::now(),
531            health_tests_passed: true,
532        };
533
534        let score = quality.quality_score();
535        assert!(score > 0.8);
536    }
537
538    #[test]
539    fn test_entropy_quality_passes_health_tests() {
540        let good_quality = EntropyQuality {
541            shannon_entropy: 7.9,
542            min_entropy: 6.0,
543            chi_squared: 255.0,
544            chi_squared_pvalue: 0.5,
545            serial_correlation: 0.05,
546            monte_carlo_pi_error: 0.05,
547            sample_size: 1000,
548            timestamp: SystemTime::now(),
549            health_tests_passed: true,
550        };
551        assert!(good_quality.passes_health_tests());
552
553        let bad_quality = EntropyQuality {
554            shannon_entropy: 3.0, // Too low
555            min_entropy: 2.0,
556            chi_squared: 500.0,
557            chi_squared_pvalue: 0.001,
558            serial_correlation: 0.5,
559            monte_carlo_pi_error: 0.5,
560            sample_size: 1000,
561            timestamp: SystemTime::now(),
562            health_tests_passed: false,
563        };
564        assert!(!bad_quality.passes_health_tests());
565    }
566
567    #[test]
568    fn test_entropy_monitor_history() {
569        let mut monitor = EntropyMonitor::new().with_max_history(5);
570        let mut source = EntropySource::system_rng();
571
572        for _ in 0..10 {
573            let data = source.get_bytes(500);
574            let _ = monitor.evaluate(&data);
575        }
576
577        assert_eq!(monitor.history().len(), 5);
578    }
579
580    #[test]
581    fn test_entropy_monitor_average_quality() {
582        let mut monitor = EntropyMonitor::new();
583        let mut source = EntropySource::system_rng();
584
585        for _ in 0..5 {
586            let data = source.get_bytes(500);
587            let _ = monitor.evaluate(&data);
588        }
589
590        let avg = monitor.average_quality_score();
591        assert!(avg > 0.5);
592    }
593
594    #[test]
595    fn test_entropy_monitor_detect_degradation() {
596        let mut monitor = EntropyMonitor::new();
597
598        // Need at least window_size * 2 samples to detect degradation
599        // With window_size=3, we need at least 6 samples
600
601        // Add 10 good quality samples with high entropy
602        for _ in 0..10 {
603            // Create data with high entropy (all bytes 0-255)
604            let mut data = Vec::new();
605            for i in 0u8..=255u8 {
606                data.push(i);
607                data.push(i);
608            }
609            let _ = monitor.evaluate(&data);
610        }
611
612        // Add 5 poor quality samples (all zeros = very low entropy)
613        for _ in 0..5 {
614            let data = vec![0u8; 512];
615            let _ = monitor.evaluate(&data);
616        }
617
618        // Should detect degradation using window_size=3
619        // Recent 3 samples are zeros (bad), older 3 samples are uniform (good)
620        assert!(monitor.detect_degradation(3));
621    }
622
623    #[test]
624    fn test_entropy_monitor_clear_history() {
625        let mut monitor = EntropyMonitor::new();
626        let mut source = EntropySource::system_rng();
627        let data = source.get_bytes(500);
628        let _ = monitor.evaluate(&data);
629
630        assert_eq!(monitor.history().len(), 1);
631
632        monitor.clear_history();
633        assert_eq!(monitor.history().len(), 0);
634    }
635
636    #[test]
637    fn test_entropy_quality_serialization() {
638        let quality = EntropyQuality {
639            shannon_entropy: 7.9,
640            min_entropy: 6.0,
641            chi_squared: 255.0,
642            chi_squared_pvalue: 0.5,
643            serial_correlation: 0.05,
644            monte_carlo_pi_error: 0.05,
645            sample_size: 1000,
646            timestamp: SystemTime::now(),
647            health_tests_passed: true,
648        };
649
650        let serialized = crate::codec::encode(&quality).unwrap();
651        let deserialized: EntropyQuality = crate::codec::decode(&serialized).unwrap();
652
653        assert!((deserialized.shannon_entropy - quality.shannon_entropy).abs() < 0.01);
654        assert_eq!(deserialized.sample_size, quality.sample_size);
655    }
656}