Skip to main content

openentropy_core/sources/thermal/
counter_beat.rs

1//! Two-oscillator beat frequency — CPU counter vs audio PLL crystal.
2//!
3//! Measures the phase difference between two physically independent oscillators:
4//! - **CNTVCT_EL0**: ARM generic timer counter, driven by the CPU's 24 MHz crystal
5//! - **Audio PLL**: the audio subsystem's independent crystal oscillator, probed
6//!   via CoreAudio property queries that force clock domain crossings
7//!
8//! The entropy arises from independent thermal noise (Johnson-Nyquist) in each
9//! crystal oscillator's sustaining circuit, causing uncorrelated phase drift
10//! between the two clock domains. This two-oscillator beat technique is used in
11//! some hardware random number generators for anomaly detection research (note:
12//! the original PEAR lab REGs used noise diodes, not oscillator beats).
13//!
14//! ## Mechanism
15//!
16//! Each sample reads CNTVCT_EL0 immediately before and after a CoreAudio property
17//! query (actual sample rate, latency) that forces synchronization with the audio
18//! PLL clock domain. The query duration in raw counter ticks is modulated by the
19//! instantaneous phase relationship between the CPU crystal and the audio PLL.
20//! XORing the raw counter value with this PLL-modulated duration produces a beat
21//! that encodes the phase difference between the two independent oscillators.
22//!
23//! ## Why this matters for anomaly detection research
24//!
25//! - **Clean physical signal**: thermal noise in crystal oscillators is a genuine
26//!   physical noise source independent of software state
27//! - **High sample rate**: thousands of phase-difference samples per second
28//! - **Well-characterized physics**: crystal oscillator phase noise (Allan variance,
29//!   flicker FM, white PM) is thoroughly documented in metrology literature
30//! - **Low min-entropy is a feature**: a source with H∞ ~1–3 bits/byte is easier
31//!   to detect statistical anomalies in than one at 7.9 — useful for anomaly
32//!   detection experiments
33//!
34//! ## Previous version
35//!
36//! An earlier `counter_beat` was removed because it XOR'd CNTVCT_EL0 with
37//! `mach_absolute_time()` — which on Apple Silicon is the *same* counter, not an
38//! independent oscillator. This version fixes that by using the audio PLL as the
39//! genuinely independent second clock domain, validated by `audio_pll_timing`'s
40//! confirmation that CoreAudio queries cross an independent PLL clock domain.
41
42use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
43#[cfg(target_os = "macos")]
44use crate::sources::helpers::read_cntvct;
45#[cfg(target_os = "macos")]
46use crate::sources::helpers::xor_fold_u64;
47
48static COUNTER_BEAT_INFO: SourceInfo = SourceInfo {
49    name: "counter_beat",
50    description: "Two-oscillator beat frequency: CPU counter (CNTVCT_EL0) vs audio PLL crystal",
51    physics: "Reads the ARM generic timer counter (CNTVCT_EL0, driven by a 24 MHz crystal) \
52              immediately before and after a CoreAudio property query that forces \
53              synchronization with the audio PLL clock domain. The query duration in raw \
54              counter ticks is modulated by the instantaneous phase relationship between \
55              the CPU crystal and the independent audio PLL crystal. XORing the counter \
56              value with this PLL-modulated duration produces a two-oscillator beat that \
57              encodes the phase difference between two independent oscillators. \
58              Entropy arises from independent \
59              Johnson-Nyquist thermal noise in each crystal's sustaining amplifier. \
60              The raw physical signal is preserved for statistical analysis.",
61    category: SourceCategory::Thermal,
62    platform: Platform::MacOS,
63    requirements: &[Requirement::AppleSilicon, Requirement::AudioUnit],
64    entropy_rate_estimate: 3.0,
65    composite: false,
66    is_fast: false,
67};
68
69/// Two-oscillator beat frequency entropy source.
70///
71/// Captures the instantaneous phase difference between the CPU's ARM counter
72/// and the audio PLL clock — two physically independent crystal oscillators
73/// with uncorrelated thermal noise.
74pub struct CounterBeatSource;
75
76impl EntropySource for CounterBeatSource {
77    fn info(&self) -> &SourceInfo {
78        &COUNTER_BEAT_INFO
79    }
80
81    fn is_available(&self) -> bool {
82        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
83        {
84            super::coreaudio_ffi::get_default_output_device() != 0
85        }
86        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
87        {
88            false
89        }
90    }
91
92    fn collect(&self, n_samples: usize) -> Vec<u8> {
93        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
94        {
95            let _ = n_samples;
96            Vec::new()
97        }
98
99        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
100        {
101            use super::coreaudio_ffi;
102
103            let device = coreaudio_ffi::get_default_output_device();
104            if device == 0 {
105                return Vec::new();
106            }
107
108            // Cycle through different audio property queries to exercise
109            // different code paths crossing the PLL clock domain boundary.
110            let selectors = [
111                (
112                    coreaudio_ffi::AUDIO_DEVICE_PROPERTY_ACTUAL_SAMPLE_RATE,
113                    coreaudio_ffi::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
114                ),
115                (
116                    coreaudio_ffi::AUDIO_DEVICE_PROPERTY_LATENCY,
117                    coreaudio_ffi::AUDIO_DEVICE_PROPERTY_SCOPE_OUTPUT,
118                ),
119                (
120                    coreaudio_ffi::AUDIO_DEVICE_PROPERTY_NOMINAL_SAMPLE_RATE,
121                    coreaudio_ffi::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
122                ),
123            ];
124
125            // Over-collect: delta + XOR + fold reduces count.
126            let raw_count = n_samples * 4 + 64;
127            let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
128
129            for i in 0..raw_count {
130                let (sel, scope) = selectors[i % selectors.len()];
131
132                // Read CNTVCT_EL0 immediately before the audio PLL crossing.
133                let counter_before = read_cntvct();
134
135                // Force a clock domain crossing into the audio PLL.
136                coreaudio_ffi::query_device_property_timed(device, sel, scope);
137
138                // Read CNTVCT_EL0 immediately after.
139                let counter_after = read_cntvct();
140
141                // The beat: XOR the raw counter value (CPU oscillator phase)
142                // with the PLL-modulated duration (audio oscillator phase).
143                // The duration encodes how long the CPU had to wait for the
144                // audio PLL to respond — modulated by the instantaneous phase
145                // relationship between the two independent crystals.
146                let pll_duration = counter_after.wrapping_sub(counter_before);
147                let beat = counter_before ^ pll_duration;
148                beats.push(beat);
149            }
150
151            if beats.len() < 4 {
152                return Vec::new();
153            }
154
155            // Extract entropy: consecutive beat differences capture the phase
156            // drift rate, then XOR adjacent deltas and fold to bytes.
157            let deltas: Vec<u64> = beats.windows(2).map(|w| w[1].wrapping_sub(w[0])).collect();
158
159            let xored: Vec<u64> = deltas.windows(2).map(|w| w[0] ^ w[1]).collect();
160
161            let mut output: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
162            output.truncate(n_samples);
163            output
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn info() {
174        let src = CounterBeatSource;
175        assert_eq!(src.name(), "counter_beat");
176        assert_eq!(src.info().category, SourceCategory::Thermal);
177        assert!(!src.info().composite);
178    }
179
180    #[test]
181    fn physics_mentions_two_oscillators() {
182        let src = CounterBeatSource;
183        assert!(src.info().physics.contains("CNTVCT_EL0"));
184        assert!(src.info().physics.contains("two-oscillator"));
185        assert!(src.info().physics.contains("phase difference"));
186    }
187
188    #[test]
189    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
190    fn cntvct_is_nonzero() {
191        let v = read_cntvct();
192        assert!(v > 0);
193    }
194
195    #[test]
196    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
197    #[ignore] // Requires audio hardware
198    fn collects_bytes() {
199        let src = CounterBeatSource;
200        if src.is_available() {
201            let data = src.collect(64);
202            assert!(!data.is_empty());
203            assert!(data.len() <= 64);
204        }
205    }
206}