openentropy_core/sources/ipc/
keychain_timing.rs1use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
5#[cfg(target_os = "macos")]
6use crate::sources::helpers::mach_time;
7
8#[cfg(target_os = "macos")]
9use crate::sources::helpers::extract_timing_entropy_variance;
10
11#[derive(Debug, Clone, Default)]
19pub struct KeychainTimingConfig {
20 pub use_write_path: bool,
24}
25
26#[derive(Default)]
56pub struct KeychainTimingSource {
57 pub config: KeychainTimingConfig,
59}
60
61static KEYCHAIN_TIMING_INFO: SourceInfo = SourceInfo {
62 name: "keychain_timing",
63 description: "Keychain/securityd round-trip timing jitter",
64 physics: "Times keychain operations that traverse: XPC IPC to securityd → SQLite \
65 database lookup → access control evaluation → return. Each domain (IPC \
66 scheduling, securityd process scheduling, database page I/O, kernel context \
67 switches) contributes independent jitter. Variance extraction removes serial \
68 correlation (lag-1 autocorrelation ~0.43 in raw timings). First 100 samples \
69 discarded to avoid warm-up transient from securityd cold caches.",
70 category: SourceCategory::IPC,
71 platform: Platform::MacOS,
72 requirements: &[Requirement::SecurityFramework],
73 entropy_rate_estimate: 3.0,
74 composite: false,
75 is_fast: false,
76};
77
78impl EntropySource for KeychainTimingSource {
79 fn info(&self) -> &SourceInfo {
80 &KEYCHAIN_TIMING_INFO
81 }
82
83 fn is_available(&self) -> bool {
84 cfg!(target_os = "macos")
85 }
86
87 fn collect(&self, n_samples: usize) -> Vec<u8> {
88 #[cfg(target_os = "macos")]
89 {
90 if self.config.use_write_path {
91 collect_write_path(n_samples)
92 } else {
93 collect_read_path(n_samples)
94 }
95 }
96 #[cfg(not(target_os = "macos"))]
97 {
98 let _ = n_samples;
99 Vec::new()
100 }
101 }
102}
103
104#[cfg(target_os = "macos")]
105const WARMUP_SAMPLES: usize = 100;
106
107#[cfg(target_os = "macos")]
110fn collect_read_path(n_samples: usize) -> Vec<u8> {
111 #[link(name = "Security", kind = "framework")]
113 unsafe extern "C" {
114 static kSecClass: CFStringRef;
115 static kSecClassGenericPassword: CFStringRef;
116 static kSecAttrLabel: CFStringRef;
117 static kSecReturnData: CFStringRef;
118 static kCFBooleanTrue: CFBooleanRef;
119 static kSecValueData: CFStringRef;
120 static kSecAttrAccessible: CFStringRef;
121 static kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: CFStringRef;
122
123 fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
124 fn SecItemDelete(query: CFDictionaryRef) -> i32;
125 fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
126 }
127
128 #[link(name = "CoreFoundation", kind = "framework")]
129 unsafe extern "C" {
130 fn CFStringCreateWithCString(
131 alloc: CFAllocatorRef,
132 cStr: *const i8,
133 encoding: u32,
134 ) -> CFStringRef;
135 fn CFDataCreate(alloc: CFAllocatorRef, bytes: *const u8, length: isize) -> CFDataRef;
136 fn CFDictionaryCreateMutable(
137 alloc: CFAllocatorRef,
138 capacity: isize,
139 keyCallBacks: *const std::ffi::c_void,
140 valueCallBacks: *const std::ffi::c_void,
141 ) -> CFMutableDictionaryRef;
142 fn CFDictionarySetValue(
143 dict: CFMutableDictionaryRef,
144 key: *const std::ffi::c_void,
145 value: *const std::ffi::c_void,
146 );
147 fn CFRelease(cf: CFTypeRef);
148
149 static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks;
150 static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks;
151 }
152
153 type CFAllocatorRef = *const std::ffi::c_void;
155 type CFStringRef = *const std::ffi::c_void;
156 type CFDataRef = *const std::ffi::c_void;
157 type CFBooleanRef = *const std::ffi::c_void;
158 type CFDictionaryRef = *const std::ffi::c_void;
159 type CFMutableDictionaryRef = *mut std::ffi::c_void;
160 type CFTypeRef = *const std::ffi::c_void;
161
162 #[repr(C)]
163 struct CFDictionaryKeyCallBacks {
164 _data: [u8; 40],
165 }
166 #[repr(C)]
167 struct CFDictionaryValueCallBacks {
168 _data: [u8; 40],
169 }
170
171 const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
172
173 let label_cstr = b"openentropy-timing-probe\0";
174
175 unsafe {
176 let label_cf = CFStringCreateWithCString(
178 std::ptr::null(),
179 label_cstr.as_ptr() as *const i8,
180 K_CF_STRING_ENCODING_UTF8,
181 );
182 let secret: [u8; 16] = [0x42; 16];
183 let secret_cf = CFDataCreate(std::ptr::null(), secret.as_ptr(), 16);
184
185 let add_dict = CFDictionaryCreateMutable(
187 std::ptr::null(),
188 0,
189 &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
190 &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
191 );
192 CFDictionarySetValue(add_dict, kSecClass as _, kSecClassGenericPassword as _);
193 CFDictionarySetValue(add_dict, kSecAttrLabel as _, label_cf as _);
194 CFDictionarySetValue(add_dict, kSecValueData as _, secret_cf as _);
195 CFDictionarySetValue(
196 add_dict,
197 kSecAttrAccessible as _,
198 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as _,
199 );
200
201 let del_dict = CFDictionaryCreateMutable(
203 std::ptr::null(),
204 0,
205 &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
206 &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
207 );
208 CFDictionarySetValue(del_dict, kSecClass as _, kSecClassGenericPassword as _);
209 CFDictionarySetValue(del_dict, kSecAttrLabel as _, label_cf as _);
210 SecItemDelete(del_dict as _);
211 SecItemAdd(add_dict as _, std::ptr::null_mut());
212
213 let query = CFDictionaryCreateMutable(
215 std::ptr::null(),
216 0,
217 &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
218 &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
219 );
220 CFDictionarySetValue(query, kSecClass as _, kSecClassGenericPassword as _);
221 CFDictionarySetValue(query, kSecAttrLabel as _, label_cf as _);
222 CFDictionarySetValue(query, kSecReturnData as _, kCFBooleanTrue as _);
223
224 for _ in 0..WARMUP_SAMPLES {
226 let mut result: CFTypeRef = std::ptr::null();
227 SecItemCopyMatching(query as _, &mut result);
228 if !result.is_null() {
229 CFRelease(result);
230 }
231 }
232
233 let raw_count = n_samples * 2 + 64;
235 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
236
237 for _ in 0..raw_count {
238 let mut result: CFTypeRef = std::ptr::null();
239 let t0 = mach_time();
240 SecItemCopyMatching(query as _, &mut result);
241 let t1 = mach_time();
242 timings.push(t1.wrapping_sub(t0));
243 if !result.is_null() {
244 CFRelease(result);
245 }
246 }
247
248 SecItemDelete(del_dict as _);
250 CFRelease(del_dict as CFTypeRef);
251 CFRelease(add_dict as CFTypeRef);
252 CFRelease(query as CFTypeRef);
253 CFRelease(secret_cf);
254 CFRelease(label_cf);
255
256 extract_timing_entropy_variance(&timings, n_samples)
257 }
258}
259
260#[cfg(target_os = "macos")]
263fn collect_write_path(n_samples: usize) -> Vec<u8> {
264 #[link(name = "Security", kind = "framework")]
265 unsafe extern "C" {
266 static kSecClass: CFStringRef;
267 static kSecClassGenericPassword: CFStringRef;
268 static kSecAttrLabel: CFStringRef;
269 static kSecValueData: CFStringRef;
270 static kSecAttrAccessible: CFStringRef;
271 static kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: CFStringRef;
272
273 fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
274 fn SecItemDelete(query: CFDictionaryRef) -> i32;
275 }
276
277 #[link(name = "CoreFoundation", kind = "framework")]
278 unsafe extern "C" {
279 fn CFStringCreateWithCString(
280 alloc: CFAllocatorRef,
281 cStr: *const i8,
282 encoding: u32,
283 ) -> CFStringRef;
284 fn CFDataCreate(alloc: CFAllocatorRef, bytes: *const u8, length: isize) -> CFDataRef;
285 fn CFDictionaryCreateMutable(
286 alloc: CFAllocatorRef,
287 capacity: isize,
288 keyCallBacks: *const std::ffi::c_void,
289 valueCallBacks: *const std::ffi::c_void,
290 ) -> CFMutableDictionaryRef;
291 fn CFDictionarySetValue(
292 dict: CFMutableDictionaryRef,
293 key: *const std::ffi::c_void,
294 value: *const std::ffi::c_void,
295 );
296 fn CFRelease(cf: CFTypeRef);
297
298 static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks;
299 static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks;
300 }
301
302 type CFAllocatorRef = *const std::ffi::c_void;
303 type CFStringRef = *const std::ffi::c_void;
304 type CFDataRef = *const std::ffi::c_void;
305 type CFDictionaryRef = *const std::ffi::c_void;
306 type CFMutableDictionaryRef = *mut std::ffi::c_void;
307 type CFTypeRef = *const std::ffi::c_void;
308
309 #[repr(C)]
310 struct CFDictionaryKeyCallBacks {
311 _data: [u8; 40],
312 }
313 #[repr(C)]
314 struct CFDictionaryValueCallBacks {
315 _data: [u8; 40],
316 }
317
318 const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
319
320 let raw_count = n_samples * 4 + 64;
321 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
322
323 unsafe {
324 for i in 0..raw_count {
325 let mut label_buf = [0u8; 64];
326 let label_str = format!("oe-ent-{}\0", i);
327 label_buf[..label_str.len()].copy_from_slice(label_str.as_bytes());
328
329 let label_cf = CFStringCreateWithCString(
330 std::ptr::null(),
331 label_buf.as_ptr() as *const i8,
332 K_CF_STRING_ENCODING_UTF8,
333 );
334
335 let secret: [u8; 16] = [i as u8; 16];
336 let secret_cf = CFDataCreate(std::ptr::null(), secret.as_ptr(), 16);
337
338 let attrs = CFDictionaryCreateMutable(
339 std::ptr::null(),
340 0,
341 &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
342 &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
343 );
344 CFDictionarySetValue(attrs, kSecClass as _, kSecClassGenericPassword as _);
345 CFDictionarySetValue(attrs, kSecAttrLabel as _, label_cf as _);
346 CFDictionarySetValue(attrs, kSecValueData as _, secret_cf as _);
347 CFDictionarySetValue(
348 attrs,
349 kSecAttrAccessible as _,
350 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as _,
351 );
352
353 let t0 = mach_time();
354 let status = SecItemAdd(attrs as _, std::ptr::null_mut());
355 let t1 = mach_time();
356
357 if status == 0 {
358 timings.push(t1.wrapping_sub(t0));
360 }
361
362 let del = CFDictionaryCreateMutable(
364 std::ptr::null(),
365 0,
366 &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
367 &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
368 );
369 CFDictionarySetValue(del, kSecClass as _, kSecClassGenericPassword as _);
370 CFDictionarySetValue(del, kSecAttrLabel as _, label_cf as _);
371 SecItemDelete(del as _);
372
373 CFRelease(del as CFTypeRef);
374 CFRelease(attrs as CFTypeRef);
375 CFRelease(secret_cf);
376 CFRelease(label_cf);
377 }
378 }
379
380 extract_timing_entropy_variance(&timings, n_samples)
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn info() {
389 let src = KeychainTimingSource::default();
390 assert_eq!(src.name(), "keychain_timing");
391 assert_eq!(src.info().category, SourceCategory::IPC);
392 assert!(!src.info().composite);
393 }
394
395 #[test]
396 fn default_config() {
397 let config = KeychainTimingConfig::default();
398 assert!(!config.use_write_path);
399 }
400
401 #[test]
402 #[ignore] fn collects_bytes_read_path() {
404 let src = KeychainTimingSource::default();
405 if src.is_available() {
406 let data = src.collect(64);
407 assert!(!data.is_empty());
408 assert!(data.len() <= 64);
409 }
410 }
411
412 #[test]
413 #[ignore] fn collects_bytes_write_path() {
415 let src = KeychainTimingSource {
416 config: KeychainTimingConfig {
417 use_write_path: true,
418 },
419 };
420 if src.is_available() {
421 let data = src.collect(32);
422 assert!(!data.is_empty());
423 assert!(data.len() <= 32);
424 }
425 }
426}