Skip to main content

openentropy_core/sources/sensor/
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: 1.0,
31    composite: false,
32    is_fast: false,
33};
34
35/// Entropy source that harvests randomness from Bluetooth RSSI and timing jitter.
36pub struct BluetoothNoiseSource;
37
38/// Parse RSSI values from system_profiler SPBluetoothDataType output.
39fn parse_rssi_values(output: &str) -> Vec<i32> {
40    let mut rssi_values = Vec::new();
41    for line in output.lines() {
42        let trimmed = line.trim();
43        let lower = trimmed.to_lowercase();
44        if lower.contains("rssi") {
45            for token in trimmed.split(&[':', '=', ' '][..]) {
46                let clean = token.trim();
47                if let Ok(v) = clean.parse::<i32>() {
48                    rssi_values.push(v);
49                }
50            }
51        }
52    }
53    rssi_values
54}
55
56/// Run system_profiler with a timeout, returning (output_option, elapsed_ns).
57fn get_bluetooth_info_timed() -> (Option<String>, u64) {
58    let t0 = Instant::now();
59
60    let child = Command::new(SYSTEM_PROFILER_PATH)
61        .arg("SPBluetoothDataType")
62        .stdout(std::process::Stdio::piped())
63        .stderr(std::process::Stdio::null())
64        .spawn();
65
66    let mut child = match child {
67        Ok(c) => c,
68        Err(_) => return (None, t0.elapsed().as_nanos() as u64),
69    };
70
71    let deadline = Instant::now() + BT_COMMAND_TIMEOUT;
72    loop {
73        match child.try_wait() {
74            Ok(Some(status)) => {
75                let elapsed = t0.elapsed().as_nanos() as u64;
76                if !status.success() {
77                    return (None, elapsed);
78                }
79                // Child already reaped by try_wait — read stdout directly
80                // (wait_with_output() would call waitpid again, getting empty output).
81                use std::io::Read;
82                let mut stdout_str = String::new();
83                if let Some(mut out) = child.stdout.take() {
84                    let _ = out.read_to_string(&mut stdout_str);
85                }
86                let result = if stdout_str.is_empty() {
87                    None
88                } else {
89                    Some(stdout_str)
90                };
91                return (result, elapsed);
92            }
93            Ok(None) => {
94                if Instant::now() >= deadline {
95                    let _ = child.kill();
96                    let _ = child.wait();
97                    return (None, t0.elapsed().as_nanos() as u64);
98                }
99                std::thread::sleep(Duration::from_millis(50));
100            }
101            Err(_) => return (None, t0.elapsed().as_nanos() as u64),
102        }
103    }
104}
105
106impl EntropySource for BluetoothNoiseSource {
107    fn info(&self) -> &SourceInfo {
108        &BLUETOOTH_NOISE_INFO
109    }
110
111    fn is_available(&self) -> bool {
112        cfg!(target_os = "macos") && std::path::Path::new(SYSTEM_PROFILER_PATH).exists()
113    }
114
115    fn collect(&self, n_samples: usize) -> Vec<u8> {
116        let mut raw = Vec::with_capacity(n_samples);
117
118        let time_budget = Duration::from_secs(4);
119        let start = Instant::now();
120        let max_scans = 50;
121
122        for _ in 0..max_scans {
123            if start.elapsed() >= time_budget || raw.len() >= n_samples {
124                break;
125            }
126
127            let (bt_info, elapsed_ns) = get_bluetooth_info_timed();
128
129            // Extract timing bytes (raw nanosecond LSBs)
130            for shift in (0..64).step_by(8) {
131                raw.push((elapsed_ns >> shift) as u8);
132            }
133
134            // Parse RSSI values — raw LSBs
135            if let Some(info) = bt_info {
136                let rssi_values = parse_rssi_values(&info);
137                for rssi in &rssi_values {
138                    raw.push((*rssi & 0xFF) as u8);
139                }
140                raw.push(info.len() as u8);
141                raw.push((info.len() >> 8) as u8);
142            }
143        }
144
145        raw.truncate(n_samples);
146        raw
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn bluetooth_noise_info() {
156        let src = BluetoothNoiseSource;
157        assert_eq!(src.name(), "bluetooth_noise");
158        assert_eq!(src.info().category, SourceCategory::Sensor);
159    }
160
161    #[test]
162    fn parse_rssi_values_works() {
163        let sample = r#"
164            Connected: Yes
165            RSSI: -45
166            Some Device:
167              RSSI: -72
168              Name: Test
169        "#;
170        let values = parse_rssi_values(sample);
171        assert_eq!(values, vec![-45, -72]);
172    }
173
174    #[test]
175    fn parse_rssi_empty() {
176        let sample = "No bluetooth data here";
177        let values = parse_rssi_values(sample);
178        assert!(values.is_empty());
179    }
180
181    #[test]
182    fn bluetooth_composite_flag() {
183        let src = BluetoothNoiseSource;
184        assert!(!src.info().composite);
185    }
186
187    #[test]
188    #[cfg(target_os = "macos")]
189    #[ignore] // Requires Bluetooth hardware
190    fn bluetooth_noise_collects_bytes() {
191        let src = BluetoothNoiseSource;
192        if src.is_available() {
193            let data = src.collect(32);
194            assert!(!data.is_empty());
195            assert!(data.len() <= 32);
196        }
197    }
198}