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//! Shannon entropy ~3-5 bits/byte. Timing bytes have higher entropy than
9//! RSSI values which cluster around typical signal strengths.
10
11use std::process::Command;
12use std::time::{Duration, Instant};
13
14use crate::source::{EntropySource, SourceCategory, SourceInfo};
15
16/// Path to system_profiler on macOS.
17const SYSTEM_PROFILER_PATH: &str = "/usr/sbin/system_profiler";
18
19/// Timeout for system_profiler command.
20const BT_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
21
22static BLUETOOTH_NOISE_INFO: SourceInfo = SourceInfo {
23    name: "bluetooth_noise",
24    description: "BLE RSSI values and scanning timing jitter",
25    physics: "Scans BLE advertisements via CoreBluetooth and collects RSSI values from \
26              nearby devices. Each RSSI reading reflects: 2.4 GHz multipath propagation, \
27              frequency hopping across 40 channels, advertising interval jitter (\u{00b1}10ms), \
28              transmit power variation, and receiver thermal noise.",
29    category: SourceCategory::Hardware,
30    platform_requirements: &["macos"],
31    entropy_rate_estimate: 50.0,
32    composite: 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                let stdout = child
80                    .wait_with_output()
81                    .ok()
82                    .map(|o| String::from_utf8_lossy(&o.stdout).to_string());
83                return (stdout, elapsed);
84            }
85            Ok(None) => {
86                if Instant::now() >= deadline {
87                    let _ = child.kill();
88                    let _ = child.wait();
89                    return (None, t0.elapsed().as_nanos() as u64);
90                }
91                std::thread::sleep(Duration::from_millis(50));
92            }
93            Err(_) => return (None, t0.elapsed().as_nanos() as u64),
94        }
95    }
96}
97
98impl EntropySource for BluetoothNoiseSource {
99    fn info(&self) -> &SourceInfo {
100        &BLUETOOTH_NOISE_INFO
101    }
102
103    fn is_available(&self) -> bool {
104        std::path::Path::new(SYSTEM_PROFILER_PATH).exists()
105    }
106
107    fn collect(&self, n_samples: usize) -> Vec<u8> {
108        let mut raw = Vec::with_capacity(n_samples);
109
110        let time_budget = Duration::from_secs(10);
111        let start = Instant::now();
112        let max_scans = 50;
113
114        for _ in 0..max_scans {
115            if start.elapsed() >= time_budget || raw.len() >= n_samples {
116                break;
117            }
118
119            let (bt_info, elapsed_ns) = get_bluetooth_info_timed();
120
121            // Extract timing bytes (raw nanosecond LSBs)
122            for shift in (0..64).step_by(8) {
123                raw.push((elapsed_ns >> shift) as u8);
124            }
125
126            // Parse RSSI values — raw LSBs
127            if let Some(info) = bt_info {
128                let rssi_values = parse_rssi_values(&info);
129                for rssi in &rssi_values {
130                    raw.push((*rssi & 0xFF) as u8);
131                }
132                raw.push(info.len() as u8);
133                raw.push((info.len() >> 8) as u8);
134            }
135        }
136
137        raw.truncate(n_samples);
138        raw
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn bluetooth_noise_info() {
148        let src = BluetoothNoiseSource;
149        assert_eq!(src.name(), "bluetooth_noise");
150        assert_eq!(src.info().category, SourceCategory::Hardware);
151    }
152
153    #[test]
154    fn parse_rssi_values_works() {
155        let sample = r#"
156            Connected: Yes
157            RSSI: -45
158            Some Device:
159              RSSI: -72
160              Name: Test
161        "#;
162        let values = parse_rssi_values(sample);
163        assert_eq!(values, vec![-45, -72]);
164    }
165
166    #[test]
167    fn parse_rssi_empty() {
168        let sample = "No bluetooth data here";
169        let values = parse_rssi_values(sample);
170        assert!(values.is_empty());
171    }
172}