#[cfg(target_os = "macos")]
pub fn mach_time() -> u64 {
unsafe extern "C" {
fn mach_absolute_time() -> u64;
}
unsafe { mach_absolute_time() }
}
#[cfg(not(target_os = "macos"))]
pub fn mach_time() -> u64 {
use std::sync::OnceLock;
use std::time::Instant;
static EPOCH: OnceLock<Instant> = OnceLock::new();
let epoch = EPOCH.get_or_init(Instant::now);
epoch.elapsed().as_nanos() as u64
}
fn pack_bits_into_bytes(bits: &[u8]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(bits.len() / 8);
for chunk in bits.chunks_exact(8) {
let mut byte = 0u8;
for (i, &bit) in chunk.iter().enumerate() {
byte |= bit << (7 - i);
}
bytes.push(byte);
}
bytes
}
pub fn extract_lsbs_u64(deltas: &[u64]) -> Vec<u8> {
let bits: Vec<u8> = deltas.iter().map(|d| (d & 1) as u8).collect();
pack_bits_into_bytes(&bits)
}
pub fn extract_lsbs_i64(deltas: &[i64]) -> Vec<u8> {
let bits: Vec<u8> = deltas.iter().map(|d| (d & 1) as u8).collect();
pack_bits_into_bytes(&bits)
}
pub fn command_exists(name: &str) -> bool {
std::process::Command::new("which")
.arg(name)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn run_command_output(program: &str, args: &[&str]) -> Option<std::process::Output> {
run_command_output_timeout(program, args, 30_000)
}
pub fn run_command_output_timeout(
program: &str,
args: &[&str],
timeout_ms: u64,
) -> Option<std::process::Output> {
use std::io::Read;
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
let mut child = Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let mut stdout_reader = child.stdout.take().map(|mut stdout| {
thread::spawn(move || {
let mut buf = Vec::new();
let _ = stdout.read_to_end(&mut buf);
buf
})
});
let start = Instant::now();
let timeout = Duration::from_millis(timeout_ms.max(1));
loop {
match child.try_wait() {
Ok(Some(status)) => {
let stdout = stdout_reader
.take()
.and_then(|h| h.join().ok())
.unwrap_or_default();
let output = std::process::Output {
status,
stdout,
stderr: Vec::new(),
};
return output.status.success().then_some(output);
}
Ok(None) => {
if start.elapsed() >= timeout {
let _ = child.kill();
let _ = child.wait();
if let Some(reader) = stdout_reader.take() {
let _ = reader.join();
}
return None;
}
thread::sleep(Duration::from_millis(10));
}
Err(_e) => {
let _ = child.kill();
let _ = child.wait();
if let Some(reader) = stdout_reader.take() {
let _ = reader.join();
}
return None;
}
}
}
}
pub fn run_command(program: &str, args: &[&str]) -> Option<String> {
let output = run_command_output(program, args)?;
Some(String::from_utf8_lossy(&output.stdout).into_owned())
}
pub fn run_command_raw(program: &str, args: &[&str]) -> Option<Vec<u8>> {
run_command_output(program, args).map(|o| o.stdout)
}
pub fn run_command_raw_timeout(program: &str, args: &[&str], timeout_ms: u64) -> Option<Vec<u8>> {
run_command_output_timeout(program, args, timeout_ms).map(|o| o.stdout)
}
pub fn capture_camera_gray_frame(timeout_ms: u64, device_index: Option<u32>) -> Option<Vec<u8>> {
match device_index {
Some(n) => {
let input = format!("{n}:none");
capture_camera_with_inputs(timeout_ms, &[&input])
}
None => {
capture_camera_with_inputs(timeout_ms, &["default:none", "0:none", "1:none", "0:0"])
}
}
}
fn capture_camera_with_inputs(timeout_ms: u64, inputs: &[&str]) -> Option<Vec<u8>> {
for &input in inputs {
let args = [
"-hide_banner",
"-loglevel",
"error",
"-nostdin",
"-f",
"avfoundation",
"-framerate",
"30",
"-i",
input,
"-frames:v",
"1",
"-f",
"rawvideo",
"-pix_fmt",
"gray",
"pipe:1",
];
if let Some(frame) = run_command_raw_timeout("ffmpeg", &args, timeout_ms)
&& !frame.is_empty()
{
return Some(frame);
}
}
None
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
#[inline(always)]
pub fn read_cntvct() -> u64 {
let val: u64;
unsafe {
std::arch::asm!("mrs {}, cntvct_el0", out(reg) val, options(nostack, nomem));
}
val
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
#[inline(always)]
pub fn read_cntvct() -> u64 {
0
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn probe_jit_instruction_safe(mrs_instr: u32) -> bool {
let pid = unsafe { libc::fork() };
if pid < 0 {
return false; }
if pid == 0 {
let page = unsafe {
libc::mmap(
std::ptr::null_mut(),
4096,
libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | 0x0800, -1,
0,
)
};
if page == libc::MAP_FAILED {
unsafe { libc::_exit(1) };
}
unsafe {
libc::pthread_jit_write_protect_np(0);
let code = page as *mut u32;
code.write(mrs_instr);
code.add(1).write(0xD65F03C0u32); libc::pthread_jit_write_protect_np(1);
core::arch::asm!("dc cvau, {p}", "ic ivau, {p}", p = in(reg) page, options(nostack));
core::arch::asm!("dsb ish", "isb", options(nostack));
}
type FnPtr = unsafe extern "C" fn() -> u64;
let fn_ptr: FnPtr = unsafe { std::mem::transmute(page) };
let _val = unsafe { fn_ptr() };
unsafe {
libc::munmap(page, 4096);
libc::_exit(0); }
}
let mut status: libc::c_int = 0;
let ret = unsafe { libc::waitpid(pid, &mut status, 0) };
if ret < 0 {
return false;
}
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0
}
#[inline]
pub fn xor_fold_u64(v: u64) -> u8 {
let b = v.to_le_bytes();
b[0] ^ b[1] ^ b[2] ^ b[3] ^ b[4] ^ b[5] ^ b[6] ^ b[7]
}
pub fn extract_timing_entropy(timings: &[u64], n_samples: usize) -> Vec<u8> {
if timings.len() < 2 {
return Vec::new();
}
let deltas: Vec<u64> = timings
.windows(2)
.map(|w| w[1].wrapping_sub(w[0]))
.collect();
let xored: Vec<u64> = deltas.windows(2).map(|w| w[0] ^ w[1]).collect();
let mut raw: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
raw.truncate(n_samples);
raw
}
pub fn pack_nibbles(nibbles: impl Iterator<Item = u8>, max_bytes: usize) -> Vec<u8> {
let mut output = Vec::with_capacity(max_bytes);
let mut buf: u8 = 0;
let mut count: u8 = 0;
for nibble in nibbles {
if count == 0 {
buf = nibble << 4;
count = 1;
} else {
buf |= nibble;
output.push(buf);
count = 0;
if output.len() >= max_bytes {
break;
}
}
}
if count == 1 && output.len() < max_bytes {
output.push(buf);
}
output.truncate(max_bytes);
output
}
pub fn extract_delta_bytes_i64(deltas: &[i64], n_samples: usize) -> Vec<u8> {
let xor_deltas: Vec<i64> = if deltas.len() >= 2 {
deltas.windows(2).map(|w| w[0] ^ w[1]).collect()
} else {
Vec::new()
};
let mut entropy = Vec::with_capacity(n_samples);
for d in deltas {
for &b in &d.to_le_bytes() {
entropy.push(b);
}
if entropy.len() >= n_samples {
entropy.truncate(n_samples);
return entropy;
}
}
for d in &xor_deltas {
for &b in &d.to_le_bytes() {
entropy.push(b);
}
if entropy.len() >= n_samples {
break;
}
}
entropy.truncate(n_samples);
entropy
}
#[cfg(target_os = "macos")]
pub fn extract_timing_entropy_debiased(timings: &[u64], n_samples: usize) -> Vec<u8> {
if timings.len() < 4 {
return Vec::new();
}
let deltas: Vec<u64> = timings
.windows(2)
.map(|w| w[1].wrapping_sub(w[0]))
.collect();
let mut debiased_bits: Vec<u8> = Vec::with_capacity(deltas.len() / 2);
for pair in deltas.chunks_exact(2) {
if pair[0] != pair[1] {
debiased_bits.push(if pair[0] < pair[1] { 1 } else { 0 });
}
}
let mut bytes = Vec::with_capacity(n_samples);
for chunk in debiased_bits.chunks(8) {
if chunk.len() < 8 {
break;
}
let mut byte = 0u8;
for (i, &bit) in chunk.iter().enumerate() {
byte |= bit << (7 - i);
}
bytes.push(byte);
if bytes.len() >= n_samples {
break;
}
}
bytes.truncate(n_samples);
bytes
}
pub fn extract_timing_entropy_variance(timings: &[u64], n_samples: usize) -> Vec<u8> {
if timings.len() < 4 {
return Vec::new();
}
let deltas: Vec<u64> = timings
.windows(2)
.map(|w| w[1].wrapping_sub(w[0]))
.collect();
let variance: Vec<u64> = deltas.windows(2).map(|w| w[1].wrapping_sub(w[0])).collect();
let xored: Vec<u64> = variance.windows(2).map(|w| w[0] ^ w[1]).collect();
let mut raw: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
raw.truncate(n_samples);
raw
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_lsbs_u64_basic() {
let deltas = vec![2, 3, 4, 5, 6, 7, 8, 9];
let bytes = extract_lsbs_u64(&deltas);
assert_eq!(bytes.len(), 1);
assert_eq!(bytes[0], 0b01010101);
}
#[test]
fn extract_lsbs_i64_basic() {
let deltas = vec![2i64, 3, 4, 5, 6, 7, 8, 9];
let bytes = extract_lsbs_i64(&deltas);
assert_eq!(bytes.len(), 1);
assert_eq!(bytes[0], 0b01010101);
}
#[test]
fn extract_lsbs_u64_empty() {
let bytes = extract_lsbs_u64(&[]);
assert!(bytes.is_empty());
}
#[test]
fn extract_lsbs_i64_empty() {
let bytes = extract_lsbs_i64(&[]);
assert!(bytes.is_empty());
}
#[test]
fn extract_lsbs_u64_all_odd() {
let deltas = vec![1u64, 3, 5, 7, 9, 11, 13, 15];
let bytes = extract_lsbs_u64(&deltas);
assert_eq!(bytes[0], 0xFF);
}
#[test]
fn extract_lsbs_u64_all_even() {
let deltas = vec![0u64, 2, 4, 6, 8, 10, 12, 14];
let bytes = extract_lsbs_u64(&deltas);
assert_eq!(bytes[0], 0x00);
}
#[test]
fn extract_lsbs_partial_byte() {
let deltas = vec![1u64, 0, 1, 0, 1];
let bytes = extract_lsbs_u64(&deltas);
assert!(bytes.is_empty());
}
#[test]
fn extract_lsbs_u64_i64_agree() {
let u_deltas = vec![1u64, 2, 3, 4, 5, 6, 7, 8];
let i_deltas = vec![1i64, 2, 3, 4, 5, 6, 7, 8];
assert_eq!(extract_lsbs_u64(&u_deltas), extract_lsbs_i64(&i_deltas));
}
#[test]
fn pack_bits_empty() {
let bits: Vec<u8> = vec![];
let bytes = pack_bits_into_bytes(&bits);
assert!(bytes.is_empty());
}
#[test]
fn pack_bits_full_byte() {
let bits = vec![1, 0, 1, 0, 1, 0, 1, 0];
let bytes = pack_bits_into_bytes(&bits);
assert_eq!(bytes, vec![0b10101010]);
}
#[test]
fn mach_time_is_monotonic() {
let t1 = mach_time();
let t2 = mach_time();
assert!(t2 >= t1);
}
#[test]
fn pack_nibbles_basic() {
let nibbles = vec![0x0A_u8, 0x0B];
let bytes = pack_nibbles(nibbles.into_iter(), 10);
assert_eq!(bytes.len(), 1);
assert_eq!(bytes[0], 0xAB);
}
#[test]
fn pack_nibbles_empty() {
let bytes = pack_nibbles(std::iter::empty(), 10);
assert!(bytes.is_empty());
}
#[test]
fn pack_nibbles_odd_count() {
let nibbles = vec![0x0C_u8, 0x0D, 0x0E];
let bytes = pack_nibbles(nibbles.into_iter(), 10);
assert_eq!(bytes.len(), 2);
assert_eq!(bytes[0], 0xCD);
assert_eq!(bytes[1], 0xE0); }
#[test]
fn pack_nibbles_respects_max() {
let nibbles = vec![0x01_u8, 0x02, 0x03, 0x04, 0x05, 0x06];
let bytes = pack_nibbles(nibbles.into_iter(), 2);
assert_eq!(bytes.len(), 2);
}
#[test]
fn extract_delta_bytes_empty() {
let bytes = extract_delta_bytes_i64(&[], 10);
assert!(bytes.is_empty());
}
#[test]
fn extract_delta_bytes_single_delta() {
let deltas = vec![0x0102030405060708i64];
let bytes = extract_delta_bytes_i64(&deltas, 8);
assert_eq!(bytes, 0x0102030405060708i64.to_le_bytes().to_vec());
}
#[test]
fn extract_delta_bytes_truncated() {
let deltas = vec![0x0102030405060708i64];
let bytes = extract_delta_bytes_i64(&deltas, 4);
assert_eq!(bytes.len(), 4);
assert_eq!(bytes, &0x0102030405060708i64.to_le_bytes()[..4]);
}
#[test]
fn extract_delta_bytes_with_xor_mixing() {
let deltas = vec![100i64, 200];
let bytes = extract_delta_bytes_i64(&deltas, 24);
assert_eq!(bytes.len(), 24);
}
#[test]
fn extract_delta_bytes_respects_n_samples() {
let deltas: Vec<i64> = (1..=100).collect();
let bytes = extract_delta_bytes_i64(&deltas, 50);
assert_eq!(bytes.len(), 50);
}
#[test]
fn xor_fold_u64_zero() {
assert_eq!(xor_fold_u64(0), 0);
}
#[test]
fn xor_fold_u64_identical_bytes() {
assert_eq!(xor_fold_u64(0x0101010101010101), 0);
}
#[test]
fn xor_fold_u64_single_byte() {
assert_eq!(xor_fold_u64(0xFF), 0xFF);
}
#[test]
fn xor_fold_u64_two_bytes() {
assert_eq!(xor_fold_u64(0xBB_00_00_00_00_00_00_AA), 0xAA ^ 0xBB);
}
#[test]
fn xor_fold_u64_max() {
assert_eq!(xor_fold_u64(u64::MAX), 0);
}
#[test]
fn extract_timing_entropy_basic() {
let timings = vec![100, 110, 105, 120, 108, 130, 112, 125];
let result = extract_timing_entropy(&timings, 4);
assert!(!result.is_empty());
assert!(result.len() <= 4);
}
#[test]
fn extract_timing_entropy_too_few_samples() {
assert!(extract_timing_entropy(&[], 10).is_empty());
assert!(extract_timing_entropy(&[42], 10).is_empty());
}
#[test]
fn extract_timing_entropy_exactly_two_timings() {
let result = extract_timing_entropy(&[100, 200], 10);
assert!(result.is_empty());
}
#[test]
fn extract_timing_entropy_exactly_three_timings() {
let result = extract_timing_entropy(&[100, 200, 150], 10);
assert_eq!(result.len(), 1);
}
#[test]
fn extract_timing_entropy_truncates_to_n_samples() {
let timings: Vec<u64> = (0..100).collect();
let result = extract_timing_entropy(&timings, 5);
assert_eq!(result.len(), 5);
}
#[test]
fn extract_timing_entropy_constant_timings() {
let timings = vec![42u64; 20];
let result = extract_timing_entropy(&timings, 10);
assert!(result.iter().all(|&b| b == 0));
}
#[test]
fn run_command_echo() {
let out = run_command("echo", &["hello"]);
assert!(out.is_some());
assert_eq!(out.unwrap().trim(), "hello");
}
#[test]
fn run_command_nonexistent() {
let out = run_command("/nonexistent/binary", &[]);
assert!(out.is_none());
}
#[test]
fn run_command_raw_echo() {
let out = run_command_raw("echo", &["bytes"]);
assert!(out.is_some());
assert!(out.unwrap().starts_with(b"bytes"));
}
#[test]
fn run_command_failing_status() {
let out = run_command("false", &[]);
assert!(out.is_none());
}
#[test]
fn run_command_raw_failing_status() {
let out = run_command_raw("false", &[]);
assert!(out.is_none());
}
#[test]
fn run_command_empty_output() {
let out = run_command("true", &[]);
assert!(out.is_some());
assert!(out.unwrap().is_empty());
}
#[test]
fn command_exists_true() {
assert!(command_exists("echo"));
}
#[test]
fn command_exists_false() {
assert!(!command_exists("nonexistent_binary_xyz_12345"));
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
#[test]
fn debiased_extraction_basic() {
let timings: Vec<u64> = (0..200).map(|i| 100 + (i * 7 + i * i) % 50).collect();
let result = extract_timing_entropy_debiased(&timings, 10);
assert!(result.len() <= 10);
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
#[test]
fn debiased_extraction_too_few() {
assert!(extract_timing_entropy_debiased(&[1, 2, 3], 10).is_empty());
assert!(extract_timing_entropy_debiased(&[], 10).is_empty());
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
#[test]
fn debiased_extraction_constant_input() {
let timings = vec![42u64; 100];
let result = extract_timing_entropy_debiased(&timings, 10);
assert!(result.is_empty());
}
#[test]
fn variance_extraction_basic() {
let timings: Vec<u64> = (0..100).map(|i| 100 + (i * 7 + i * i) % 50).collect();
let result = extract_timing_entropy_variance(&timings, 10);
assert!(!result.is_empty());
assert!(result.len() <= 10);
}
#[test]
fn variance_extraction_too_few() {
assert!(extract_timing_entropy_variance(&[1, 2, 3], 10).is_empty());
}
}