use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
#[cfg(target_os = "macos")]
use crate::sources::helpers::mach_time;
#[cfg(target_os = "macos")]
use crate::sources::helpers::extract_timing_entropy_variance;
#[derive(Debug, Clone, Default)]
pub struct KeychainTimingConfig {
pub use_write_path: bool,
}
#[derive(Default)]
pub struct KeychainTimingSource {
pub config: KeychainTimingConfig,
}
static KEYCHAIN_TIMING_INFO: SourceInfo = SourceInfo {
name: "keychain_timing",
description: "Keychain/securityd round-trip timing jitter",
physics: "Times keychain operations that traverse: XPC IPC to securityd → SQLite \
database lookup → access control evaluation → return. Each domain (IPC \
scheduling, securityd process scheduling, database page I/O, kernel context \
switches) contributes independent jitter. Variance extraction removes serial \
correlation (lag-1 autocorrelation ~0.43 in raw timings). First 100 samples \
discarded to avoid warm-up transient from securityd cold caches.",
category: SourceCategory::IPC,
platform: Platform::MacOS,
requirements: &[Requirement::SecurityFramework],
entropy_rate_estimate: 3.0,
composite: false,
is_fast: false,
};
impl EntropySource for KeychainTimingSource {
fn info(&self) -> &SourceInfo {
&KEYCHAIN_TIMING_INFO
}
fn is_available(&self) -> bool {
cfg!(target_os = "macos")
}
fn collect(&self, n_samples: usize) -> Vec<u8> {
#[cfg(target_os = "macos")]
{
if self.config.use_write_path {
collect_write_path(n_samples)
} else {
collect_read_path(n_samples)
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = n_samples;
Vec::new()
}
}
}
#[cfg(target_os = "macos")]
const WARMUP_SAMPLES: usize = 100;
#[cfg(target_os = "macos")]
fn collect_read_path(n_samples: usize) -> Vec<u8> {
#[link(name = "Security", kind = "framework")]
unsafe extern "C" {
static kSecClass: CFStringRef;
static kSecClassGenericPassword: CFStringRef;
static kSecAttrLabel: CFStringRef;
static kSecReturnData: CFStringRef;
static kCFBooleanTrue: CFBooleanRef;
static kSecValueData: CFStringRef;
static kSecAttrAccessible: CFStringRef;
static kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: CFStringRef;
fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
fn SecItemDelete(query: CFDictionaryRef) -> i32;
fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFStringCreateWithCString(
alloc: CFAllocatorRef,
cStr: *const i8,
encoding: u32,
) -> CFStringRef;
fn CFDataCreate(alloc: CFAllocatorRef, bytes: *const u8, length: isize) -> CFDataRef;
fn CFDictionaryCreateMutable(
alloc: CFAllocatorRef,
capacity: isize,
keyCallBacks: *const std::ffi::c_void,
valueCallBacks: *const std::ffi::c_void,
) -> CFMutableDictionaryRef;
fn CFDictionarySetValue(
dict: CFMutableDictionaryRef,
key: *const std::ffi::c_void,
value: *const std::ffi::c_void,
);
fn CFRelease(cf: CFTypeRef);
static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks;
static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks;
}
type CFAllocatorRef = *const std::ffi::c_void;
type CFStringRef = *const std::ffi::c_void;
type CFDataRef = *const std::ffi::c_void;
type CFBooleanRef = *const std::ffi::c_void;
type CFDictionaryRef = *const std::ffi::c_void;
type CFMutableDictionaryRef = *mut std::ffi::c_void;
type CFTypeRef = *const std::ffi::c_void;
#[repr(C)]
struct CFDictionaryKeyCallBacks {
_data: [u8; 40],
}
#[repr(C)]
struct CFDictionaryValueCallBacks {
_data: [u8; 40],
}
const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
let label_cstr = b"openentropy-timing-probe\0";
unsafe {
let label_cf = CFStringCreateWithCString(
std::ptr::null(),
label_cstr.as_ptr() as *const i8,
K_CF_STRING_ENCODING_UTF8,
);
let secret: [u8; 16] = [0x42; 16];
let secret_cf = CFDataCreate(std::ptr::null(), secret.as_ptr(), 16);
let add_dict = CFDictionaryCreateMutable(
std::ptr::null(),
0,
&kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
&kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
);
CFDictionarySetValue(add_dict, kSecClass as _, kSecClassGenericPassword as _);
CFDictionarySetValue(add_dict, kSecAttrLabel as _, label_cf as _);
CFDictionarySetValue(add_dict, kSecValueData as _, secret_cf as _);
CFDictionarySetValue(
add_dict,
kSecAttrAccessible as _,
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as _,
);
let del_dict = CFDictionaryCreateMutable(
std::ptr::null(),
0,
&kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
&kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
);
CFDictionarySetValue(del_dict, kSecClass as _, kSecClassGenericPassword as _);
CFDictionarySetValue(del_dict, kSecAttrLabel as _, label_cf as _);
SecItemDelete(del_dict as _);
SecItemAdd(add_dict as _, std::ptr::null_mut());
let query = CFDictionaryCreateMutable(
std::ptr::null(),
0,
&kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
&kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
);
CFDictionarySetValue(query, kSecClass as _, kSecClassGenericPassword as _);
CFDictionarySetValue(query, kSecAttrLabel as _, label_cf as _);
CFDictionarySetValue(query, kSecReturnData as _, kCFBooleanTrue as _);
for _ in 0..WARMUP_SAMPLES {
let mut result: CFTypeRef = std::ptr::null();
SecItemCopyMatching(query as _, &mut result);
if !result.is_null() {
CFRelease(result);
}
}
let raw_count = n_samples * 2 + 64;
let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
for _ in 0..raw_count {
let mut result: CFTypeRef = std::ptr::null();
let t0 = mach_time();
SecItemCopyMatching(query as _, &mut result);
let t1 = mach_time();
timings.push(t1.wrapping_sub(t0));
if !result.is_null() {
CFRelease(result);
}
}
SecItemDelete(del_dict as _);
CFRelease(del_dict as CFTypeRef);
CFRelease(add_dict as CFTypeRef);
CFRelease(query as CFTypeRef);
CFRelease(secret_cf);
CFRelease(label_cf);
extract_timing_entropy_variance(&timings, n_samples)
}
}
#[cfg(target_os = "macos")]
fn collect_write_path(n_samples: usize) -> Vec<u8> {
#[link(name = "Security", kind = "framework")]
unsafe extern "C" {
static kSecClass: CFStringRef;
static kSecClassGenericPassword: CFStringRef;
static kSecAttrLabel: CFStringRef;
static kSecValueData: CFStringRef;
static kSecAttrAccessible: CFStringRef;
static kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: CFStringRef;
fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
fn SecItemDelete(query: CFDictionaryRef) -> i32;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFStringCreateWithCString(
alloc: CFAllocatorRef,
cStr: *const i8,
encoding: u32,
) -> CFStringRef;
fn CFDataCreate(alloc: CFAllocatorRef, bytes: *const u8, length: isize) -> CFDataRef;
fn CFDictionaryCreateMutable(
alloc: CFAllocatorRef,
capacity: isize,
keyCallBacks: *const std::ffi::c_void,
valueCallBacks: *const std::ffi::c_void,
) -> CFMutableDictionaryRef;
fn CFDictionarySetValue(
dict: CFMutableDictionaryRef,
key: *const std::ffi::c_void,
value: *const std::ffi::c_void,
);
fn CFRelease(cf: CFTypeRef);
static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks;
static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks;
}
type CFAllocatorRef = *const std::ffi::c_void;
type CFStringRef = *const std::ffi::c_void;
type CFDataRef = *const std::ffi::c_void;
type CFDictionaryRef = *const std::ffi::c_void;
type CFMutableDictionaryRef = *mut std::ffi::c_void;
type CFTypeRef = *const std::ffi::c_void;
#[repr(C)]
struct CFDictionaryKeyCallBacks {
_data: [u8; 40],
}
#[repr(C)]
struct CFDictionaryValueCallBacks {
_data: [u8; 40],
}
const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
let raw_count = n_samples * 4 + 64;
let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
unsafe {
for i in 0..raw_count {
let mut label_buf = [0u8; 64];
let label_str = format!("oe-ent-{}\0", i);
label_buf[..label_str.len()].copy_from_slice(label_str.as_bytes());
let label_cf = CFStringCreateWithCString(
std::ptr::null(),
label_buf.as_ptr() as *const i8,
K_CF_STRING_ENCODING_UTF8,
);
let secret: [u8; 16] = [i as u8; 16];
let secret_cf = CFDataCreate(std::ptr::null(), secret.as_ptr(), 16);
let attrs = CFDictionaryCreateMutable(
std::ptr::null(),
0,
&kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
&kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
);
CFDictionarySetValue(attrs, kSecClass as _, kSecClassGenericPassword as _);
CFDictionarySetValue(attrs, kSecAttrLabel as _, label_cf as _);
CFDictionarySetValue(attrs, kSecValueData as _, secret_cf as _);
CFDictionarySetValue(
attrs,
kSecAttrAccessible as _,
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as _,
);
let t0 = mach_time();
let status = SecItemAdd(attrs as _, std::ptr::null_mut());
let t1 = mach_time();
if status == 0 {
timings.push(t1.wrapping_sub(t0));
}
let del = CFDictionaryCreateMutable(
std::ptr::null(),
0,
&kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
&kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
);
CFDictionarySetValue(del, kSecClass as _, kSecClassGenericPassword as _);
CFDictionarySetValue(del, kSecAttrLabel as _, label_cf as _);
SecItemDelete(del as _);
CFRelease(del as CFTypeRef);
CFRelease(attrs as CFTypeRef);
CFRelease(secret_cf);
CFRelease(label_cf);
}
}
extract_timing_entropy_variance(&timings, n_samples)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn info() {
let src = KeychainTimingSource::default();
assert_eq!(src.name(), "keychain_timing");
assert_eq!(src.info().category, SourceCategory::IPC);
assert!(!src.info().composite);
}
#[test]
fn default_config() {
let config = KeychainTimingConfig::default();
assert!(!config.use_write_path);
}
#[test]
#[ignore] fn collects_bytes_read_path() {
let src = KeychainTimingSource::default();
if src.is_available() {
let data = src.collect(64);
assert!(!data.is_empty());
assert!(data.len() <= 64);
}
}
#[test]
#[ignore] fn collects_bytes_write_path() {
let src = KeychainTimingSource {
config: KeychainTimingConfig {
use_write_path: true,
},
};
if src.is_available() {
let data = src.collect(32);
assert!(!data.is_empty());
assert!(data.len() <= 32);
}
}
}