openentropy_core/sources/frontier/
usb_timing.rs1use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
14#[cfg(target_os = "macos")]
15use crate::sources::helpers::extract_timing_entropy;
16
17static USB_TIMING_INFO: SourceInfo = SourceInfo {
18 name: "usb_timing",
19 description: "USB IORegistry query timing jitter from crystal oscillator phase noise",
20 physics: "Rapidly queries USB device properties via IOKit. Each query traverses the \
21 USB host controller\u{2019}s IORegistry tree, crossing the USB crystal oscillator / \
22 CPU clock domain boundary. The USB crystal has thermally-driven phase noise \
23 from quartz lattice phonon excitations, load capacitance Johnson-Nyquist noise, \
24 and oscillator circuit thermal fluctuations. Timing jitter also includes USB \
25 bus arbitration contention.",
26 category: SourceCategory::IO,
27 platform: Platform::MacOS,
28 requirements: &[Requirement::Usb, Requirement::IOKit],
29 entropy_rate_estimate: 1500.0,
30 composite: false,
31};
32
33pub struct USBTimingSource;
35
36#[cfg(target_os = "macos")]
38mod iokit {
39 use std::ffi::{c_char, c_void};
40
41 pub type IOReturn = i32;
43 pub type MachPort = u32;
44
45 #[link(name = "IOKit", kind = "framework")]
46 unsafe extern "C" {
47 pub fn IOServiceGetMatchingServices(
48 main_port: MachPort,
49 matching: *const c_void,
50 existing: *mut u32,
51 ) -> IOReturn;
52
53 pub fn IOServiceMatching(name: *const c_char) -> *mut c_void;
54
55 pub fn IOIteratorNext(iterator: u32) -> u32;
56
57 pub fn IOObjectRelease(object: u32) -> IOReturn;
58
59 pub fn IORegistryEntryCreateCFProperty(
60 entry: u32,
61 key: *const c_void,
62 allocator: *const c_void,
63 options: u32,
64 ) -> *const c_void;
65 }
66
67 #[link(name = "CoreFoundation", kind = "framework")]
68 unsafe extern "C" {
69 pub fn CFRelease(cf: *const c_void);
70
71 pub fn CFStringCreateWithCString(
72 alloc: *const c_void,
73 c_str: *const i8,
74 encoding: u32,
75 ) -> *const c_void;
76 }
77
78 pub const K_IO_MAIN_PORT_DEFAULT: MachPort = 0;
80
81 pub const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
82
83 pub fn cfstr(s: &[u8]) -> *const c_void {
86 unsafe {
90 CFStringCreateWithCString(
91 std::ptr::null(),
92 s.as_ptr() as *const i8,
93 K_CF_STRING_ENCODING_UTF8,
94 )
95 }
96 }
97
98 pub fn find_usb_devices() -> Vec<u32> {
102 let mut devices = Vec::new();
103 let matching = unsafe { IOServiceMatching(c"IOUSBHostDevice".as_ptr()) };
106 if matching.is_null() {
107 return devices;
108 }
109
110 let mut iter: u32 = 0;
111 let kr =
114 unsafe { IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter) };
115 if kr != 0 || iter == 0 {
116 return devices;
117 }
118
119 loop {
120 let service = unsafe { IOIteratorNext(iter) };
122 if service == 0 {
123 break;
124 }
125 devices.push(service);
126 }
127 unsafe { IOObjectRelease(iter) };
129 devices
130 }
131
132 pub fn query_device_property(device: u32, key: &[u8]) -> std::time::Duration {
134 let cf_key = cfstr(key);
135 if cf_key.is_null() {
136 return std::time::Duration::ZERO;
137 }
138 let t0 = std::time::Instant::now();
139 let prop = unsafe { IORegistryEntryCreateCFProperty(device, cf_key, std::ptr::null(), 0) };
142 let elapsed = t0.elapsed();
143 if !prop.is_null() {
144 unsafe { CFRelease(prop) };
146 }
147 unsafe { CFRelease(cf_key) };
149 elapsed
150 }
151}
152
153impl EntropySource for USBTimingSource {
154 fn info(&self) -> &SourceInfo {
155 &USB_TIMING_INFO
156 }
157
158 fn is_available(&self) -> bool {
159 #[cfg(target_os = "macos")]
160 {
161 let devices = iokit::find_usb_devices();
163 let available = !devices.is_empty();
164 for device in &devices {
166 unsafe { iokit::IOObjectRelease(*device) };
168 }
169 available
170 }
171 #[cfg(not(target_os = "macos"))]
172 {
173 false
174 }
175 }
176
177 fn collect(&self, n_samples: usize) -> Vec<u8> {
178 #[cfg(not(target_os = "macos"))]
179 {
180 let _ = n_samples;
181 Vec::new()
182 }
183
184 #[cfg(target_os = "macos")]
185 {
186 let devices = iokit::find_usb_devices();
187 if devices.is_empty() {
188 return Vec::new();
189 }
190
191 let raw_count = n_samples * 4 + 64;
192 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
193
194 let property_keys: &[&[u8]] = &[b"sessionID\0", b"USB Address\0"];
195
196 for i in 0..raw_count {
197 let device = devices[i % devices.len()];
198 let key = property_keys[i % property_keys.len()];
199 let elapsed = iokit::query_device_property(device, key);
200 timings.push(elapsed.as_nanos() as u64);
201 }
202
203 for device in &devices {
206 unsafe { iokit::IOObjectRelease(*device) };
208 }
209 drop(devices);
210
211 extract_timing_entropy(&timings, n_samples)
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn info() {
222 let src = USBTimingSource;
223 assert_eq!(src.name(), "usb_timing");
224 assert_eq!(src.info().category, SourceCategory::IO);
225 assert!(!src.info().composite);
226 }
227
228 #[test]
229 #[cfg(target_os = "macos")]
230 #[ignore] fn collects_bytes() {
232 let src = USBTimingSource;
233 if src.is_available() {
234 let data = src.collect(64);
235 assert!(!data.is_empty());
236 assert!(data.len() <= 64);
237 }
238 }
239}