openentropy-core 0.12.3

Core entropy harvesting library — hardware noise sources, raw or SHA-256 conditioned
Documentation
//! Memory bus AES-XTS crypto context switching timing entropy.
//!
//! Apple Silicon encrypts all DRAM accesses using AES-XTS with per-page keys
//! managed by the memory controller in hardware. This is the "memory tagging"
//! component of Apple's Data Protection and Pointer Authentication systems.
//! When the CPU accesses pages from different security contexts in rapid
//! succession, the memory controller must swap AES-XTS key contexts — a
//! process with timing variance rooted in the crypto engine's internal state.
//!
//! ## Physics
//!
//! The Apple Silicon memory controller implements inline AES-XTS encryption
//! transparently. Each physical page has an associated encryption key tweak
//! derived from its physical address and protection domain. Switching between
//! pages that belong to different protection domains requires:
//!
//! 1. A cache flush (DC CIVAC) to push dirty data to main memory
//! 2. A data synchronisation barrier (DSB SY) to ensure ordering
//! 3. A cross-page load that causes the memory controller to activate
//!    a different AES key context
//!
//! The latency of step 3 varies with the AES engine's pipeline state,
//! outstanding transaction queue depth, and DRAM refresh timing. Empirically
//! this produces a coefficient of variation >270% — far beyond what software
//! scheduling alone can explain. The source needs Von Neumann debiasing (the
//! raw LSBs are biased toward 0 from cache-hit fast-paths), which
//! `extract_timing_entropy` handles via delta extraction.
//!
//! ## Why this is not documented
//!
//! Apple does not publicly document the inline memory encryption architecture
//! of Apple Silicon. The AES-XTS key-context switching overhead is an
//! implementation detail of the memory controller that falls below the level
//! of any published hardware specification.

use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::sources::helpers::{extract_timing_entropy, mach_time};
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use std::ptr;

static MEMORY_BUS_CRYPTO_INFO: SourceInfo = SourceInfo {
    name: "memory_bus_crypto",
    description: "AES-XTS crypto context switching timing from cross-page cache flush cycles",
    physics: "Apple Silicon encrypts all DRAM accesses via inline AES-XTS with per-page keys. \
              Flushing a cache line (DC CIVAC + DSB SY) then accessing a page from a \
              different virtual address region forces the memory controller to switch \
              AES-XTS key contexts. The key-context switch latency varies with the AES \
              engine pipeline state, DRAM transaction queue depth, and refresh timing. \
              Measured CV >270%, consistent with crypto engine state variation rather \
              than scheduler noise. Raw LSBs are biased (cache fast-path dominates); \
              delta extraction removes the bias while preserving the variance signal.",
    category: SourceCategory::Microarch,
    platform: Platform::MacOS,
    requirements: &[],
    entropy_rate_estimate: 2.0,
    composite: false,
    is_fast: false,
};

/// Apple Silicon page size: 16 KB (not 4 KB like x86).
const APPLE_PAGE_SIZE: usize = 16 * 1024;

/// Number of separately mapped regions to rotate through.
/// More regions = more distinct AES key contexts = higher context-switch rate.
const NUM_REGIONS: usize = 8;

/// Size of each mapped region: 16 pages.
const REGION_PAGES: usize = 16;
const REGION_SIZE: usize = APPLE_PAGE_SIZE * REGION_PAGES;

/// Entropy source: memory bus AES-XTS crypto context timing.
pub struct MemoryBusCryptoSource;

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
mod imp {
    use super::*;

    /// RAII guard for mmap regions — ensures munmap on drop (including panic unwind).
    struct MappedRegions(Vec<*mut u8>);

    impl Drop for MappedRegions {
        fn drop(&mut self) {
            for &r in &self.0 {
                unsafe {
                    libc::munmap(r as *mut libc::c_void, REGION_SIZE);
                }
            }
        }
    }

    impl EntropySource for MemoryBusCryptoSource {
        fn info(&self) -> &SourceInfo {
            &MEMORY_BUS_CRYPTO_INFO
        }

        fn is_available(&self) -> bool {
            // Requires Apple Silicon (aarch64 + macOS) for the DC CIVAC
            // cache-flush instruction and the AES-XTS memory controller.
            true
        }

