openentropy-core 0.12.3

Core entropy harvesting library — hardware noise sources, raw or SHA-256 conditioned
Documentation
//! Two-oscillator beat frequency — CPU counter vs audio PLL crystal.
//!
//! Measures the phase difference between two physically independent oscillators:
//! - **CNTVCT_EL0**: ARM generic timer counter, driven by the CPU's 24 MHz crystal
//! - **Audio PLL**: the audio subsystem's independent crystal oscillator, probed
//!   via CoreAudio property queries that force clock domain crossings
//!
//! The entropy arises from independent thermal noise (Johnson-Nyquist) in each
//! crystal oscillator's sustaining circuit, causing uncorrelated phase drift
//! between the two clock domains. This two-oscillator beat technique is used in
//! some hardware random number generators for anomaly detection research (note:
//! the original PEAR lab REGs used noise diodes, not oscillator beats).
//!
//! ## Mechanism
//!
//! Each sample reads CNTVCT_EL0 immediately before and after a CoreAudio property
//! query (actual sample rate, latency) that forces synchronization with the audio
//! PLL clock domain. The query duration in raw counter ticks is modulated by the
//! instantaneous phase relationship between the CPU crystal and the audio PLL.
//! XORing the raw counter value with this PLL-modulated duration produces a beat
//! that encodes the phase difference between the two independent oscillators.
//!
//! ## Why this matters for anomaly detection research
//!
//! - **Clean physical signal**: thermal noise in crystal oscillators is a genuine
//!   physical noise source independent of software state
//! - **High sample rate**: thousands of phase-difference samples per second
//! - **Well-characterized physics**: crystal oscillator phase noise (Allan variance,
//!   flicker FM, white PM) is thoroughly documented in metrology literature
//! - **Low min-entropy is a feature**: a source with H∞ ~1–3 bits/byte is easier
//!   to detect statistical anomalies in than one at 7.9 — useful for anomaly
//!   detection experiments
//!
//! ## Previous version
//!
//! An earlier `counter_beat` was removed because it XOR'd CNTVCT_EL0 with
//! `mach_absolute_time()` — which on Apple Silicon is the *same* counter, not an
//! independent oscillator. This version fixes that by using the audio PLL as the
//! genuinely independent second clock domain, validated by `audio_pll_timing`'s
//! confirmation that CoreAudio queries cross an independent PLL clock domain.

use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
#[cfg(target_os = "macos")]
use crate::sources::helpers::read_cntvct;
#[cfg(target_os = "macos")]
use crate::sources::helpers::xor_fold_u64;

static COUNTER_BEAT_INFO: SourceInfo = SourceInfo {
    name: "counter_beat",
    description: "Two-oscillator beat frequency: CPU counter (CNTVCT_EL0) vs audio PLL crystal",
    physics: "Reads the ARM generic timer counter (CNTVCT_EL0, driven by a 24 MHz crystal) \
              immediately before and after a CoreAudio property query that forces \
              synchronization with the audio PLL clock domain. The query duration in raw \
              counter ticks is modulated by the instantaneous phase relationship between \
              the CPU crystal and the independent audio PLL crystal. XORing the counter \
              value with this PLL-modulated duration produces a two-oscillator beat that \
              encodes the phase difference between two independent oscillators. \
              Entropy arises from independent \
              Johnson-Nyquist thermal noise in each crystal's sustaining amplifier. \
              The raw physical signal is preserved for statistical analysis.",
    category: SourceCategory::Thermal,
    platform: Platform::MacOS,
    requirements: &[Requirement::AppleSilicon, Requirement::AudioUnit],
    entropy_rate_estimate: 3.0,
    composite: false,
    is_fast: false,
};

/// Two-oscillator beat frequency entropy source.
///
/// Captures the instantaneous phase difference between the CPU's ARM counter
/// and the audio PLL clock — two physically independent crystal oscillators
/// with uncorrelated thermal noise.
pub struct CounterBeatSource;

impl EntropySource for CounterBeatSource {
    fn info(&self) -> &SourceInfo {
        &COUNTER_BEAT_INFO
    }

    fn is_available(&self) -> bool {
        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
        {
            super::coreaudio_ffi::get_default_output_device() != 0
        }
        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
        {
            false
        }
    }

    fn collect(&self, n_samples: usize) -> Vec<u8> {
        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
        {
            let _ = n_samples;
            Vec::new()
        }

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

            let device = coreaudio_ffi::get_default_output_device();
            if device == 0 {
                return Vec::new();
            }

            // Cycle through different audio property queries to exercise
            // different code paths crossing the PLL clock domain boundary.
            let selectors = [
                (
                    coreaudio_ffi::AUDIO_DEVICE_PROPERTY_ACTUAL_SAMPLE_RATE,
                    coreaudio_ffi::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
                ),
                (
                    coreaudio_ffi::AUDIO_DEVICE_PROPERTY_LATENCY,
                    coreaudio_ffi::AUDIO_DEVICE_PROPERTY_SCOPE_OUTPUT,
                ),
                (
                    coreaudio_ffi::AUDIO_DEVICE_PROPERTY_NOMINAL_SAMPLE_RATE,
                    coreaudio_ffi::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
                ),
            ];

            // Over-collect: delta + XOR + fold reduces count.
            let raw_count = n_samples * 4 + 64;
            let mut beats: Vec<u64> = Vec::with_capacity(raw_count);

            for i in 0..raw_count {
                let (sel, scope) = selectors[i % selectors.len()];

                // Read CNTVCT_EL0 immediately before the audio PLL crossing.
                let counter_before = read_cntvct();

                // Force a clock domain crossing into the audio PLL.
                coreaudio_ffi::query_device_property_timed(device, sel, scope);

                // Read CNTVCT_EL0 immediately after.
                let counter_after = read_cntvct();

                // The beat: XOR the raw counter value (CPU oscillator phase)
                // with the PLL-modulated duration (audio oscillator phase).
                // The duration encodes how long the CPU had to wait for the
                // audio PLL to respond — modulated by the instantaneous phase
                // relationship between the two independent crystals.
                let pll_duration = counter_after.wrapping_sub(counter_before);
                let beat = counter_before ^ pll_duration;
                beats.push(beat);
            }

            if beats.len() < 4 {
                return Vec::new();
            }

            // Extract entropy: consecutive beat differences capture the phase
            // drift rate, then XOR adjacent deltas and fold to bytes.
            let deltas: Vec<u64> = beats.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 output: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
            output.truncate(n_samples);
            output
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn info() {
        let src = CounterBeatSource;
        assert_eq!(src.name(), "counter_beat");
        assert_eq!(src.info().category, SourceCategory::Thermal);
        assert!(!src.info().composite);
    }

    #[test]
    fn physics_mentions_two_oscillators() {
        let src = CounterBeatSource;
        assert!(src.info().physics.contains("CNTVCT_EL0"));
        assert!(src.info().physics.contains("two-oscillator"));
        assert!(src.info().physics.contains("phase difference"));
    }

    #[test]
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    fn cntvct_is_nonzero() {
        let v = read_cntvct();
        assert!(v > 0);
    }

    #[test]
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    #[ignore] // Requires audio hardware
    fn collects_bytes() {
        let src = CounterBeatSource;
        if src.is_available() {
            let data = src.collect(64);
            assert!(!data.is_empty());
            assert!(data.len() <= 64);
        }
    }
}