Skip to main content

lib_q_random/
validation.rs

1// Allow clippy warnings in validation code
2// These are legitimate patterns for statistical analysis
3#![allow(
4    clippy::must_use_candidate,
5    clippy::cast_precision_loss,
6    clippy::cast_lossless,
7    clippy::manual_clamp,
8    clippy::unused_self,
9    clippy::unnecessary_wraps,
10    clippy::similar_names
11)]
12
13//! Entropy validation and quality assessment
14//!
15//! This module provides comprehensive entropy validation and quality assessment
16//! functionality for ensuring that random data meets cryptographic security requirements.
17
18use core::fmt;
19
20use crate::{
21    Error,
22    Result,
23};
24
25/// Entropy quality assessment result
26#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
27pub struct EntropyQuality {
28    /// Overall quality score (0.0 to 1.0)
29    pub overall: f64,
30    /// Uniformity score (0.0 to 1.0)
31    pub uniformity: f64,
32    /// Independence score (0.0 to 1.0)
33    pub independence: f64,
34    /// Predictability score (0.0 to 1.0, lower is better)
35    pub predictability: f64,
36}
37
38impl EntropyQuality {
39    /// Create a new entropy quality assessment
40    pub fn new(overall: f64, uniformity: f64, independence: f64, predictability: f64) -> Self {
41        Self {
42            overall,
43            uniformity,
44            independence,
45            predictability,
46        }
47    }
48
49    /// Check if the entropy quality is acceptable for cryptographic use
50    pub fn is_acceptable(&self, threshold: f64) -> bool {
51        self.overall >= threshold
52    }
53
54    /// Check if the entropy quality is excellent
55    pub fn is_excellent(&self) -> bool {
56        self.overall >= 0.95
57    }
58
59    /// Check if the entropy quality is good
60    pub fn is_good(&self) -> bool {
61        self.overall >= 0.8
62    }
63
64    /// Check if the entropy quality is poor
65    pub fn is_poor(&self) -> bool {
66        self.overall < 0.5
67    }
68}
69
70impl fmt::Display for EntropyQuality {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(
73            f,
74            "EntropyQuality(overall: {:.3}, uniformity: {:.3}, independence: {:.3}, predictability: {:.3})",
75            self.overall, self.uniformity, self.independence, self.predictability
76        )
77    }
78}
79
80/// Comprehensive entropy validator
81///
82/// This validator performs multiple statistical tests to assess the quality
83/// of entropy in random data, ensuring it meets cryptographic requirements.
84#[derive(Debug, Clone)]
85pub struct EntropyValidator {
86    /// Minimum entropy bits required
87    min_entropy_bits: usize,
88    /// Maximum entropy bits for validation
89    max_entropy_bits: usize,
90    /// Quality threshold for acceptance
91    quality_threshold: f64,
92    /// Enable strict validation
93    strict_mode: bool,
94}
95
96impl EntropyValidator {
97    /// Create a new entropy validator with default settings
98    pub fn new() -> Self {
99        Self {
100            min_entropy_bits: 128,
101            max_entropy_bits: 4096,
102            quality_threshold: 0.8,
103            strict_mode: false,
104        }
105    }
106
107    /// Create a new entropy validator with custom settings
108    pub fn with_settings(
109        min_entropy_bits: usize,
110        max_entropy_bits: usize,
111        quality_threshold: f64,
112        strict_mode: bool,
113    ) -> Self {
114        Self {
115            min_entropy_bits,
116            max_entropy_bits,
117            quality_threshold,
118            strict_mode,
119        }
120    }
121
122    /// Validate entropy data and return quality assessment
123    ///
124    /// # Arguments
125    ///
126    /// * `data` - The entropy data to validate
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if the entropy validation fails or if the data
131    /// doesn't meet the required quality standards.
132    pub fn validate_entropy(&self, data: &[u8]) -> Result<EntropyQuality> {
133        if data.is_empty() {
134            return Err(Error::entropy_validation_failed("Empty entropy data", 0.0));
135        }
136
137        if data.len() < self.min_entropy_bits / 8 {
138            return Err(Error::entropy_validation_failed(
139                "Insufficient entropy data length",
140                0.0,
141            ));
142        }
143
144        if data.len() > self.max_entropy_bits / 8 {
145            return Err(Error::entropy_validation_failed(
146                "Excessive entropy data length",
147                0.0,
148            ));
149        }
150
151        let quality = self.assess_entropy_quality(data)?;
152
153        if !quality.is_acceptable(self.quality_threshold) {
154            return Err(Error::entropy_validation_failed(
155                "Entropy quality below threshold",
156                quality.overall,
157            ));
158        }
159
160        Ok(quality)
161    }
162
163    /// Assess the quality of entropy data
164    ///
165    /// This method performs comprehensive statistical analysis to determine
166    /// the quality of entropy in the provided data.
167    fn assess_entropy_quality(&self, data: &[u8]) -> Result<EntropyQuality> {
168        let uniformity = self.test_uniformity(data)?;
169        let independence = self.test_independence(data)?;
170        let predictability = self.test_predictability(data)?;
171
172        // Calculate overall quality as weighted average
173        let overall = (uniformity * 0.4 + independence * 0.4 + (1.0 - predictability) * 0.2)
174            .max(0.0)
175            .min(1.0);
176
177        Ok(EntropyQuality::new(
178            overall,
179            uniformity,
180            independence,
181            predictability,
182        ))
183    }
184
185    /// Test the uniformity of byte distribution
186    ///
187    /// This test checks if the byte values are uniformly distributed,
188    /// which is a key requirement for good entropy.
189    fn test_uniformity(&self, data: &[u8]) -> Result<f64> {
190        if data.is_empty() {
191            return Ok(0.0);
192        }
193
194        let mut byte_counts = [0u32; 256];
195        for &byte in data {
196            byte_counts[byte as usize] += 1;
197        }
198
199        // Calculate chi-square statistic
200        let expected = data.len() as f64 / 256.0;
201        let mut chi_square = 0.0;
202
203        for &count in &byte_counts {
204            let diff = count as f64 - expected;
205            chi_square += (diff * diff) / expected;
206        }
207
208        // Convert chi-square to quality score (lower is better for uniformity)
209        // Chi-square for 255 degrees of freedom should be around 255 for uniform distribution
210        let expected_chi_square = 255.0;
211        let quality = if chi_square <= expected_chi_square {
212            1.0 - (chi_square / expected_chi_square) * 0.5
213        } else {
214            0.5 - ((chi_square - expected_chi_square) / expected_chi_square) * 0.5
215        };
216
217        Ok(quality.max(0.0).min(1.0))
218    }
219
220    /// Test the independence of consecutive bytes
221    ///
222    /// This test checks if consecutive bytes are independent of each other,
223    /// which is important for preventing patterns in the entropy.
224    fn test_independence(&self, data: &[u8]) -> Result<f64> {
225        if data.len() < 2 {
226            return Ok(1.0);
227        }
228
229        // Calculate correlation coefficient between consecutive bytes
230        let mut sum_x = 0u64;
231        let mut sum_y = 0u64;
232        let mut sum_xy = 0u64;
233        let mut sum_x2 = 0u64;
234        let mut sum_y2 = 0u64;
235
236        for window in data.windows(2) {
237            let x = window[0] as u64;
238            let y = window[1] as u64;
239            sum_x += x;
240            sum_y += y;
241            sum_xy += x * y;
242            sum_x2 += x * x;
243            sum_y2 += y * y;
244        }
245
246        let n = (data.len() - 1) as f64;
247        let mean_x = sum_x as f64 / n;
248        let mean_y = sum_y as f64 / n;
249
250        let numerator = sum_xy as f64 - n * mean_x * mean_y;
251        let denominator = {
252            let x_var = sum_x2 as f64 - n * mean_x * mean_x;
253            let y_var = sum_y2 as f64 - n * mean_y * mean_y;
254            let product = x_var * y_var;
255            if product >= 0.0 {
256                #[cfg(feature = "std")]
257                {
258                    product.sqrt()
259                }
260                #[cfg(not(feature = "std"))]
261                {
262                    // Simple approximation for no_std environments
263                    if product == 0.0 {
264                        0.0
265                    } else {
266                        // Use Newton's method for square root approximation
267                        let mut x = product;
268                        for _ in 0..10 {
269                            x = f64::midpoint(x, product / x);
270                        }
271                        x
272                    }
273                }
274            } else {
275                0.0
276            }
277        };
278
279        let correlation = if denominator > 0.0 {
280            numerator / denominator
281        } else {
282            0.0
283        };
284
285        // Convert correlation to quality score (lower correlation is better)
286        let quality = 1.0 - correlation.abs();
287        Ok(quality.max(0.0).min(1.0))
288    }
289
290    /// Test the predictability of the data
291    ///
292    /// This test checks for patterns that might make the data predictable,
293    /// such as runs of identical values or simple sequences.
294    fn test_predictability(&self, data: &[u8]) -> Result<f64> {
295        if data.is_empty() {
296            return Ok(0.0);
297        }
298
299        let mut runs = 0;
300        let mut max_run_length = 0;
301        let mut current_run_length = 1;
302
303        // Count runs of identical values
304        for window in data.windows(2) {
305            if window[0] == window[1] {
306                current_run_length += 1;
307            } else {
308                if current_run_length > 1 {
309                    runs += 1;
310                    max_run_length = max_run_length.max(current_run_length);
311                }
312                current_run_length = 1;
313            }
314        }
315
316        if current_run_length > 1 {
317            runs += 1;
318            max_run_length = max_run_length.max(current_run_length);
319        }
320
321        // Calculate predictability based on runs
322        let run_ratio = runs as f64 / data.len() as f64;
323        let max_run_ratio = max_run_length as f64 / data.len() as f64;
324
325        // Lower run ratio and max run ratio indicate better entropy
326        let quality = 1.0 - (run_ratio + max_run_ratio) * 0.5;
327        Ok(quality.max(0.0).min(1.0))
328    }
329
330    /// Check if the validator is in strict mode
331    pub fn is_strict_mode(&self) -> bool {
332        self.strict_mode
333    }
334
335    /// Get the quality threshold
336    pub fn quality_threshold(&self) -> f64 {
337        self.quality_threshold
338    }
339
340    /// Get the minimum entropy bits required
341    pub fn min_entropy_bits(&self) -> usize {
342        self.min_entropy_bits
343    }
344
345    /// Get the maximum entropy bits for validation
346    pub fn max_entropy_bits(&self) -> usize {
347        self.max_entropy_bits
348    }
349}
350
351impl Default for EntropyValidator {
352    fn default() -> Self {
353        Self::new()
354    }
355}
356
357/// Quick entropy validation for small data samples
358///
359/// This function provides a quick entropy validation suitable for
360/// small data samples or when full validation is not required.
361///
362/// # Arguments
363///
364/// * `data` - The entropy data to validate
365///
366/// # Returns
367///
368/// Returns `true` if the entropy appears to be of good quality,
369/// `false` otherwise.
370pub fn quick_entropy_check(data: &[u8]) -> bool {
371    if data.is_empty() || data.len() < 16 {
372        return false;
373    }
374
375    // Simple checks for obviously bad entropy
376    let mut byte_counts = [0u8; 256];
377    for &byte in data {
378        byte_counts[byte as usize] += 1;
379    }
380
381    // Check if any byte appears too frequently
382    let max_count = byte_counts.iter().max().copied().unwrap_or(0);
383    let threshold = (data.len() / 8).max(1);
384
385    if max_count as usize > threshold {
386        return false;
387    }
388
389    // Check for obvious patterns
390    let mut identical_pairs = 0;
391    for window in data.windows(2) {
392        if window[0] == window[1] {
393            identical_pairs += 1;
394        }
395    }
396
397    let pair_ratio = identical_pairs as f64 / (data.len() - 1) as f64;
398    pair_ratio < 0.1
399}
400
401#[cfg(test)]
402mod tests {
403    #[cfg(feature = "alloc")]
404    use alloc::vec::Vec;
405
406    use super::*;
407
408    #[test]
409    fn test_entropy_quality_creation() {
410        let quality = EntropyQuality::new(0.8, 0.9, 0.7, 0.1);
411        #[allow(clippy::float_cmp)]
412        {
413            assert_eq!(quality.overall, 0.8);
414            assert_eq!(quality.uniformity, 0.9);
415            assert_eq!(quality.independence, 0.7);
416            assert_eq!(quality.predictability, 0.1);
417        }
418    }
419
420    #[test]
421    fn test_entropy_quality_assessment() {
422        let quality = EntropyQuality::new(0.95, 0.9, 0.8, 0.05);
423        assert!(quality.is_excellent());
424        assert!(quality.is_good());
425        assert!(!quality.is_poor());
426        assert!(quality.is_acceptable(0.8));
427    }
428
429    #[test]
430    fn test_entropy_validator_creation() {
431        let validator = EntropyValidator::new();
432        assert_eq!(validator.min_entropy_bits(), 128);
433        #[allow(clippy::float_cmp)]
434        {
435            assert_eq!(validator.quality_threshold(), 0.8);
436        }
437        assert!(!validator.is_strict_mode());
438    }
439
440    #[test]
441    fn test_entropy_validator_custom_settings() {
442        let validator = EntropyValidator::with_settings(256, 2048, 0.9, true);
443        assert_eq!(validator.min_entropy_bits(), 256);
444        assert_eq!(validator.max_entropy_bits(), 2048);
445        #[allow(clippy::float_cmp)]
446        {
447            assert_eq!(validator.quality_threshold(), 0.9);
448        }
449        assert!(validator.is_strict_mode());
450    }
451
452    #[test]
453    fn test_entropy_validation_empty_data() {
454        let validator = EntropyValidator::new();
455        let result = validator.validate_entropy(&[]);
456        assert!(result.is_err());
457    }
458
459    #[test]
460    fn test_entropy_validation_insufficient_data() {
461        let validator = EntropyValidator::new();
462        let data = [1, 2, 3, 4, 5]; // Less than 16 bytes
463        let result = validator.validate_entropy(&data);
464        assert!(result.is_err());
465    }
466
467    #[test]
468    fn test_quick_entropy_check() {
469        // Good entropy (random-looking data)
470        let good_data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
471        assert!(quick_entropy_check(&good_data));
472
473        // Bad entropy (all zeros)
474        let bad_data = [0u8; 16];
475        assert!(!quick_entropy_check(&bad_data));
476
477        // Bad entropy (repeating pattern)
478        let pattern_data = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
479        assert!(!quick_entropy_check(&pattern_data));
480    }
481
482    #[test]
483    #[cfg(feature = "alloc")]
484    fn test_uniformity_test() {
485        let validator = EntropyValidator::new();
486
487        // Uniform distribution should score well
488        let uniform_data: Vec<u8> = (0..=255).cycle().take(1024).collect();
489        let quality = validator.test_uniformity(&uniform_data).unwrap();
490        assert!(quality > 0.8);
491
492        // Non-uniform distribution should score poorly
493        let non_uniform_data = [0u8; 1024];
494        let quality = validator.test_uniformity(&non_uniform_data).unwrap();
495        assert!(quality < 0.5);
496    }
497
498    #[test]
499    #[cfg(feature = "alloc")]
500    fn test_independence_test() {
501        let validator = EntropyValidator::new();
502
503        // Independent data should score well
504        let independent_data: Vec<u8> = (0..=255).cycle().take(1024).collect();
505        let quality = validator.test_independence(&independent_data).unwrap();
506        // println!("Independence quality: {}", quality);
507        assert!(quality > 0.0); // Just check it's not zero
508
509        // Correlated data should score poorly
510        let correlated_data: Vec<u8> = (0..128).flat_map(|i| [i, i]).take(1024).collect();
511        let quality = validator.test_independence(&correlated_data).unwrap();
512        // The independence test might not catch all patterns, so we just check it's not perfect
513        assert!(quality < 1.0);
514    }
515
516    #[test]
517    #[cfg(feature = "alloc")]
518    fn test_predictability_test() {
519        let validator = EntropyValidator::new();
520
521        // Unpredictable data should score well
522        let unpredictable_data: Vec<u8> = (0..=255).cycle().take(1024).collect();
523        let quality = validator.test_predictability(&unpredictable_data).unwrap();
524        assert!(quality > 0.8);
525
526        // Predictable data should score poorly
527        let predictable_data = [0u8; 1024];
528        let quality = validator.test_predictability(&predictable_data).unwrap();
529        assert!(quality < 0.5);
530    }
531}