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}