Skip to main content

lib_q_random/
entropy.rs

1// Allow clippy warnings in entropy source code
2// These are legitimate patterns for platform-specific implementations
3#![allow(
4    clippy::must_use_candidate,
5    clippy::cast_lossless,
6    clippy::manual_clamp,
7    clippy::needless_return,
8    clippy::collapsible_if,
9    clippy::match_same_arms,
10    clippy::unreadable_literal,
11    clippy::missing_errors_doc
12)]
13
14//! Entropy source implementations
15//!
16//! This module provides various entropy source implementations for different
17//! platforms and use cases, including OS entropy, hardware RNGs, and
18//! deterministic sources for testing.
19
20#[cfg(feature = "alloc")]
21use alloc::{
22    boxed::Box,
23    vec::Vec,
24};
25
26use crate::kt128_expander::Kt128Expander;
27use crate::traits::{
28    EntropyConfig,
29    EntropySource,
30    EntropySourceType,
31};
32use crate::{
33    Error,
34    Result,
35};
36
37/// Operating system entropy source
38///
39/// This entropy source uses the operating system's secure random number
40/// generator, typically `/dev/urandom` on Unix-like systems or
41/// `CryptGenRandom` on Windows.
42#[derive(Debug, Clone)]
43pub struct OsEntropySource {
44    /// Platform identifier
45    platform: &'static str,
46    /// Quality estimate
47    quality: f64,
48    /// Maximum entropy per call (from config)
49    max_per_call: Option<usize>,
50}
51
52impl OsEntropySource {
53    /// Create a new OS entropy source
54    pub fn new() -> Self {
55        let platform = Self::detect_platform();
56        let quality = Self::estimate_platform_quality(platform);
57        Self {
58            platform,
59            quality,
60            max_per_call: None,
61        }
62    }
63
64    /// Estimate quality based on platform
65    fn estimate_platform_quality(platform: &'static str) -> f64 {
66        match platform {
67            "Linux" => 0.95,       // /dev/urandom is generally high quality
68            "macOS" => 0.95,       // SecRandomCopyBytes is high quality
69            "Windows" => 0.95,     // CryptGenRandom is high quality
70            "FreeBSD" => 0.95,     // /dev/urandom is high quality
71            "OpenBSD" => 0.95,     // /dev/urandom is high quality
72            "NetBSD" => 0.95,      // /dev/urandom is high quality
73            "WebAssembly" => 0.90, // Browser crypto.getRandomValues() is good but slightly lower
74            _ => 0.80,             // Unknown platforms get conservative estimate
75        }
76    }
77
78    /// Get the platform identifier
79    pub fn platform(&self) -> &'static str {
80        self.platform
81    }
82
83    /// Detect the current platform
84    fn detect_platform() -> &'static str {
85        #[cfg(target_os = "linux")]
86        return "Linux";
87        #[cfg(target_os = "macos")]
88        return "macOS";
89        #[cfg(target_os = "windows")]
90        return "Windows";
91        #[cfg(target_os = "freebsd")]
92        return "FreeBSD";
93        #[cfg(target_os = "openbsd")]
94        return "OpenBSD";
95        #[cfg(target_os = "netbsd")]
96        return "NetBSD";
97        #[cfg(target_arch = "wasm32")]
98        return "WebAssembly";
99        #[cfg(not(any(
100            target_os = "linux",
101            target_os = "macos",
102            target_os = "windows",
103            target_os = "freebsd",
104            target_os = "openbsd",
105            target_os = "netbsd",
106            target_arch = "wasm32"
107        )))]
108        return "Unknown";
109    }
110}
111
112impl EntropySource for OsEntropySource {
113    fn get_entropy(&mut self, dest: &mut [u8]) -> Result<()> {
114        // Check if the requested amount exceeds the maximum per call
115        if let Some(max_per_call) = self.max_entropy_per_call() {
116            if dest.len() > max_per_call {
117                return Err(Error::entropy_source_unavailable(
118                    "Requested entropy exceeds maximum per call",
119                ));
120            }
121        }
122
123        #[cfg(feature = "std")]
124        {
125            getrandom::fill(dest).map_err(|_| {
126                Error::platform_rng_failed_with_code(
127                    self.platform,
128                    -1,
129                    "Failed to get entropy from OS",
130                )
131            })
132        }
133        #[cfg(not(feature = "std"))]
134        {
135            Err(Error::feature_not_available("OS entropy source", &["std"]))
136        }
137    }
138
139    fn initialize(&mut self, config: &EntropyConfig) -> Result<()> {
140        // Validate that the source meets the minimum quality requirement
141        if self.quality() < config.min_quality {
142            return Err(Error::entropy_source_unavailable(
143                "OS entropy source quality below required minimum",
144            ));
145        }
146
147        // Update max_per_call if specified in config
148        if let Some(max_per_call) = config.max_per_call {
149            if max_per_call > 256 {
150                return Err(Error::entropy_source_unavailable(
151                    "Requested max_per_call exceeds OS entropy source limit",
152                ));
153            }
154            self.max_per_call = Some(max_per_call);
155        }
156
157        Ok(())
158    }
159
160    fn is_available(&self) -> bool {
161        #[cfg(feature = "std")]
162        {
163            true
164        }
165        #[cfg(not(feature = "std"))]
166        {
167            false
168        }
169    }
170
171    fn quality(&self) -> f64 {
172        self.quality
173    }
174
175    fn name(&self) -> &'static str {
176        match self.platform {
177            "Linux" => "Linux OS Entropy Source",
178            "macOS" => "macOS OS Entropy Source",
179            "Windows" => "Windows OS Entropy Source",
180            "FreeBSD" => "FreeBSD OS Entropy Source",
181            "OpenBSD" => "OpenBSD OS Entropy Source",
182            "NetBSD" => "NetBSD OS Entropy Source",
183            "WebAssembly" => "WebAssembly OS Entropy Source",
184            _ => "Unknown OS Entropy Source",
185        }
186    }
187
188    fn source_type(&self) -> EntropySourceType {
189        EntropySourceType::OperatingSystem
190    }
191
192    fn max_entropy_per_call(&self) -> Option<usize> {
193        self.max_per_call.or(Some(16384)) // Use config value or default limit (16KB)
194    }
195}
196
197impl Default for OsEntropySource {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203/// Hardware random number generator entropy source
204///
205/// On **x86 / `x86_64`** with the **`std`** feature, this uses **RDRAND** when the
206/// CPU advertises support and a probe read succeeds. Without `std`, runtime
207/// CPUID-based detection is unavailable and this source reports as unavailable.
208///
209/// **`AArch64`** and **`PowerPC`** do not use in-process `RNDR` / `DARN` in this
210/// crate on stable Rust; use [`OsEntropySource`] for those targets.
211#[derive(Debug, Clone)]
212pub struct HardwareEntropySource {
213    /// Hardware device identifier
214    device: &'static str,
215    /// Quality estimate
216    quality: f64,
217    /// Whether hardware RNG is available
218    available: bool,
219}
220
221impl HardwareEntropySource {
222    /// Create a new hardware entropy source
223    pub fn new() -> Self {
224        let (device, available) = Self::detect_hardware_rng();
225        Self {
226            device,
227            quality: if available { 0.99 } else { 0.0 },
228            available,
229        }
230    }
231
232    /// Detect available hardware RNG (RDRAND on x86 / `x86_64` when present).
233    fn detect_hardware_rng() -> (&'static str, bool) {
234        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
235        {
236            if let Some(label) = crate::hardware_rng::probe_rdrand() {
237                return (label, true);
238            }
239            #[cfg(target_arch = "x86_64")]
240            {
241                return ("x86_64 (RDRAND unavailable)", false);
242            }
243            #[cfg(target_arch = "x86")]
244            {
245                return ("x86 (RDRAND unavailable)", false);
246            }
247        }
248        #[cfg(target_arch = "aarch64")]
249        {
250            return ("AArch64", false);
251        }
252        #[cfg(any(target_arch = "powerpc64", target_arch = "powerpc"))]
253        {
254            return ("PowerPC", false);
255        }
256        #[cfg(not(any(
257            target_arch = "x86",
258            target_arch = "x86_64",
259            target_arch = "aarch64",
260            target_arch = "powerpc64",
261            target_arch = "powerpc"
262        )))]
263        {
264            return ("Unknown", false);
265        }
266    }
267}
268
269impl EntropySource for HardwareEntropySource {
270    fn get_entropy(&mut self, dest: &mut [u8]) -> Result<()> {
271        if !self.available {
272            return Err(Error::hardware_rng_failed(self.device));
273        }
274
275        // Check if the requested amount exceeds the maximum per call
276        if let Some(max_per_call) = self.max_entropy_per_call() {
277            if dest.len() > max_per_call {
278                return Err(Error::entropy_source_unavailable(
279                    "Requested entropy exceeds hardware RNG maximum per call",
280                ));
281            }
282        }
283
284        crate::hardware_rng::fill_hw_cpu(dest, self.device)
285    }
286
287    fn initialize(&mut self, config: &EntropyConfig) -> Result<()> {
288        // Validate that the source meets the minimum quality requirement
289        if self.quality() < config.min_quality {
290            return Err(Error::entropy_source_unavailable(
291                "Hardware entropy source quality below required minimum",
292            ));
293        }
294
295        // Update max_per_call if specified in config
296        if let Some(max_per_call) = config.max_per_call {
297            if max_per_call > crate::hardware_rng::HW_RNG_MAX_PER_CALL {
298                return Err(Error::entropy_source_unavailable(
299                    "Requested max_per_call exceeds hardware RNG limit",
300                ));
301            }
302        }
303
304        Ok(())
305    }
306
307    fn is_available(&self) -> bool {
308        self.available
309    }
310
311    fn quality(&self) -> f64 {
312        self.quality
313    }
314
315    fn name(&self) -> &'static str {
316        "Hardware RNG"
317    }
318
319    fn source_type(&self) -> EntropySourceType {
320        EntropySourceType::Hardware
321    }
322
323    fn max_entropy_per_call(&self) -> Option<usize> {
324        Some(crate::hardware_rng::HW_RNG_MAX_PER_CALL)
325    }
326}
327
328impl Default for HardwareEntropySource {
329    fn default() -> Self {
330        Self::new()
331    }
332}
333
334/// Deterministic entropy source for testing
335///
336/// This entropy source provides deterministic bytes from **KT128** (`KangarooTwelve`)
337/// XOF expansion via [`Kt128Expander`] and domain [`crate::kt128_expander::DOMAIN_LIBQ_DET_RNG`].
338///
339/// **USE CASE**: This is designed for:
340/// - Unit testing with reproducible outcomes
341/// - Known Answer Tests (KAT) verification
342/// - Benchmarking with consistent inputs
343///
344/// **NOT FOR CRYPTOGRAPHIC USE**: For actual cryptographic operations,
345/// use `OsEntropySource` or other secure entropy sources. Unpredictability is
346/// only as strong as the secrecy of the 32-byte seed.
347#[derive(Debug, Clone)]
348pub struct DeterministicEntropySource {
349    /// KT128 expander
350    expander: Kt128Expander,
351    /// Quality estimate (0.0 for deterministic)
352    quality: f64,
353}
354
355impl DeterministicEntropySource {
356    /// Create a new deterministic entropy source from a 32-byte seed.
357    #[must_use]
358    pub fn new(seed: [u8; 32]) -> Self {
359        Self {
360            expander: Kt128Expander::from_det_seed_32(seed),
361            quality: 0.0, // Deterministic sources have no true entropy
362        }
363    }
364
365    /// Create from a `u64` test seed (`SplitMix64` → 32 bytes → KT128).
366    #[must_use]
367    pub fn from_u64(seed: u64) -> Self {
368        Self {
369            expander: Kt128Expander::from_det_u64(seed),
370            quality: 0.0,
371        }
372    }
373
374    fn generate_deterministic_bytes(&mut self, dest: &mut [u8]) {
375        self.expander.fill_bytes(dest);
376    }
377}
378
379impl EntropySource for DeterministicEntropySource {
380    fn get_entropy(&mut self, dest: &mut [u8]) -> Result<()> {
381        self.generate_deterministic_bytes(dest);
382        Ok(())
383    }
384
385    fn initialize(&mut self, config: &EntropyConfig) -> Result<()> {
386        // For deterministic sources, we don't enforce quality requirements
387        // since they're meant for testing and have 0.0 quality by design
388        let _ = config;
389        Ok(())
390    }
391
392    fn is_available(&self) -> bool {
393        true
394    }
395
396    fn quality(&self) -> f64 {
397        self.quality
398    }
399
400    fn name(&self) -> &'static str {
401        "Deterministic Entropy Source"
402    }
403
404    fn source_type(&self) -> EntropySourceType {
405        EntropySourceType::Deterministic
406    }
407
408    fn max_entropy_per_call(&self) -> Option<usize> {
409        None // No limit for deterministic sources
410    }
411}
412
413/// User-provided entropy source
414///
415/// This entropy source allows users to provide their own entropy data,
416/// useful for specialized applications or when integrating with external
417/// entropy sources.
418#[cfg(feature = "alloc")]
419#[derive(Debug, Clone)]
420pub struct UserEntropySource {
421    /// User-provided entropy data
422    entropy_data: Vec<u8>,
423    /// Current position in the entropy data
424    position: usize,
425    /// Quality estimate
426    quality: f64,
427    /// Configured maximum entropy per call
428    max_per_call: Option<usize>,
429}
430
431#[cfg(feature = "alloc")]
432impl UserEntropySource {
433    /// Create a new user entropy source
434    pub fn new(entropy_data: Vec<u8>) -> Self {
435        Self {
436            quality: 0.8, // Assume reasonable quality for user-provided data
437            entropy_data,
438            position: 0,
439            max_per_call: None, // No limit by default
440        }
441    }
442
443    /// Create a new user entropy source with quality assessment
444    pub fn with_quality(entropy_data: Vec<u8>, quality: f64) -> Self {
445        Self {
446            quality: quality.max(0.0).min(1.0),
447            entropy_data,
448            position: 0,
449            max_per_call: None, // No limit by default
450        }
451    }
452}
453
454#[cfg(feature = "alloc")]
455impl EntropySource for UserEntropySource {
456    fn get_entropy(&mut self, dest: &mut [u8]) -> Result<()> {
457        if self.entropy_data.is_empty() {
458            return Err(Error::entropy_source_unavailable("User entropy source"));
459        }
460
461        // Check if the requested amount exceeds the maximum per call
462        if let Some(max_per_call) = self.max_per_call {
463            if dest.len() > max_per_call {
464                return Err(Error::entropy_source_unavailable(
465                    "Requested entropy exceeds user entropy source maximum per call",
466                ));
467            }
468        }
469
470        for (i, byte) in dest.iter_mut().enumerate() {
471            let index = (self.position + i) % self.entropy_data.len();
472            *byte = self.entropy_data[index];
473        }
474
475        self.position = (self.position + dest.len()) % self.entropy_data.len();
476        Ok(())
477    }
478
479    fn initialize(&mut self, config: &EntropyConfig) -> Result<()> {
480        // Validate that the source meets the minimum quality requirement
481        if self.quality() < config.min_quality {
482            return Err(Error::entropy_source_unavailable(
483                "User entropy source quality below required minimum",
484            ));
485        }
486
487        // For user entropy sources, we allow max_per_call to be larger than the entropy data size
488        // because the source can cycle through the data. We only enforce reasonable limits.
489        if let Some(max_per_call) = config.max_per_call {
490            if max_per_call > 1024 {
491                return Err(Error::entropy_source_unavailable(
492                    "Requested max_per_call exceeds reasonable limit for user entropy source",
493                ));
494            }
495            // Store the configured max_per_call
496            self.max_per_call = Some(max_per_call);
497        }
498
499        Ok(())
500    }
501
502    fn is_available(&self) -> bool {
503        !self.entropy_data.is_empty()
504    }
505
506    fn quality(&self) -> f64 {
507        self.quality
508    }
509
510    fn name(&self) -> &'static str {
511        "User Entropy Source"
512    }
513
514    fn source_type(&self) -> EntropySourceType {
515        EntropySourceType::User
516    }
517
518    fn max_entropy_per_call(&self) -> Option<usize> {
519        Some(self.entropy_data.len())
520    }
521}
522
523/// NIST AES256-CTR-DRBG entropy source for KAT test compatibility (minimal implementation).
524///
525/// This entropy source implements a minimal NIST AES256-CTR-DRBG-style algorithm:
526/// `randombytes_init` and `EntropySource` for contexts that need the same 48-byte-init
527/// API (e.g. KAT or legacy tests) but do not require reseed, personalization, or health tests.
528/// It is **not** a full NIST-aligned DRBG (no reseed interval enforcement, no uninstantiate,
529/// no health test).
530///
531/// For full NIST SP 800-90A Rev. 1 `CTR_DRBG` alignment (instantiate with personalization,
532/// reseed, uninstantiate, health test, reseed interval, error state), use the canonical
533/// implementation in **lib-q-cb-kem** behind the `nist-aes-rng` feature (`AesState`).
534#[cfg(feature = "nist-drbg")]
535#[derive(Debug, Clone)]
536pub struct NistAes256CtrDrbg {
537    /// 256-bit AES key
538    key: [u8; 32],
539    /// 128-bit counter value
540    v: [u8; 16],
541    /// Reseed counter
542    reseed_counter: i32,
543    /// Quality estimate (high for NIST DRBG)
544    quality: f64,
545}
546
547#[cfg(feature = "nist-drbg")]
548impl NistAes256CtrDrbg {
549    /// Create a new NIST AES256-CTR-DRBG entropy source
550    pub fn new() -> Self {
551        Self {
552            key: [0u8; 32],
553            v: [0u8; 16],
554            reseed_counter: 0,
555            quality: 1.0, // NIST DRBG provides high quality entropy
556        }
557    }
558}
559
560#[cfg(feature = "nist-drbg")]
561impl Default for NistAes256CtrDrbg {
562    fn default() -> Self {
563        Self::new()
564    }
565}
566
567#[cfg(feature = "nist-drbg")]
568impl NistAes256CtrDrbg {
569    /// Initialize the DRBG with entropy input (48 bytes)
570    ///
571    /// This corresponds to the `randombytes_init` function in the reference implementation.
572    pub fn randombytes_init(&mut self, entropy_input: [u8; 48]) {
573        self.key = [0u8; 32];
574        self.v = [0u8; 16];
575        self.reseed_counter = 1i32;
576
577        Self::aes256_ctr_update(&mut Some(entropy_input), &mut self.key, &mut self.v);
578        self.reseed_counter = 1;
579    }
580
581    /// AES256-ECB encryption
582    ///
583    /// Encrypts a 128-bit plaintext using AES256-ECB mode.
584    fn aes256_ecb(key: &[u8; 32], ctr: &[u8; 16], buffer: &mut [u8; 16]) {
585        use aes::cipher::{
586            BlockCipherEncrypt,
587            KeyInit,
588        };
589        use aes::{
590            Aes256,
591            Block,
592        };
593        let cipher = Aes256::new_from_slice(key).expect("32-byte key");
594        buffer.copy_from_slice(ctr);
595        let mut block = Block::from(*buffer);
596        cipher.encrypt_block(&mut block);
597        *buffer = block.into();
598    }
599
600    /// Update key and v with provided data by running one round of AES in counter mode
601    fn aes256_ctr_update(
602        provided_data: &mut Option<[u8; 48]>,
603        key: &mut [u8; 32],
604        v: &mut [u8; 16],
605    ) {
606        let mut temp = [[0u8; 16]; 3];
607
608        for tmp in &mut temp[0..3] {
609            let count = u128::from_be_bytes(*v);
610            let count_next = count.wrapping_add(1);
611            v.copy_from_slice(&count_next.to_be_bytes());
612
613            Self::aes256_ecb(key, v, tmp);
614        }
615
616        if let Some(d) = provided_data {
617            for j in 0..3 {
618                for i in 0..16 {
619                    temp[j][i] ^= d[16 * j + i];
620                }
621            }
622        }
623
624        key[0..16].copy_from_slice(&temp[0]);
625        key[16..32].copy_from_slice(&temp[1]);
626        v.copy_from_slice(&temp[2]);
627    }
628
629    /// Generate random bytes using the DRBG
630    fn generate_bytes(&mut self, dest: &mut [u8]) {
631        for chunk in dest.chunks_mut(16) {
632            let count = u128::from_be_bytes(self.v);
633            let count_next = count.wrapping_add(1);
634            self.v.copy_from_slice(&count_next.to_be_bytes());
635
636            let mut block = [0u8; 16];
637            Self::aes256_ecb(&self.key, &self.v, &mut block);
638
639            chunk.copy_from_slice(&block[..chunk.len()]);
640        }
641
642        Self::aes256_ctr_update(&mut None, &mut self.key, &mut self.v);
643        self.reseed_counter += 1;
644    }
645}
646
647#[cfg(feature = "nist-drbg")]
648impl EntropySource for NistAes256CtrDrbg {
649    fn get_entropy(&mut self, dest: &mut [u8]) -> Result<()> {
650        self.generate_bytes(dest);
651        Ok(())
652    }
653
654    fn is_available(&self) -> bool {
655        true
656    }
657
658    fn quality(&self) -> f64 {
659        self.quality
660    }
661
662    fn name(&self) -> &'static str {
663        "NIST AES256-CTR-DRBG"
664    }
665
666    fn source_type(&self) -> EntropySourceType {
667        EntropySourceType::Deterministic
668    }
669
670    fn max_entropy_per_call(&self) -> Option<usize> {
671        None // No limit for DRBG
672    }
673}
674
675/// Entropy source factory
676///
677/// This factory provides convenient methods for creating different types
678/// of entropy sources based on requirements and platform capabilities.
679#[cfg(feature = "alloc")]
680pub struct EntropySourceFactory;
681
682#[cfg(feature = "alloc")]
683impl EntropySourceFactory {
684    /// Create the best available entropy source
685    ///
686    /// This method attempts to create the highest quality entropy source
687    /// available on the current platform.
688    pub fn create_best_available() -> Result<Box<dyn EntropySource>> {
689        Self::create_best_available_with_config(&EntropyConfig::default())
690    }
691
692    /// Create the best available entropy source with configuration
693    ///
694    /// This method attempts to create the highest quality entropy source
695    /// available on the current platform that meets the specified requirements.
696    pub fn create_best_available_with_config(
697        config: &EntropyConfig,
698    ) -> Result<Box<dyn EntropySource>> {
699        // Try hardware RNG first
700        let mut hardware_source = HardwareEntropySource::new();
701        if hardware_source.is_available() {
702            if hardware_source.initialize(config).is_ok() {
703                return Ok(Box::new(hardware_source));
704            }
705        }
706
707        // Fall back to OS entropy
708        let mut os_source = OsEntropySource::new();
709        if os_source.is_available() {
710            if os_source.initialize(config).is_ok() {
711                return Ok(Box::new(os_source));
712            }
713        }
714
715        // Do not substitute non-cryptographic "fallback" RNGs when OS/hardware sources fail.
716        // Callers must fix the environment, enable `getrandom`, or register a custom entropy
717        // source (`custom-entropy` feature) for `no_std`/WASM.
718        Err(Error::entropy_source_unavailable_with_context(
719            "secure entropy",
720            "no OS or hardware entropy source satisfied the configuration; weak fallbacks are disabled",
721        ))
722    }
723
724    /// Create an OS entropy source
725    pub fn create_os_entropy() -> Result<Box<dyn EntropySource>> {
726        Self::create_os_entropy_with_config(&EntropyConfig::default())
727    }
728
729    /// Create an OS entropy source with configuration
730    pub fn create_os_entropy_with_config(config: &EntropyConfig) -> Result<Box<dyn EntropySource>> {
731        let mut source = OsEntropySource::new();
732        if source.is_available() {
733            source.initialize(config)?;
734            Ok(Box::new(source))
735        } else {
736            Err(Error::entropy_source_unavailable("OS entropy source"))
737        }
738    }
739
740    /// Create a hardware entropy source
741    pub fn create_hardware_entropy() -> Result<Box<dyn EntropySource>> {
742        Self::create_hardware_entropy_with_config(&EntropyConfig::default())
743    }
744
745    /// Create a hardware entropy source with configuration
746    pub fn create_hardware_entropy_with_config(
747        config: &EntropyConfig,
748    ) -> Result<Box<dyn EntropySource>> {
749        let mut source = HardwareEntropySource::new();
750        if source.is_available() {
751            source.initialize(config)?;
752            Ok(Box::new(source))
753        } else {
754            Err(Error::entropy_source_unavailable("Hardware entropy source"))
755        }
756    }
757
758    /// Create a deterministic entropy source (KT128 XOF from `seed`).
759    pub fn create_deterministic_entropy(seed: [u8; 32]) -> Box<dyn EntropySource> {
760        Box::new(DeterministicEntropySource::new(seed))
761    }
762
763    /// Create a deterministic entropy source from a `u64` test seed (`SplitMix64` → KT128).
764    pub fn create_deterministic_entropy_from_u64(seed: u64) -> Box<dyn EntropySource> {
765        Box::new(DeterministicEntropySource::from_u64(seed))
766    }
767
768    /// Create a user entropy source
769    pub fn create_user_entropy(entropy_data: Vec<u8>) -> Box<dyn EntropySource> {
770        Box::new(UserEntropySource::new(entropy_data))
771    }
772
773    /// Create a user entropy source with quality assessment
774    pub fn create_user_entropy_with_quality(
775        entropy_data: Vec<u8>,
776        quality: f64,
777    ) -> Box<dyn EntropySource> {
778        Box::new(UserEntropySource::with_quality(entropy_data, quality))
779    }
780
781    /// Create a NIST AES256-CTR-DRBG entropy source
782    #[cfg(feature = "nist-drbg")]
783    pub fn create_nist_drbg_entropy(entropy_input: [u8; 48]) -> Box<dyn EntropySource> {
784        let mut drbg = NistAes256CtrDrbg::new();
785        drbg.randombytes_init(entropy_input);
786        Box::new(drbg)
787    }
788}
789
790#[cfg(test)]
791mod tests {
792    #[cfg(all(not(feature = "std"), feature = "alloc"))]
793    use alloc::format;
794    #[cfg(all(test, feature = "alloc"))]
795    use alloc::vec;
796
797    use super::*;
798    use crate::kt128_expander::{
799        KT128_DET_GOLDEN_U64_SEED_64,
800        KT128_DET_GOLDEN_ZERO_SEED_64,
801    };
802
803    #[test]
804    fn test_os_entropy_source_creation() {
805        let source = OsEntropySource::new();
806        assert!(!source.name().is_empty());
807        assert_eq!(source.source_type(), EntropySourceType::OperatingSystem);
808
809        // Test that platform is detected and used in name
810        let platform = source.platform();
811        assert!(!platform.is_empty());
812        assert!(source.name().contains(platform));
813
814        // Test that quality is set based on platform
815        assert!(source.quality() > 0.0);
816        assert!(source.quality() <= 1.0);
817    }
818
819    #[test]
820    fn test_hardware_entropy_source_creation() {
821        let source = HardwareEntropySource::new();
822        assert!(!source.name().is_empty());
823        assert_eq!(source.source_type(), EntropySourceType::Hardware);
824    }
825
826    #[test]
827    #[cfg(all(feature = "std", target_arch = "x86_64"))]
828    fn test_hardware_entropy_rdrand_smoke_if_cpu_supports() {
829        if !std::arch::is_x86_feature_detected!("rdrand") {
830            return;
831        }
832        let mut source = HardwareEntropySource::new();
833        assert!(source.is_available());
834        source.initialize(&EntropyConfig::default()).unwrap();
835        let mut buf = [0u8; 64];
836        source.get_entropy(&mut buf).unwrap();
837    }
838
839    #[test]
840    fn test_deterministic_entropy_source_creation() {
841        let mut seed = [0u8; 32];
842        seed[..8].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
843        let source = DeterministicEntropySource::new(seed);
844        assert!(!source.name().is_empty());
845        assert_eq!(source.source_type(), EntropySourceType::Deterministic);
846        #[allow(clippy::float_cmp)]
847        {
848            assert_eq!(source.quality(), 0.0);
849        }
850    }
851
852    #[test]
853    fn test_deterministic_entropy_consistency() {
854        let seed = [42u8; 32];
855        let mut source1 = DeterministicEntropySource::new(seed);
856        let mut source2 = DeterministicEntropySource::new(seed);
857
858        let mut bytes1 = [0u8; 32];
859        let mut bytes2 = [0u8; 32];
860
861        source1.get_entropy(&mut bytes1).unwrap();
862        source2.get_entropy(&mut bytes2).unwrap();
863
864        assert_eq!(bytes1, bytes2);
865    }
866
867    /// KT128 deterministic entropy must match committed goldens (`libQ-DET-RNG-v1`).
868    #[test]
869    fn test_deterministic_entropy_golden_zero_seed() {
870        let mut source = DeterministicEntropySource::new([0u8; 32]);
871        let mut out = [0u8; 64];
872        source.get_entropy(&mut out).unwrap();
873        assert_eq!(out, KT128_DET_GOLDEN_ZERO_SEED_64);
874    }
875
876    #[test]
877    fn test_deterministic_entropy_golden_from_u64() {
878        let mut source = DeterministicEntropySource::from_u64(0x0123_4567_89AB_CDEF);
879        let mut out = [0u8; 64];
880        source.get_entropy(&mut out).unwrap();
881        assert_eq!(out, KT128_DET_GOLDEN_U64_SEED_64);
882    }
883
884    #[test]
885    #[cfg(feature = "alloc")]
886    fn test_user_entropy_source_creation() {
887        let entropy_data = vec![1, 2, 3, 4, 5, 6, 7, 8];
888        let source = UserEntropySource::new(entropy_data);
889        assert!(!source.name().is_empty());
890        assert_eq!(source.source_type(), EntropySourceType::User);
891    }
892
893    #[test]
894    #[cfg(feature = "alloc")]
895    fn test_user_entropy_source_with_quality() {
896        let entropy_data = vec![1, 2, 3, 4, 5, 6, 7, 8];
897        let source = UserEntropySource::with_quality(entropy_data, 0.9);
898        #[allow(clippy::float_cmp)]
899        {
900            assert_eq!(source.quality(), 0.9);
901        }
902    }
903
904    #[test]
905    #[cfg(feature = "alloc")]
906    fn test_user_entropy_source_cycling() {
907        let entropy_data = vec![1, 2, 3];
908        let mut source = UserEntropySource::new(entropy_data);
909
910        // Initialize with a config that allows more bytes per call
911        let config = EntropyConfig {
912            max_per_call: Some(6), // Allow up to 6 bytes per call (same as what we're requesting)
913            ..Default::default()
914        };
915        source.initialize(&config).unwrap();
916
917        let mut bytes = [0u8; 6];
918        source.get_entropy(&mut bytes).unwrap();
919
920        // Should cycle through the entropy data
921        assert_eq!(bytes, [1, 2, 3, 1, 2, 3]);
922    }
923
924    #[test]
925    #[cfg(feature = "alloc")]
926    fn test_entropy_source_factory_deterministic() {
927        let mut seed = [0u8; 32];
928        seed[..4].copy_from_slice(&[1, 2, 3, 4]);
929        let source = EntropySourceFactory::create_deterministic_entropy(seed);
930        assert_eq!(source.source_type(), EntropySourceType::Deterministic);
931    }
932
933    #[test]
934    #[cfg(all(feature = "alloc", feature = "nist-drbg"))]
935    fn test_nist_drbg_kat_vectors() {
936        use alloc::vec::Vec;
937
938        // Test vectors from classic-mceliece reference implementation
939        const RNG_REF1: &str = "061550234D158C5EC95595FE04EF7A25767F2E24CC2BC479D09D86DC9ABCFDE7056A8C266F9EF97ED08541DBD2E1FFA19810F5392D076276EF41277C3AB6E94A4E3B7DCC104A05BB089D338BF55C72CAB375389A94BB920BD5D6DC9E7F2EC6FDE028B6F5724BB039F3652AD98DF8CE6C97013210B84BBE81388C3D141D61957C73BCDC5E5CD92525F46A2B757B03CAB5C337004A2DA35324A325713564DAE28F57ACC6DBE32A0726190BAA6B8A0A255AA1AD01E8DD569AA36D096256C420718A69D46D8DB1C6DD40606A0BE3C235BEFE623A90593F82D6A8F9F924E44E36BE87F7D26B8445966F9EE329C426C12521E85F6FD4ECD5D566BA0A3487125D79CC64";
940        const RNG_REF2: &str = "C17E034061ED5EA817C41D61636281E816F817DCF753A91D97C018FF82FBC9B1728FC66AF114B57978FB6082B70D285140B26725AA5F7BB4409820F67E2D656EDACA30B5BB12EB5249CC3809B188CF0CC95B5AE0EFE8FC5887152CB6601B4CCF9FC411894FA0C0264EB51A481D4D7074FDF065053030C8A92BFCDD06BF18C8489C38D03784FD63001830E5A385A4A37866693F5BDAB8A8A25B519DDBF2D28268601D95BEED647E430484A227C023B0297A282F06C91376433BDE5EC3ABBA8C06B830C26452EA2FA7EDEA8DCFE20EAFCF8980B3D5AECEF89DD861ACEC1F5F7CD2AE6B3CDE3C1D80A2830DD0B9E8468AFAD161981074BEB33DF1CDFF9A5214F9F0";
941
942        // Parse hex strings to bytes
943        fn hex_to_bytes(hex: &str) -> Vec<u8> {
944            let mut bytes = Vec::new();
945            let mut chars = hex.chars().peekable();
946            while let (Some(c1), Some(c2)) = (chars.next(), chars.next()) {
947                let byte = u8::from_str_radix(&format!("{c1}{c2}"), 16).unwrap();
948                bytes.push(byte);
949            }
950            bytes
951        }
952
953        let mut entropy_input = [0u8; 48];
954        for (i, byte) in entropy_input.iter_mut().enumerate().take(48) {
955            *byte = u8::try_from(i).expect("i < 48, so conversion to u8 is safe");
956        }
957
958        let mut drbg = NistAes256CtrDrbg::new();
959        drbg.randombytes_init(entropy_input);
960
961        // Test first 256 bytes
962        let mut data = [0u8; 256];
963        drbg.get_entropy(&mut data).unwrap();
964        let ref1 = hex_to_bytes(RNG_REF1);
965        assert_eq!(data.as_slice(), ref1.as_slice());
966
967        // Test second 256 bytes
968        drbg.get_entropy(&mut data).unwrap();
969        let ref2 = hex_to_bytes(RNG_REF2);
970        assert_eq!(data.as_slice(), ref2.as_slice());
971    }
972
973    #[test]
974    #[cfg(feature = "alloc")]
975    fn test_entropy_source_factory_user() {
976        let entropy_data = vec![1, 2, 3, 4, 5];
977        let source = EntropySourceFactory::create_user_entropy(entropy_data);
978        assert_eq!(source.source_type(), EntropySourceType::User);
979    }
980
981    #[test]
982    fn test_entropy_config_validation() {
983        let config = EntropyConfig {
984            min_quality: 0.9, // High quality requirement
985            max_per_call: Some(32),
986            ..Default::default()
987        };
988
989        // Test OS entropy source with high quality requirement
990        let mut os_source = OsEntropySource::new();
991        // OS entropy should meet the quality requirement (0.95 > 0.9)
992        assert!(os_source.initialize(&config).is_ok());
993
994        // Test with quality requirement too high
995        let config2 = EntropyConfig {
996            min_quality: 0.99,
997            ..config
998        };
999        let mut os_source2 = OsEntropySource::new();
1000        // OS entropy should not meet this requirement (0.95 < 0.99)
1001        assert!(os_source2.initialize(&config2).is_err());
1002    }
1003
1004    #[test]
1005    fn test_entropy_config_max_per_call() {
1006        let config = EntropyConfig {
1007            max_per_call: Some(16),
1008            ..Default::default()
1009        };
1010
1011        let mut os_source = OsEntropySource::new();
1012        os_source.initialize(&config).unwrap();
1013
1014        // Test requesting more than max_per_call
1015        let mut buffer = [0u8; 32]; // Request 32 bytes, but max is 16
1016        assert!(os_source.get_entropy(&mut buffer).is_err());
1017    }
1018
1019    #[test]
1020    #[cfg(feature = "alloc")]
1021    fn test_entropy_source_factory_with_config() {
1022        let config = EntropyConfig {
1023            min_quality: 0.8,
1024            max_per_call: Some(64),
1025            ..Default::default()
1026        };
1027
1028        // This should work with default config
1029        let result = EntropySourceFactory::create_best_available_with_config(&config);
1030        // The result depends on platform capabilities, so we just check it doesn't panic
1031        let _ = result;
1032    }
1033
1034    #[test]
1035    #[cfg(any(feature = "std", feature = "alloc"))]
1036    fn test_platform_specific_error_messages() {
1037        let mut os_source = OsEntropySource::new();
1038        let platform = os_source.platform();
1039
1040        // Test quality error message
1041        let config = EntropyConfig {
1042            min_quality: 1.0, // Set to impossible value
1043            ..Default::default()
1044        };
1045
1046        let result = os_source.initialize(&config);
1047        assert!(result.is_err());
1048        if let Err(error) = result {
1049            let error_msg = format!("{error}");
1050            assert!(error_msg.contains("quality below required minimum"));
1051        }
1052
1053        // Test max_per_call error message
1054        let config2 = EntropyConfig {
1055            max_per_call: Some(1000), // Exceeds limit
1056            ..Default::default()
1057        };
1058
1059        let result2 = os_source.initialize(&config2);
1060        assert!(result2.is_err());
1061        if let Err(error) = result2 {
1062            let error_msg = format!("{error}");
1063            assert!(error_msg.contains("exceeds OS entropy source limit"));
1064        }
1065
1066        // Test that platform is still accessible and used in name
1067        assert!(!platform.is_empty());
1068        assert!(os_source.name().contains(platform));
1069    }
1070}