openentropy_core/sources/
timing.rs1use std::thread;
6use std::time::{Duration, Instant, SystemTime};
7
8use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
9
10pub struct ClockJitterSource;
19
20static CLOCK_JITTER_INFO: SourceInfo = SourceInfo {
21 name: "clock_jitter",
22 description: "Phase noise between Instant and SystemTime clocks",
23 physics: "Measures phase noise between two independent clock oscillators \
24 (perf_counter vs monotonic). Each clock is driven by a separate \
25 PLL (Phase-Locked Loop) on the SoC. Thermal noise in the PLL's \
26 voltage-controlled oscillator causes random frequency drift — \
27 the LSBs of their difference are genuine analog entropy from \
28 crystal oscillator physics.",
29 category: SourceCategory::Timing,
30 platform: Platform::Any,
31 requirements: &[],
32 entropy_rate_estimate: 0.5,
33 composite: false,
34};
35
36impl EntropySource for ClockJitterSource {
37 fn info(&self) -> &SourceInfo {
38 &CLOCK_JITTER_INFO
39 }
40
41 fn is_available(&self) -> bool {
42 true
43 }
44
45 fn collect(&self, n_samples: usize) -> Vec<u8> {
46 let mut output = Vec::with_capacity(n_samples);
47
48 for _ in 0..n_samples {
49 let mono = Instant::now();
50 let wall = SystemTime::now()
51 .duration_since(SystemTime::UNIX_EPOCH)
52 .unwrap_or_default();
53
54 let mono2 = Instant::now();
55 let mono_delta_ns = mono2.duration_since(mono).as_nanos() as u64;
56 let wall_ns = wall.as_nanos() as u64;
57
58 let delta = mono_delta_ns ^ wall_ns;
59 output.push(delta as u8);
60 }
61
62 output
63 }
64}
65
66use super::helpers::mach_time;
71
72pub struct MachTimingSource;
76
77static MACH_TIMING_INFO: SourceInfo = SourceInfo {
78 name: "mach_timing",
79 description: "mach_absolute_time() with micro-workload jitter (raw LSBs)",
80 physics: "Reads the ARM system counter (mach_absolute_time) at sub-nanosecond \
81 resolution with variable micro-workloads between samples. The timing \
82 jitter comes from CPU pipeline state: instruction reordering, branch \
83 prediction, cache state, interrupt coalescing, and power-state \
84 transitions.",
85 category: SourceCategory::Timing,
86 platform: Platform::MacOS,
87 requirements: &[],
88 entropy_rate_estimate: 0.3,
89 composite: false,
90};
91
92impl EntropySource for MachTimingSource {
93 fn info(&self) -> &SourceInfo {
94 &MACH_TIMING_INFO
95 }
96
97 fn is_available(&self) -> bool {
98 cfg!(target_os = "macos")
99 }
100
101 fn collect(&self, n_samples: usize) -> Vec<u8> {
102 let raw_count = n_samples * 2 + 64;
104 let mut raw = Vec::with_capacity(n_samples);
105
106 for i in 0..raw_count {
107 let t0 = mach_time();
108
109 let iterations = (i % 7) + 1;
111 let mut sink: u64 = t0;
112 for _ in 0..iterations {
113 sink = sink.wrapping_mul(6364136223846793005).wrapping_add(1);
114 }
115 std::hint::black_box(sink);
116
117 let t1 = mach_time();
118 let delta = t1.wrapping_sub(t0);
119
120 raw.push(delta as u8);
122
123 if raw.len() >= n_samples {
124 break;
125 }
126 }
127
128 raw.truncate(n_samples);
129 raw
130 }
131}
132
133pub struct SleepJitterSource;
141
142static SLEEP_JITTER_INFO: SourceInfo = SourceInfo {
143 name: "sleep_jitter",
144 description: "OS scheduler jitter from zero-duration sleeps",
145 physics: "Requests zero-duration sleeps and measures actual wake time. The jitter \
146 captures OS scheduler non-determinism: timer interrupt granularity (1-4ms), \
147 thread priority decisions, runqueue length, and thermal-dependent clock \
148 frequency scaling (DVFS).",
149 category: SourceCategory::Scheduling,
150 platform: Platform::Any,
151 requirements: &[],
152 entropy_rate_estimate: 0.4,
153 composite: false,
154};
155
156impl EntropySource for SleepJitterSource {
157 fn info(&self) -> &SourceInfo {
158 &SLEEP_JITTER_INFO
159 }
160
161 fn is_available(&self) -> bool {
162 true
163 }
164
165 fn collect(&self, n_samples: usize) -> Vec<u8> {
166 let oversample = n_samples * 2 + 64;
167 let mut raw_timings = Vec::with_capacity(oversample);
168
169 for _ in 0..oversample {
170 let before = Instant::now();
171 thread::sleep(Duration::ZERO);
172 let elapsed_ns = before.elapsed().as_nanos() as u64;
173 raw_timings.push(elapsed_ns);
174 }
175
176 let deltas: Vec<u64> = raw_timings
178 .windows(2)
179 .map(|w| w[1].wrapping_sub(w[0]))
180 .collect();
181
182 let mut raw = Vec::with_capacity(n_samples);
183 for pair in deltas.windows(2) {
184 let xored = pair[0] ^ pair[1];
185 raw.push(xored as u8);
186 if raw.len() >= n_samples {
187 break;
188 }
189 }
190
191 raw
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 #[ignore] fn clock_jitter_collects_bytes() {
202 let src = ClockJitterSource;
203 assert!(src.is_available());
204 let data = src.collect(128);
205 assert!(!data.is_empty());
206 assert!(data.len() <= 128);
207 let first = data[0];
208 assert!(data.iter().any(|&b| b != first), "all bytes were identical");
209 }
210
211 #[test]
212 #[cfg(target_os = "macos")]
213 #[ignore] fn mach_timing_collects_bytes() {
215 let src = MachTimingSource;
216 assert!(src.is_available());
217 let data = src.collect(64);
218 assert!(!data.is_empty());
219 assert!(data.len() <= 64);
220 }
221
222 #[test]
223 #[ignore] fn sleep_jitter_collects_bytes() {
225 let src = SleepJitterSource;
226 assert!(src.is_available());
227 let data = src.collect(64);
228 assert!(!data.is_empty());
229 assert!(data.len() <= 64);
230 }
231
232 #[test]
233 fn source_info_names() {
234 assert_eq!(ClockJitterSource.name(), "clock_jitter");
235 assert_eq!(MachTimingSource.name(), "mach_timing");
236 assert_eq!(SleepJitterSource.name(), "sleep_jitter");
237 }
238
239 #[test]
240 fn source_info_categories() {
241 assert_eq!(ClockJitterSource.info().category, SourceCategory::Timing);
242 assert_eq!(MachTimingSource.info().category, SourceCategory::Timing);
243 assert_eq!(
244 SleepJitterSource.info().category,
245 SourceCategory::Scheduling
246 );
247 assert!(!ClockJitterSource.info().composite);
248 assert!(!MachTimingSource.info().composite);
249 assert!(!SleepJitterSource.info().composite);
250 }
251}