openentropy_core/sources/io/
usb_enumeration.rs1use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
42
43#[cfg(target_os = "macos")]
44use crate::sources::helpers::{extract_timing_entropy, mach_time};
45
46static USB_ENUMERATION_INFO: SourceInfo = SourceInfo {
47 name: "usb_enumeration",
48 description: "IOKit USB device enumeration timing — CV=116%, USB controller state",
49 physics: "Times IOKit USB device enumeration (IOServiceMatching(kIOUSBDeviceClassName) + \
50 device tree walk). USB enumeration latency varies with: USB xHCI controller \
51 port state, USB bus traffic from active devices, IOKit registry lock contention \
52 from hot-plug events, controller power state wake-up latency. \
53 Measured: mean=13046 ticks (~544 µs), CV=116.2%, range=[9791,169338]. \
54 Cross-process sensitivity: any process using USB devices or hot-plugging \
55 changes enumeration timing.",
56 category: SourceCategory::IO,
57 platform: Platform::MacOS,
58 requirements: &[],
59 entropy_rate_estimate: 1.5,
60 composite: false,
61 is_fast: false,
62};
63
64pub struct USBEnumerationSource;
66
67#[cfg(target_os = "macos")]
68mod usb_imp {
69 use std::ffi::c_void;
70
71 pub type IOReturn = i32;
72 pub type MachPort = u32;
73
74 #[link(name = "IOKit", kind = "framework")]
75 unsafe extern "C" {
76 pub fn IOServiceMatching(name: *const i8) -> *mut c_void;
77 pub fn IOServiceGetMatchingServices(
78 main_port: MachPort,
79 matching: *const c_void,
80 iter: *mut u32,
81 ) -> IOReturn;
82 pub fn IOIteratorNext(iterator: u32) -> u32;
83 pub fn IOObjectRelease(obj: u32) -> IOReturn;
84 }
85
86 pub const K_IO_MAIN_PORT_DEFAULT: MachPort = 0;
87}
88
89#[cfg(target_os = "macos")]
90impl EntropySource for USBEnumerationSource {
91 fn info(&self) -> &SourceInfo {
92 &USB_ENUMERATION_INFO
93 }
94
95 fn is_available(&self) -> bool {
96 true
97 }
98
99 fn collect(&self, n_samples: usize) -> Vec<u8> {
100 use usb_imp::*;
101
102 let raw = n_samples * 2 + 32;
103 let mut timings = Vec::with_capacity(raw);
104
105 for _ in 0..4 {
107 let matching = unsafe { IOServiceMatching(c"IOUSBDevice".as_ptr()) };
108 if !matching.is_null() {
109 let mut iter: u32 = 0;
110 unsafe {
111 IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter);
112 if iter != 0 {
113 let mut obj = IOIteratorNext(iter);
114 while obj != 0 {
115 IOObjectRelease(obj);
116 obj = IOIteratorNext(iter);
117 }
118 IOObjectRelease(iter);
119 }
120 }
121 }
122 }
123
124 for _ in 0..raw {
125 let matching = unsafe { IOServiceMatching(c"IOUSBDevice".as_ptr()) };
126 if matching.is_null() {
127 continue;
128 }
129
130 let t0 = mach_time();
131 let mut iter: u32 = 0;
132 let kr = unsafe {
133 IOServiceGetMatchingServices(K_IO_MAIN_PORT_DEFAULT, matching, &mut iter)
134 };
135
136 if kr == 0 && iter != 0 {
137 let mut count = 0;
139 let mut obj = unsafe { IOIteratorNext(iter) };
140 while obj != 0 && count < 50 {
141 unsafe { IOObjectRelease(obj) };
142 obj = unsafe { IOIteratorNext(iter) };
143 count += 1;
144 }
145 unsafe { IOObjectRelease(iter) };
146 }
147
148 let elapsed = mach_time().wrapping_sub(t0);
149 if elapsed < 2_400_000 {
151 timings.push(elapsed);
152 }
153 }
154
155 extract_timing_entropy(&timings, n_samples)
156 }
157}
158
159#[cfg(not(target_os = "macos"))]
160impl EntropySource for USBEnumerationSource {
161 fn info(&self) -> &SourceInfo {
162 &USB_ENUMERATION_INFO
163 }
164 fn is_available(&self) -> bool {
165 false
166 }
167 fn collect(&self, _: usize) -> Vec<u8> {
168 Vec::new()
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn info() {
178 let src = USBEnumerationSource;
179 assert_eq!(src.info().name, "usb_enumeration");
180 assert!(matches!(src.info().category, SourceCategory::IO));
181 assert_eq!(src.info().platform, Platform::MacOS);
182 }
183
184 #[test]
185 #[cfg(target_os = "macos")]
186 fn is_available() {
187 assert!(USBEnumerationSource.is_available());
188 }
189
190 #[test]
191 #[ignore]
192 fn collects_usb_controller_state() {
193 let data = USBEnumerationSource.collect(32);
194 assert!(!data.is_empty());
195 }
196}