openentropy_core/sources/sensor/
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: 1.0,
31 composite: false,
32 is_fast: false,
33};
34
35pub struct BluetoothNoiseSource;
37
38fn 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
56fn 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 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 for shift in (0..64).step_by(8) {
131 raw.push((elapsed_ns >> shift) as u8);
132 }
133
134 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] 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}