Skip to main content

strat9_kernel/
entropy.rs

1//! Kernel entropy pool : a cryptographically sound random bytes from interrupt noise.
2//!
3//! Collects entropy from:
4//!   - keyboard IRQ timing + scancode data
5//!   - timer tick jitter (low bits of TSC)
6//!   - storage (AHCI) IRQ timing
7//!   - RDRAND when available (as seed material)
8//!
9//! The pool uses a tweaked Threefish-like mixing over 128 bytes (4×4 u64s)
10//! for efficient diffusion. Output is extracted by hashing the pool state
11//! through a **compression round** : no built-in hash dependency needed.
12
13use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
14
15// === Pool constants ===================================
16/// Number of 64-bit words in the entropy pool (16 = 128 bytes).
17const POOL_WORDS: usize = 16;
18/// Entropy threshold in bytes before we consider the pool "initialised".
19const ENTROPY_HIGH_WATER: u32 = 64;
20
21// === Global pool state =================================
22/// The entropy pool : a 128‑byte array of 16 u64s.
23static POOL: [AtomicU64; POOL_WORDS] = [const { AtomicU64::new(0) }; POOL_WORDS];
24/// Estimated entropy count (in bytes). Saturates at POOL_WORDS * 8.
25static ENTROPY_CTR: AtomicU32 = AtomicU32::new(0);
26/// Index where the next sample will be mixed in.
27static POOL_IDX: AtomicU32 = AtomicU32::new(0);
28
29// === Initial seed from RDRAND ================================================
30/// One-time boot‑time seed of the pool from RDRAND (if available).
31pub fn seed_from_rdrand() {
32    // RDRAND can hang on some QEMU configurations (especially AMD CPU models).
33    // Fall back to TSC as a weak initial seed (better than all-zero pool).
34    let lo: u32;
35    let hi: u32;
36    unsafe {
37        core::arch::asm!(
38            "rdtsc",
39            out("eax") lo,
40            out("edx") hi,
41            options(nostack, nomem),
42        );
43    }
44    let seed = ((hi as u64) << 32 | lo as u64).wrapping_mul(6364136223846793005);
45    for w in &POOL {
46        let old = w.load(Ordering::Relaxed);
47        w.store(old ^ seed, Ordering::Relaxed);
48    }
49    ENTROPY_CTR.store(32, Ordering::Relaxed);
50}
51
52// === public API ================================================
53
54/// Check whether the entropy pool has accumulated enough entropy
55/// (at least `ENTROPY_HIGH_WATER` bytes). Used by GRND_NONBLOCK.
56pub fn is_ready() -> bool {
57    ENTROPY_CTR.load(Ordering::Relaxed) >= ENTROPY_HIGH_WATER
58}
59
60/// Feed a 64‑bit sample into the entropy pool (called from interrupt handlers).
61///
62/// `tag` should be a small unique discriminator for the source
63/// (e.g. 1 = keyboard, 2 = timer, 3 = storage).
64#[inline]
65pub fn add_entropy(tag: u8, sample: u64) {
66    let idx = POOL_IDX.fetch_add(1, Ordering::Relaxed) as usize % POOL_WORDS;
67    let tag64 = (tag as u64) << 56;
68
69    // Mix: fold in the sample with a non‑linear twist.
70    // The tagged value is permuted through a small S‑box (multiplicative inverse)
71    // to destroy algebraic structure, then XORed into the pool word.
72    let mixed = twist(tag64 | (sample & 0x00FF_FFFF_FFFF_FFFF));
73    let old = POOL[idx].load(Ordering::Relaxed);
74    POOL[idx].store(old.wrapping_add(mixed), Ordering::Relaxed);
75
76    // Gentle avalanche: stir two more pool slots to avoid "stuck" zero
77    // if add_entropy is never called for some indices.
78    let idx2 = (idx + 3) % POOL_WORDS;
79    let old2 = POOL[idx2].load(Ordering::Relaxed);
80    POOL[idx2].store(old2 ^ mixed.rotate_right(17), Ordering::Relaxed);
81
82    // Bump entropy estimate (saturing at the pool capacity).
83    let old_e = ENTROPY_CTR.load(Ordering::Relaxed);
84    if old_e < (POOL_WORDS * 8) as u32 {
85        ENTROPY_CTR.fetch_add(2, Ordering::Relaxed); // 2 bytes per sample
86    }
87}
88
89/// Fill a byte buffer with random bytes from the entropy pool.
90///
91/// If the pool has not accumulated `ENTROPY_HIGH_WATER` bytes of entropy yet,
92/// this function spins briefly, waiting for interrupt noise.  It will not
93/// block indefinitely: after ~1000 spin iterations it falls through and
94/// delivers whatever randomness is available.
95pub fn fill_random(buf: &mut [u8]) {
96    // Wait until we have enough entropy (very short on any live system).
97    let mut spins = 0u32;
98    while ENTROPY_CTR.load(Ordering::Relaxed) < ENTROPY_HIGH_WATER {
99        core::hint::spin_loop();
100        spins += 1;
101        if spins > 1024 {
102            break; // don't hang if entropy source is absent
103        }
104    }
105
106    let mut offset = 0usize;
107    while offset < buf.len() {
108        let block = extract_block();
109        let n = (buf.len() - offset).min(8);
110        buf[offset..offset + n].copy_from_slice(&block[..n]);
111        offset += n;
112    }
113}
114
115// === Internal helpers ===============================================
116
117/// Non‑linear twist: multiplicative inverse in GF(2⁶⁴) masked by a prime,
118/// then XORed with a rotation of itself.
119#[inline(always)]
120fn twist(x: u64) -> u64 {
121    // "Multiply‑then‑rotate" – cheap, non‑linear, 1‑to‑1.
122    let a = x.wrapping_mul(0x6A09E667F3BCC909);
123    a ^ a.rotate_right(37)
124}
125
126/// Extract 8 bytes from the pool by folding all words together.
127///
128/// This is **not** a cryptographic hash : it's a feed‑forward compression
129/// that provides avalanche.  If the pool has been seeded from RDRAND and
130/// stirred by interrupt noise, the output is cryptographically acceptable.
131#[inline(always)]
132fn extract_block() -> [u8; 8] {
133    let mut h: u64 = 0x9E3779B97F4A7C15; // nothing‑up‑my‑sleeve constant
134    for (i, w) in POOL.iter().enumerate() {
135        let v = w.load(Ordering::Relaxed);
136        h = h.wrapping_add(v).wrapping_mul(0xBF58476D1CE4E5B9);
137        h ^= h.rotate_right((i as u32) % 63 + 1);
138    }
139
140    // One final avalanche
141    h ^= h >> 33;
142    h = h.wrapping_mul(0xFF51AFD7ED558CCD);
143    h ^= h >> 33;
144    h = h.wrapping_mul(0xC4CEB9FE1A85EC53);
145    h ^= h >> 33;
146
147    h.to_le_bytes()
148}
149
150/// BUG TODO : RDRAND helper (used during boot seeding).
151// hang during boot on some QEMU configurations if RDRAND is unavailable
152fn rdrand64() -> Option<u64> {
153    #[cfg(target_arch = "x86_64")]
154    {
155        let mut val: u64 = 0;
156        let mut ok: u8;
157        for _ in 0..10 {
158            unsafe {
159                core::arch::asm!(
160                    "rdrand {0}",
161                    "setc {1}",
162                    out(reg) val,
163                    out(reg_byte) ok,
164                    options(nostack, preserves_flags),
165                );
166            }
167            if ok != 0 {
168                return Some(val);
169            }
170        }
171    }
172    None
173}