openentropy_core/sources/
bluetooth.rs1use std::process::Command;
12use std::time::{Duration, Instant};
13
14use crate::source::{EntropySource, SourceCategory, SourceInfo};
15
16const SYSTEM_PROFILER_PATH: &str = "/usr/sbin/system_profiler";
18
19const 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
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 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 for shift in (0..64).step_by(8) {
123 raw.push((elapsed_ns >> shift) as u8);
124 }
125
126 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}