Skip to main content

lib_q_random/
custom_entropy.rs

1//! Custom Entropy Source System for `no_std` and WASM Environments
2//!
3//! This module provides a secure, callback-based entropy source system that allows
4//! developers to plug in custom entropy sources for `no_std` and WASM environments.
5//! With `std`, registrations are thread-local. On `no_std` / WASM, a single process-wide
6//! registry is used (browser WASM and typical firmware are single-threaded).
7
8use core::sync::atomic::{
9    AtomicPtr,
10    Ordering,
11};
12use core::{
13    fmt,
14    ptr,
15};
16
17use crate::{
18    Error,
19    Result,
20};
21
22/// Function pointer type for custom entropy sources
23///
24/// This function should fill the provided buffer with cryptographically secure
25/// random bytes. The function must be thread-safe and should not block indefinitely.
26///
27/// # Arguments
28///
29/// * `dest` - Buffer to fill with random bytes
30/// * `len` - Number of bytes to generate
31/// * `context` - Optional context data passed to the entropy source
32///
33/// # Returns
34///
35/// Returns `Ok(())` on success, or an error if entropy generation fails.
36///
37/// # Safety
38///
39/// The `dest` pointer must be valid for `len` bytes and must not be null.
40/// The function must not cause undefined behavior.
41pub type EntropyCallback = unsafe extern "C" fn(dest: *mut u8, len: usize, context: *mut u8) -> i32;
42
43/// Context data for entropy callbacks
44///
45/// This structure can be used to pass additional context to entropy callbacks,
46/// such as user data or configuration.
47#[derive(Debug, Clone, Copy)]
48pub struct EntropyContext {
49    /// User-defined context data
50    pub user_data: *mut u8,
51    /// Context size in bytes
52    pub size: usize,
53}
54
55impl EntropyContext {
56    /// Create a new entropy context
57    ///
58    /// # Arguments
59    ///
60    /// * `user_data` - User-defined context data
61    /// * `size` - Size of the context data in bytes
62    ///
63    /// # Safety
64    ///
65    /// The `user_data` pointer must be valid for `size` bytes if `size > 0`.
66    pub const unsafe fn new(user_data: *mut u8, size: usize) -> Self {
67        Self { user_data, size }
68    }
69
70    /// Create an empty entropy context
71    #[must_use]
72    pub const fn empty() -> Self {
73        Self {
74            user_data: ptr::null_mut(),
75            size: 0,
76        }
77    }
78}
79
80/// Entropy source quality levels
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
82pub enum EntropyQuality {
83    /// Hardware-based entropy source (highest quality)
84    Hardware,
85    /// OS-provided entropy source
86    Os,
87    /// User-provided entropy source
88    User,
89    /// Deterministic source (lowest quality, testing only)
90    Deterministic,
91}
92
93impl EntropyQuality {
94    /// Get the numeric quality value (0.0 to 1.0)
95    #[must_use]
96    pub fn as_f64(self) -> f64 {
97        match self {
98            Self::Hardware => 1.0,
99            Self::Os => 0.95,
100            Self::User => 0.8,
101            Self::Deterministic => 0.0,
102        }
103    }
104
105    /// Check if this quality level is cryptographically secure
106    #[must_use]
107    pub fn is_secure(self) -> bool {
108        matches!(self, Self::Hardware | Self::Os | Self::User)
109    }
110}
111
112/// Custom entropy source configuration
113#[derive(Debug, Clone)]
114pub struct CustomEntropyConfig {
115    /// Minimum entropy quality required
116    pub min_quality: EntropyQuality,
117    /// Maximum bytes per entropy call
118    pub max_bytes_per_call: usize,
119    /// Whether to validate entropy quality
120    pub validate_quality: bool,
121    /// Timeout for entropy generation (in some unit)
122    pub timeout_ms: u32,
123}
124
125impl Default for CustomEntropyConfig {
126    fn default() -> Self {
127        Self {
128            min_quality: EntropyQuality::User,
129            max_bytes_per_call: 1024,
130            validate_quality: true,
131            timeout_ms: 1000,
132        }
133    }
134}
135
136/// Custom entropy source registration
137///
138/// This structure manages the registration of custom entropy sources
139/// for the current thread.
140#[derive(Debug)]
141pub struct CustomEntropySource {
142    /// Callback function for entropy generation
143    pub callback: EntropyCallback,
144    /// Context data for the callback
145    pub context: EntropyContext,
146    /// Quality level of this entropy source
147    pub quality: EntropyQuality,
148    /// Configuration for this entropy source
149    pub config: CustomEntropyConfig,
150    /// Source identifier
151    pub source_id: &'static str,
152}
153
154impl CustomEntropySource {
155    /// Create a new custom entropy source
156    ///
157    /// # Arguments
158    ///
159    /// * `callback` - Function to call for entropy generation
160    /// * `context` - Context data for the callback
161    /// * `quality` - Quality level of this entropy source
162    /// * `config` - Configuration for this entropy source
163    /// * `source_id` - Unique identifier for this source
164    ///
165    /// # Safety
166    ///
167    /// The `callback` function must be thread-safe and must not cause
168    /// undefined behavior. The `context.user_data` must be valid for
169    /// `context.size` bytes if `context.size > 0`.
170    pub const unsafe fn new(
171        callback: EntropyCallback,
172        context: EntropyContext,
173        quality: EntropyQuality,
174        config: CustomEntropyConfig,
175        source_id: &'static str,
176    ) -> Self {
177        Self {
178            callback,
179            context,
180            quality,
181            config,
182            source_id,
183        }
184    }
185
186    /// Get the callback function
187    #[must_use]
188    pub fn callback(&self) -> EntropyCallback {
189        self.callback
190    }
191
192    /// Get the context data
193    #[must_use]
194    pub fn context(&self) -> EntropyContext {
195        self.context
196    }
197
198    /// Get the quality level
199    #[must_use]
200    pub fn quality(&self) -> EntropyQuality {
201        self.quality
202    }
203
204    /// Get the configuration
205    #[must_use]
206    pub fn config(&self) -> &CustomEntropyConfig {
207        &self.config
208    }
209
210    /// Get the source identifier
211    #[must_use]
212    pub fn source_id(&self) -> &'static str {
213        self.source_id
214    }
215
216    /// Generate entropy using this source
217    ///
218    /// # Arguments
219    ///
220    /// * `dest` - Buffer to fill with random bytes
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if entropy generation fails or if the generated
225    /// entropy doesn't meet quality requirements.
226    pub fn generate_entropy(&self, dest: &mut [u8]) -> Result<()> {
227        if dest.len() > self.config.max_bytes_per_call {
228            return Err(Error::EntropySourceUnavailable {
229                source: self.source_id,
230                context: Some("requested bytes exceed maximum per call"),
231            });
232        }
233
234        if !self.quality.is_secure() && self.config.validate_quality {
235            return Err(Error::EntropyValidationFailed {
236                reason: "entropy source quality too low",
237                quality: self.quality.as_f64(),
238                details: Some("deterministic sources not allowed in secure mode"),
239            });
240        }
241
242        // Call the custom entropy function
243        let result =
244            unsafe { (self.callback)(dest.as_mut_ptr(), dest.len(), self.context.user_data) };
245
246        if result == 0 {
247            Ok(())
248        } else {
249            Err(Error::EntropySourceUnavailable {
250                source: self.source_id,
251                context: Some("custom entropy callback failed"),
252            })
253        }
254    }
255}
256
257/// Thread-local entropy source registry
258///
259/// This structure manages custom entropy sources for the current thread.
260/// It uses atomic operations to ensure thread safety.
261pub struct ThreadEntropyRegistry {
262    /// Currently registered entropy source
263    source: AtomicPtr<CustomEntropySource>,
264}
265
266impl Default for ThreadEntropyRegistry {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272impl ThreadEntropyRegistry {
273    /// Create a new thread entropy registry
274    #[must_use]
275    pub const fn new() -> Self {
276        Self {
277            source: AtomicPtr::new(ptr::null_mut()),
278        }
279    }
280
281    /// Register a custom entropy source for this thread
282    ///
283    /// # Arguments
284    ///
285    /// * `source` - The custom entropy source to register
286    ///
287    /// # Safety
288    ///
289    /// The `source` must remain valid for the lifetime of the registry.
290    /// The caller is responsible for ensuring the source is not dropped
291    /// while registered.
292    pub unsafe fn register(&self, source: *const CustomEntropySource) {
293        self.source.store(source.cast_mut(), Ordering::Release);
294    }
295
296    /// Unregister the current entropy source
297    pub fn unregister(&self) {
298        self.source.store(ptr::null_mut(), Ordering::Release);
299    }
300
301    /// Get the currently registered entropy source
302    ///
303    /// # Returns
304    ///
305    /// Returns a reference to the registered entropy source, or `None`
306    /// if no source is registered.
307    ///
308    /// # Safety
309    ///
310    /// The returned reference is only valid as long as the source remains
311    /// registered and not dropped.
312    pub unsafe fn get_source(&self) -> Option<&CustomEntropySource> {
313        let ptr = self.source.load(Ordering::Acquire);
314        if ptr.is_null() {
315            None
316        } else {
317            unsafe { Some(&*ptr) }
318        }
319    }
320
321    /// Generate entropy using the registered source
322    ///
323    /// # Arguments
324    ///
325    /// * `dest` - Buffer to fill with random bytes
326    ///
327    /// # Errors
328    ///
329    /// Returns an error if no source is registered or if entropy generation fails.
330    pub fn generate_entropy(&self, dest: &mut [u8]) -> Result<()> {
331        unsafe {
332            if let Some(source) = self.get_source() {
333                source.generate_entropy(dest)
334            } else {
335                Err(Error::EntropySourceUnavailable {
336                    source: "thread_local",
337                    context: Some("no custom entropy source registered"),
338                })
339            }
340        }
341    }
342}
343
344// `thread_local!` requires `std`. `no_std` / WASM builds use one registry (typical WASM and firmware are single-threaded).
345#[cfg(feature = "std")]
346thread_local! {
347    static THREAD_REGISTRY: ThreadEntropyRegistry = const { ThreadEntropyRegistry::new() };
348}
349
350#[cfg(not(feature = "std"))]
351static REGISTRY: ThreadEntropyRegistry = const { ThreadEntropyRegistry::new() };
352
353#[inline]
354fn with_registry<F, R>(f: F) -> R
355where
356    F: FnOnce(&ThreadEntropyRegistry) -> R,
357{
358    #[cfg(feature = "std")]
359    {
360        THREAD_REGISTRY.with(f)
361    }
362    #[cfg(not(feature = "std"))]
363    {
364        f(&REGISTRY)
365    }
366}
367
368/// Register a custom entropy source for the current thread
369///
370/// # Arguments
371///
372/// * `source` - The custom entropy source to register
373///
374/// # Safety
375///
376/// The `source` must remain valid for the lifetime of the registration.
377/// The caller is responsible for ensuring the source is not dropped
378/// while registered.
379pub unsafe fn register_custom_entropy_source(source: *const CustomEntropySource) {
380    with_registry(|registry| unsafe { registry.register(source) });
381}
382
383/// Unregister the current custom entropy source
384pub fn unregister_custom_entropy_source() {
385    with_registry(ThreadEntropyRegistry::unregister);
386}
387
388/// Generate entropy using the registered custom source
389///
390/// # Arguments
391///
392/// * `dest` - Buffer to fill with random bytes
393///
394/// # Errors
395///
396/// Returns an error if no source is registered or if entropy generation fails.
397pub fn generate_custom_entropy(dest: &mut [u8]) -> Result<()> {
398    with_registry(|registry| registry.generate_entropy(dest))
399}
400
401/// Check if a custom entropy source is registered
402#[must_use]
403pub fn has_custom_entropy_source() -> bool {
404    with_registry(|registry| unsafe { registry.get_source().is_some() })
405}
406
407/// Get information about the registered entropy source
408///
409/// # Returns
410///
411/// Returns a tuple of (`source_id`, quality) if a source is registered.
412#[must_use]
413pub fn get_entropy_source_info() -> Option<(&'static str, EntropyQuality)> {
414    with_registry(|registry| unsafe {
415        registry
416            .get_source()
417            .map(|source| (source.source_id(), source.quality()))
418    })
419}
420
421impl fmt::Display for EntropyQuality {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        match self {
424            Self::Hardware => write!(f, "Hardware"),
425            Self::Os => write!(f, "OS"),
426            Self::User => write!(f, "User"),
427            Self::Deterministic => write!(f, "Deterministic"),
428        }
429    }
430}
431
432impl fmt::Display for CustomEntropyConfig {
433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434        write!(
435            f,
436            "CustomEntropyConfig {{ min_quality: {}, max_bytes: {}, validate: {}, timeout: {}ms }}",
437            self.min_quality, self.max_bytes_per_call, self.validate_quality, self.timeout_ms
438        )
439    }
440}
441
442impl fmt::Display for CustomEntropySource {
443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444        write!(
445            f,
446            "CustomEntropySource {{ id: {}, quality: {}, config: {} }}",
447            self.source_id, self.quality, self.config
448        )
449    }
450}
451
452// CustomEntropySource is not an RNG itself, it's a source of entropy for RNGs
453
454#[cfg(test)]
455mod tests {
456    #[cfg(feature = "alloc")]
457    use alloc::format;
458
459    use super::*;
460
461    // Test entropy callback that generates predictable data
462    #[allow(clippy::cast_possible_truncation)]
463    unsafe extern "C" fn test_entropy_callback(
464        dest: *mut u8,
465        len: usize,
466        _context: *mut u8,
467    ) -> i32 {
468        if dest.is_null() {
469            return -1;
470        }
471
472        // Generate predictable test data (handle empty buffer case)
473        for i in 0..len {
474            unsafe {
475                *dest.add(i) = (i as u8).wrapping_add(42);
476            }
477        }
478
479        0
480    }
481
482    #[test]
483    fn test_entropy_context_creation() {
484        let context = EntropyContext::empty();
485        assert!(context.user_data.is_null());
486        assert_eq!(context.size, 0);
487
488        let data = [1u8, 2, 3, 4];
489        let context = unsafe { EntropyContext::new(data.as_ptr().cast_mut(), data.len()) };
490        assert!(!context.user_data.is_null());
491        assert_eq!(context.size, 4);
492    }
493
494    #[test]
495    fn test_entropy_quality() {
496        assert!(EntropyQuality::Hardware.is_secure());
497        assert!(EntropyQuality::Os.is_secure());
498        assert!(EntropyQuality::User.is_secure());
499        assert!(!EntropyQuality::Deterministic.is_secure());
500
501        assert!((EntropyQuality::Hardware.as_f64() - 1.0).abs() < f64::EPSILON);
502        assert!((EntropyQuality::Os.as_f64() - 0.95).abs() < f64::EPSILON);
503        assert!((EntropyQuality::User.as_f64() - 0.8).abs() < f64::EPSILON);
504        assert!((EntropyQuality::Deterministic.as_f64() - 0.0).abs() < f64::EPSILON);
505    }
506
507    #[test]
508    fn test_custom_entropy_config_default() {
509        let config = CustomEntropyConfig::default();
510        assert_eq!(config.min_quality, EntropyQuality::User);
511        assert_eq!(config.max_bytes_per_call, 1024);
512        assert!(config.validate_quality);
513        assert_eq!(config.timeout_ms, 1000);
514    }
515
516    #[test]
517    fn test_custom_entropy_source_creation() {
518        let context = EntropyContext::empty();
519        let config = CustomEntropyConfig::default();
520
521        let source = unsafe {
522            CustomEntropySource::new(
523                test_entropy_callback,
524                context,
525                EntropyQuality::User,
526                config,
527                "test_source",
528            )
529        };
530
531        assert_eq!(source.source_id(), "test_source");
532        assert_eq!(source.quality(), EntropyQuality::User);
533    }
534
535    #[test]
536    #[allow(clippy::cast_possible_truncation)]
537    fn test_custom_entropy_generation() {
538        let context = EntropyContext::empty();
539        let config = CustomEntropyConfig::default();
540
541        let source = unsafe {
542            CustomEntropySource::new(
543                test_entropy_callback,
544                context,
545                EntropyQuality::User,
546                config,
547                "test_source",
548            )
549        };
550
551        let mut buffer = [0u8; 16];
552        source.generate_entropy(&mut buffer).unwrap();
553
554        // Check that the callback was called (predictable test data)
555        for (i, &byte) in buffer.iter().enumerate() {
556            let expected = (i as u8).wrapping_add(42);
557            assert_eq!(byte, expected);
558        }
559    }
560
561    #[test]
562    fn test_custom_entropy_max_bytes_validation() {
563        let context = EntropyContext::empty();
564        let config = CustomEntropyConfig {
565            max_bytes_per_call: 8,
566            ..Default::default()
567        };
568
569        let source = unsafe {
570            CustomEntropySource::new(
571                test_entropy_callback,
572                context,
573                EntropyQuality::User,
574                config,
575                "test_source",
576            )
577        };
578
579        let mut buffer = [0u8; 16]; // Exceeds max_bytes_per_call
580        let result = source.generate_entropy(&mut buffer);
581        assert!(result.is_err());
582    }
583
584    #[test]
585    fn test_custom_entropy_quality_validation() {
586        let context = EntropyContext::empty();
587        let config = CustomEntropyConfig {
588            validate_quality: true,
589            ..Default::default()
590        };
591
592        let source = unsafe {
593            CustomEntropySource::new(
594                test_entropy_callback,
595                context,
596                EntropyQuality::Deterministic, // Low quality
597                config,
598                "test_source",
599            )
600        };
601
602        let mut buffer = [0u8; 8];
603        let result = source.generate_entropy(&mut buffer);
604        assert!(result.is_err());
605    }
606
607    #[test]
608    fn test_thread_entropy_registry() {
609        let _registry = ThreadEntropyRegistry::new();
610
611        // Initially no source registered
612        assert!(!has_custom_entropy_source());
613        assert!(get_entropy_source_info().is_none());
614
615        let context = EntropyContext::empty();
616        let config = CustomEntropyConfig::default();
617        let source = CustomEntropySource {
618            callback: test_entropy_callback,
619            context,
620            quality: EntropyQuality::User,
621            config,
622            source_id: "test_registry",
623        };
624
625        // Register the source
626        unsafe {
627            register_custom_entropy_source(&raw const source);
628        }
629
630        assert!(has_custom_entropy_source());
631        let info = get_entropy_source_info().unwrap();
632        assert_eq!(info.0, "test_registry");
633        assert_eq!(info.1, EntropyQuality::User);
634
635        // Test entropy generation
636        let mut buffer = [0u8; 8];
637        generate_custom_entropy(&mut buffer).unwrap();
638
639        // Unregister the source
640        unregister_custom_entropy_source();
641        assert!(!has_custom_entropy_source());
642        assert!(get_entropy_source_info().is_none());
643    }
644
645    #[test]
646    #[cfg(feature = "alloc")]
647    fn test_entropy_source_display() {
648        let context = EntropyContext::empty();
649        let config = CustomEntropyConfig::default();
650
651        let source = unsafe {
652            CustomEntropySource::new(
653                test_entropy_callback,
654                context,
655                EntropyQuality::Hardware,
656                config,
657                "display_test",
658            )
659        };
660
661        let display = format!("{source}");
662        assert!(display.contains("display_test"));
663        assert!(display.contains("Hardware"));
664    }
665}