use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
static COMMONCRYPTO_AES_TIMING_INFO: SourceInfo = SourceInfo {
name: "commoncrypto_aes_timing",
description: "CommonCrypto AES-128-CBC warm/cold key schedule bimodal timing",
physics: "Times CCCrypt(AES-128-CBC) calls with rotating keys. Framework dispatch \
shows bimodal distribution: ~50 ticks (warm, key schedule cached) vs \
~120 ticks (cold, key reload via system fabric). CV=155.4%; warm/cold \
transition driven by other processes' AES load (FileVault, HTTPS), AES \
coprocessor power management, and thermal state. Distinct from direct \
AESE instruction timing (aes_exec_timing): captures framework overhead \
and key schedule management layer. Cross-process sensitivity: Time Machine \
/ FileVault bursts visibly shift distribution toward cold path.",
category: SourceCategory::Microarch,
platform: Platform::MacOS,
requirements: &[],
entropy_rate_estimate: 2.0,
composite: false,
is_fast: false,
};
pub struct CommonCryptoAesTimingSource;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
mod imp {
use super::*;
use crate::sources::helpers::extract_timing_entropy_debiased;
use crate::sources::helpers::mach_time;
const CC_ENCRYPT: u32 = 0;
const CC_AES: u32 = 0;
const CC_CBC_MODE: u32 = 2;
const CC_SUCCESS: i32 = 0;
const AES_KEY_SIZE: usize = 16;
const AES_BLOCK_SIZE: usize = 16;
unsafe extern "C" {
fn CCCrypt(
operation: u32,
algorithm: u32,
options: u32,
key: *const u8,
key_length: usize,
iv: *const u8,
data_in: *const u8,
data_in_len: usize,
data_out: *mut u8,
data_out_available: usize,
data_out_moved: *mut usize,
) -> i32;
}
unsafe fn time_cccrypt(
key: &[u8; AES_KEY_SIZE],
iv: &[u8; AES_BLOCK_SIZE],
plaintext: &[u8; AES_BLOCK_SIZE],
) -> Option<u64> {
let mut ciphertext = [0u8; AES_BLOCK_SIZE];
let mut out_moved: usize = 0;
let t0 = mach_time();
let status = unsafe {
CCCrypt(
CC_ENCRYPT,
CC_AES,
CC_CBC_MODE,
key.as_ptr(),
AES_KEY_SIZE,
iv.as_ptr(),
plaintext.as_ptr(),
AES_BLOCK_SIZE,
ciphertext.as_mut_ptr(),
AES_BLOCK_SIZE,
&mut out_moved,
)
};
let t1 = mach_time();
if status == CC_SUCCESS {
Some(t1.wrapping_sub(t0))
} else {
None
}
}
fn cccrypt_probe() -> bool {
let key = [0u8; AES_KEY_SIZE];
let iv = [0u8; AES_BLOCK_SIZE];
let plaintext = [0u8; AES_BLOCK_SIZE];
unsafe { time_cccrypt(&key, &iv, &plaintext).is_some() }
}
impl EntropySource for CommonCryptoAesTimingSource {
fn info(&self) -> &SourceInfo {
&COMMONCRYPTO_AES_TIMING_INFO
}
fn is_available(&self) -> bool {
static CCCRYPT_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*CCCRYPT_AVAILABLE.get_or_init(cccrypt_probe)
}
fn collect(&self, n_samples: usize) -> Vec<u8> {
let raw_count = n_samples * 8 + 128;
let mut timings = Vec::with_capacity(raw_count);
let base_key: [u8; AES_KEY_SIZE] = [
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
0x4f, 0x3c,
];
let base_iv: [u8; AES_BLOCK_SIZE] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
];
let plaintext: [u8; AES_BLOCK_SIZE] = [
0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93,
0x17, 0x2a,
];
for i in 0..32_usize {
let mut key = base_key;
for j in 0..16 {
key[j] = base_key[j].wrapping_add(i as u8);
}
let _ = unsafe { time_cccrypt(&key, &base_iv, &plaintext) };
}
for i in 0..raw_count {
let mut key = [0u8; AES_KEY_SIZE];
for j in 0..16 {
key[j] = base_key[(j + i) & 15].wrapping_add((i >> 4) as u8);
}
if let Some(t) = unsafe { time_cccrypt(&key, &base_iv, &plaintext) } {
if t < 50_000 {
timings.push(t);
}
}
}
let shifted: Vec<u64> = timings.iter().map(|&t| t >> 1).collect();
extract_timing_entropy_debiased(&shifted, n_samples)
}
}
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
impl EntropySource for CommonCryptoAesTimingSource {
fn info(&self) -> &SourceInfo {
&COMMONCRYPTO_AES_TIMING_INFO
}
fn is_available(&self) -> bool {
false
}
fn collect(&self, _n_samples: usize) -> Vec<u8> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn info() {
let src = CommonCryptoAesTimingSource;
assert_eq!(src.info().name, "commoncrypto_aes_timing");
assert!(matches!(src.info().category, SourceCategory::Microarch));
assert_eq!(src.info().platform, Platform::MacOS);
assert!(!src.info().composite);
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn is_available_on_macos() {
assert!(CommonCryptoAesTimingSource.is_available());
}
#[test]
#[ignore] fn collects_bimodal_variation() {
let src = CommonCryptoAesTimingSource;
if !src.is_available() {
return;
}
let data = src.collect(32);
assert!(!data.is_empty());
let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
assert!(
unique.len() > 2,
"expected bimodal variation from CCCrypt warm/cold paths"
);
}
}