        fn collect(&self, n_samples: usize) -> Vec<u8> {
            // Heavily oversample: the raw CV is high but the debiased
            // extraction is conservative. 20x oversampling is safe.
            let raw_count = n_samples * 20 + 128;

            // Map NUM_REGIONS separate anonymous regions.
            // Each is in a distinct virtual address range, which maximises
            // the probability that they fall in different AES key-tweak domains.
            let mut raw_regions: Vec<*mut u8> = Vec::with_capacity(NUM_REGIONS);

            unsafe {
                for i in 0..NUM_REGIONS {
                    let p = libc::mmap(
                        ptr::null_mut(),
                        REGION_SIZE,
                        libc::PROT_READ | libc::PROT_WRITE,
                        libc::MAP_PRIVATE | libc::MAP_ANON,
                        -1,
                        0,
                    ) as *mut u8;

                    if p == libc::MAP_FAILED as *mut u8 {
                        // Partial allocation: RAII guard will clean up on drop.
                        let _guard = MappedRegions(raw_regions);
                        return Vec::new();
                    }

                    // Touch every page to ensure physical backing.
                    for page in 0..REGION_PAGES {
                        ptr::write_volatile(p.add(page * APPLE_PAGE_SIZE), i as u8);
                    }
                    raw_regions.push(p);
                }
            }

            // RAII guard: munmap on drop, including panic unwind.
            let regions = MappedRegions(raw_regions);

            let mut timings: Vec<u64> = Vec::with_capacity(raw_count);

            // Seed LCG from mach_time to prevent predictable access patterns.
            let mut lcg: u64 = mach_time() | 1;

            unsafe {
                for _ in 0..raw_count {
                    lcg = lcg
                        .wrapping_mul(6364136223846793005)
                        .wrapping_add(1442695040888963407);

                    // Pick two different regions (source and destination).
                    let src_idx = (lcg >> 32) as usize % NUM_REGIONS;
                    let dst_idx =
                        (src_idx + 1 + ((lcg & 0xFFFF) as usize % (NUM_REGIONS - 1))) % NUM_REGIONS;

                    // Pick offsets within each region (cache-line aligned).
                    let src_off = ((lcg >> 16) as usize % (REGION_SIZE / 64)) * 64;
                    let dst_off = (((lcg >> 48) as usize * 97) % (REGION_SIZE / 64)) * 64;

                    let src_ptr = regions.0[src_idx].add(src_off);
                    let dst_ptr = regions.0[dst_idx].add(dst_off);

                    // Write to source to ensure the cache line is dirty.
                    ptr::write_volatile(src_ptr, (lcg & 0xFF) as u8);

                    // Flush source cache line to DRAM (DC CIVAC instruction).
                    // This forces the memory controller to write-back and
                    // invalidate, resetting the AES-XTS pipeline for this line.
                    std::arch::asm!(
                        "dc civac, {addr}",
                        "dsb sy",
                        addr = in(reg) src_ptr,
                        options(nostack, preserves_flags)
                    );

                    // Now access the destination — this forces the memory
                    // controller to activate a different AES-XTS key context.
                    let t0 = mach_time();
                    let _v = ptr::read_volatile(dst_ptr);
                    let elapsed = mach_time().wrapping_sub(t0);

                    timings.push(elapsed);
                }
            }

            // regions dropped here (RAII munmap)
            drop(regions);

            extract_timing_entropy(&timings, n_samples)
        }
    }
}

// Non-Apple-Silicon stub: source reports unavailable.
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
impl EntropySource for MemoryBusCryptoSource {
    fn info(&self) -> &SourceInfo {
        &MEMORY_BUS_CRYPTO_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 = MemoryBusCryptoSource;
        assert_eq!(src.info().name, "memory_bus_crypto");
        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_apple_silicon() {
        assert!(MemoryBusCryptoSource.is_available());
    }

    #[test]
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    #[ignore] // Hardware-dependent timing test
    fn collects_bytes() {
        let src = MemoryBusCryptoSource;
        if !src.is_available() {
            return;
        }
        let data = src.collect(32);
        assert!(!data.is_empty());
        assert!(data.len() <= 32);
    }
}