Skip to main content

openentropy_core/sources/
bluetooth.rs

1//! BluetoothNoiseSource — BLE RSSI scanning via system_profiler.
2//!
3//! Runs `system_profiler SPBluetoothDataType` with a timeout to enumerate nearby
4//! Bluetooth devices, parses RSSI values, and extracts LSBs combined with timing
5//! jitter. Falls back to timing-only entropy if the command hangs or times out.
6//!
7//! **Raw output characteristics:** Mix of RSSI LSBs and timing bytes.
8
9use std::process::Command;
10use std::time::{Duration, Instant};
11
12use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
13
14/// Path to system_profiler on macOS.
15const SYSTEM_PROFILER_PATH: &str = "/usr/sbin/system_profiler";
16
17/// Timeout for system_profiler command.
18const BT_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
19
20static BLUETOOTH_NOISE_INFO: SourceInfo = SourceInfo {
21    name: "bluetooth_noise",
22    description: "BLE RSSI values and scanning timing jitter",
23    physics: "Scans BLE advertisements via CoreBluetooth and collects RSSI values from \
24              nearby devices. Each RSSI reading reflects: 2.4 GHz multipath propagation, \
25              frequency hopping across 40 channels, advertising interval jitter (\u{00b1}10ms), \
26              transmit power variation, and receiver thermal noise.",
27    category: SourceCategory::Sensor,
28    platform: Platform::MacOS,
29    requirements: &[Requirement::Bluetooth],
30    entropy_rate_estimate: 50.0,
31    composite: false,
32};
33
34/// Entropy source that harvests randomness from Bluetooth RSSI and timing jitter.
35pub struct BluetoothNoiseSource;
36
37/// Parse RSSI values from system_profiler SPBluetoothDataType output.
38fn parse_rssi_values(output: &str) -> Vec<i32> {
39    let mut rssi_values = Vec::new();
40    for line in output.lines() {
41        let trimmed = line.trim();
42        let lower = trimmed.to_lowercase();
43        if lower.contains("rssi") {
44            for token in trimmed.split(&[':', '=', ' '][..]) {
45                let clean = token.trim();
46                if let Ok(v) = clean.parse::<i32>() {
47                    rssi_values.push(v);
48                }
49            }
50        }
51    }
52    rssi_values
53}
54
55/// Run system_profiler with a timeout, returning (output_option, elapsed_ns).
56fn get_bluetooth_info_timed() -> (Option<String>, u64) {
57    let t0 = Instant::now();
58
59    let child = Command::new(SYSTEM_PROFILER_PATH)
60        .arg("SPBluetoothDataType")
61        .stdout(std::process::Stdio::piped())
62        .stderr(std::process::Stdio::null())
63        .spawn();
64
65    let mut child = match child {
66        Ok(c) => c,
67        Err(_) => return (None, t0.elapsed().as_nanos() as u64),
68    };
69
70    let deadline = Instant::now() + BT_COMMAND_TIMEOUT;
71    loop {
72        match child.try_wait() {
73            Ok(Some(status)) => {
74                let elapsed = t0.elapsed().as_nanos() as u64;
75                if !status.success() {
76                    return (None, elapsed);
77                }
78                let stdout = child
79                    .wait_with_output()
80                    .ok()
81                    .map(|o| String::from_utf8_lossy(&o.stdout).to_string());
82                return (stdout, elapsed);
83            }
84            Ok(None) => {
85                if Instant::now() >= deadline {
86                    let _ = child.kill();
87                    let _ = child.wait();
88                    return (None, t0.elapsed().as_nanos() as u64);
89                }
90                std::thread::sleep(Duration::from_millis(50));
91            }
92            Err(_) => return (None, t0.elapsed().as_nanos() as u64),
93        }
94    }
95}
96
97impl EntropySource for BluetoothNoiseSource {
98    fn info(&self) -> &SourceInfo {
99        &BLUETOOTH_NOISE_INFO
100    }
101
102    fn is_available(&self) -> bool {
103        std::path::Path::new(SYSTEM_PROFILER_PATH).exists()
104    }
105
106    fn collect(&self, n_samples: usize) -> Vec<u8> {
107        let mut raw = Vec::with_capacity(n_samples);
108
109        let time_budget = Duration::from_secs(10);
110        let start = Instant::now();
111        let max_scans = 50;
112
113        for _ in 0..max_scans {
114            if start.elapsed() >= time_budget || raw.len() >= n_samples {
115                break;
116            }
117
118            let (bt_info, elapsed_ns) = get_bluetooth_info_timed();
119
120            // Extract timing bytes (raw nanosecond LSBs)
121            for shift in (0..64).step_by(8) {
122                raw.push((elapsed_ns >> shift) as u8);
123            }
124
125            // Parse RSSI values — raw LSBs
126            if let Some(info) = bt_info {
127                let rssi_values = parse_rssi_values(&info);
128                for rssi in &rssi_values {
129                    raw.push((*rssi & 0xFF) as u8);
130                }
131                raw.push(info.len() as u8);
132                raw.push((info.len() >> 8) as u8);
133            }
134        }
135
136        raw.truncate(n_samples);
137        raw
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn bluetooth_noise_info() {
147        let src = BluetoothNoiseSource;
148        assert_eq!(src.name(), "bluetooth_noise");
149        assert_eq!(src.info().category, SourceCategory::Sensor);
150    }
151
152    #[test]
153    fn parse_rssi_values_works() {
154        let sample = r#"
155            Connected: Yes
156            RSSI: -45
157            Some Device:
158              RSSI: -72
159              Name: Test
160        "#;
161        let values = parse_rssi_values(sample);
162        assert_eq!(values, vec![-45, -72]);
163    }
164
165    #[test]
166    fn parse_rssi_empty() {
167        let sample = "No bluetooth data here";
168        let values = parse_rssi_values(sample);
169        assert!(values.is_empty());
170    }
171
172    #[test]
173    fn bluetooth_composite_flag() {
174        let src = BluetoothNoiseSource;
175        assert!(!src.info().composite);
176    }
177
178    #[test]
179    #[cfg(target_os = "macos")]
180    #[ignore] // Requires Bluetooth hardware
181    fn bluetooth_noise_collects_bytes() {
182        let src = BluetoothNoiseSource;
183        if src.is_available() {
184            let data = src.collect(32);
185            assert!(!data.is_empty());
186            assert!(data.len() <= 32);
187        }
188    }
189}