openentropy_core/sources/
bluetooth.rs1use std::process::Command;
10use std::time::{Duration, Instant};
11
12use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
13
14const SYSTEM_PROFILER_PATH: &str = "/usr/sbin/system_profiler";
16
17const 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
34pub struct BluetoothNoiseSource;
36
37fn 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
55fn 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 for shift in (0..64).step_by(8) {
122 raw.push((elapsed_ns >> shift) as u8);
123 }
124
125 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] 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}