openentropy_core/sources/
timing.rs1use std::thread;
8use std::time::{Duration, Instant, SystemTime};
9
10use crate::source::{EntropySource, SourceCategory, SourceInfo};
11
12pub struct ClockJitterSource;
21
22static CLOCK_JITTER_INFO: SourceInfo = SourceInfo {
23 name: "clock_jitter",
24 description: "Phase noise between Instant and SystemTime clocks",
25 physics: "Measures phase noise between two independent clock oscillators \
26 (perf_counter vs monotonic). Each clock is driven by a separate \
27 PLL (Phase-Locked Loop) on the SoC. Thermal noise in the PLL's \
28 voltage-controlled oscillator causes random frequency drift — \
29 the LSBs of their difference are genuine analog entropy from \
30 crystal oscillator physics.",
31 category: SourceCategory::Timing,
32 platform_requirements: &[],
33 entropy_rate_estimate: 0.5,
34 composite: false,
35};
36
37impl EntropySource for ClockJitterSource {
38 fn info(&self) -> &SourceInfo {
39 &CLOCK_JITTER_INFO
40 }
41
42 fn is_available(&self) -> bool {
43 true
44 }
45
46 fn collect(&self, n_samples: usize) -> Vec<u8> {
47 let mut output = Vec::with_capacity(n_samples);
48
49 for _ in 0..n_samples {
50 let mono = Instant::now();
51 let wall = SystemTime::now()
52 .duration_since(SystemTime::UNIX_EPOCH)
53 .unwrap_or_default();
54
55 let mono2 = Instant::now();
56 let mono_delta_ns = mono2.duration_since(mono).as_nanos() as u64;
57 let wall_ns = wall.as_nanos() as u64;
58
59 let delta = mono_delta_ns ^ wall_ns;
60 output.push(delta as u8);
61 }
62
63 output
64 }
65}
66
67use super::helpers::mach_time;
72
73pub struct MachTimingSource;
77
78static MACH_TIMING_INFO: SourceInfo = SourceInfo {
79 name: "mach_timing",
80 description: "mach_absolute_time() with micro-workload jitter (raw LSBs)",
81 physics: "Reads the ARM system counter (mach_absolute_time) at sub-nanosecond \
82 resolution with variable micro-workloads between samples. The timing \
83 jitter comes from CPU pipeline state: instruction reordering, branch \
84 prediction, cache state, interrupt coalescing, and power-state \
85 transitions.",
86 category: SourceCategory::Timing,
87 platform_requirements: &["macOS"],
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::Timing,
150 platform_requirements: &[],
151 entropy_rate_estimate: 0.4,
152 composite: false,
153};
154
155impl EntropySource for SleepJitterSource {
156 fn info(&self) -> &SourceInfo {
157 &SLEEP_JITTER_INFO
158 }
159
160 fn is_available(&self) -> bool {
161 true
162 }
163
164 fn collect(&self, n_samples: usize) -> Vec<u8> {
165 let oversample = n_samples * 2 + 64;
166 let mut raw_timings = Vec::with_capacity(oversample);
167
168 for _ in 0..oversample {
169 let before = Instant::now();
170 thread::sleep(Duration::ZERO);
171 let elapsed_ns = before.elapsed().as_nanos() as u64;
172 raw_timings.push(elapsed_ns);
173 }
174
175 let deltas: Vec<u64> = raw_timings
177 .windows(2)
178 .map(|w| w[1].wrapping_sub(w[0]))
179 .collect();
180
181 let mut raw = Vec::with_capacity(n_samples);
182 for pair in deltas.windows(2) {
183 let xored = pair[0] ^ pair[1];
184 raw.push(xored as u8);
185 if raw.len() >= n_samples {
186 break;
187 }
188 }
189
190 raw
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 #[ignore] fn clock_jitter_collects_bytes() {
201 let src = ClockJitterSource;
202 assert!(src.is_available());
203 let data = src.collect(128);
204 assert!(!data.is_empty());
205 assert!(data.len() <= 128);
206 let first = data[0];
207 assert!(data.iter().any(|&b| b != first), "all bytes were identical");
208 }
209
210 #[test]
211 #[cfg(target_os = "macos")]
212 #[ignore] fn mach_timing_collects_bytes() {
214 let src = MachTimingSource;
215 assert!(src.is_available());
216 let data = src.collect(64);
217 assert!(!data.is_empty());
218 assert!(data.len() <= 64);
219 }
220
221 #[test]
222 #[ignore] fn sleep_jitter_collects_bytes() {
224 let src = SleepJitterSource;
225 assert!(src.is_available());
226 let data = src.collect(64);
227 assert!(!data.is_empty());
228 assert!(data.len() <= 64);
229 }
230
231 #[test]
232 fn source_info_names() {
233 assert_eq!(ClockJitterSource.name(), "clock_jitter");
234 assert_eq!(MachTimingSource.name(), "mach_timing");
235 assert_eq!(SleepJitterSource.name(), "sleep_jitter");
236 }
237}