openentropy_core/sources/
sensor.rs1use std::collections::HashMap;
9use std::process::Command;
10use std::thread;
11use std::time::Duration;
12
13use crate::source::{EntropySource, SourceCategory, SourceInfo};
14
15const SNAPSHOT_DELAY: Duration = Duration::from_millis(50);
17
18static SENSOR_NOISE_INFO: SourceInfo = SourceInfo {
19 name: "sensor_noise",
20 description: "MEMS accelerometer/gyro noise via ioreg",
21 physics: "Reads accelerometer, gyroscope, and magnetometer via CoreMotion. Even at rest, \
22 MEMS sensors exhibit: Brownian motion of the proof mass, thermo-mechanical noise, \
23 electronic 1/f noise, and quantization noise. The MacBook's accelerometer detects \
24 micro-vibrations from fans, disk, and building structure.",
25 category: SourceCategory::Hardware,
26 platform_requirements: &["macos"],
27 entropy_rate_estimate: 100.0,
28 composite: false,
29};
30
31pub struct SensorNoiseSource;
33
34fn parse_ioreg_numerics(output: &str) -> HashMap<String, i64> {
37 let mut map = HashMap::new();
38
39 for line in output.lines() {
40 let trimmed = line.trim();
41
42 if let Some(eq_idx) = trimmed.find(" = ") {
46 let raw_key = trimmed[..eq_idx].trim();
47 let val_part = trimmed[eq_idx + 3..].trim();
48
49 let key_part = raw_key
51 .trim_start_matches(['|', ' '])
52 .trim_matches('"')
53 .trim();
54
55 if key_part.is_empty() {
56 continue;
57 }
58
59 if let Ok(v) = val_part.parse::<i64>() {
61 map.insert(key_part.to_string(), v);
62 }
63 }
64 }
65
66 map
67}
68
69fn snapshot_ioreg() -> Option<String> {
71 let output = Command::new("ioreg").args(["-l", "-w0"]).output().ok()?;
72
73 if !output.status.success() {
74 return None;
75 }
76
77 Some(String::from_utf8_lossy(&output.stdout).to_string())
78}
79
80fn has_sensor_data() -> bool {
82 let output = Command::new("ioreg").args(["-l", "-w0"]).output();
83
84 match output {
85 Ok(out) if out.status.success() => {
86 let stdout = String::from_utf8_lossy(&out.stdout);
87 stdout.contains("SMCMotionSensor")
89 || stdout.contains("Accelerometer")
90 || stdout.contains("accelerometer")
91 || stdout.contains("MotionSensor")
92 || stdout.contains("gyro")
93 || stdout.contains("Gyro")
94 || stdout.contains("Temperature")
98 || stdout.contains("FanSpeed")
99 }
100 _ => false,
101 }
102}
103
104impl EntropySource for SensorNoiseSource {
105 fn info(&self) -> &SourceInfo {
106 &SENSOR_NOISE_INFO
107 }
108
109 fn is_available(&self) -> bool {
110 cfg!(target_os = "macos") && has_sensor_data()
111 }
112
113 fn collect(&self, n_samples: usize) -> Vec<u8> {
114 let raw1 = match snapshot_ioreg() {
117 Some(s) => s,
118 None => return Vec::new(),
119 };
120 let snap1 = parse_ioreg_numerics(&raw1);
121
122 thread::sleep(SNAPSHOT_DELAY);
123
124 let raw2 = match snapshot_ioreg() {
125 Some(s) => s,
126 None => return Vec::new(),
127 };
128 let snap2 = parse_ioreg_numerics(&raw2);
129
130 let mut deltas: Vec<i64> = Vec::new();
132 for (key, v2) in &snap2 {
133 if let Some(v1) = snap1.get(key) {
134 let delta = v2.wrapping_sub(*v1);
135 if delta != 0 {
136 deltas.push(delta);
137 }
138 }
139 }
140
141 if deltas.is_empty() {
142 return Vec::new();
143 }
144
145 let mut output = Vec::with_capacity(n_samples);
147
148 let mixed: Vec<i64> = if deltas.len() >= 2 {
150 deltas.windows(2).map(|w| w[0] ^ w[1]).collect()
151 } else {
152 deltas.clone()
153 };
154
155 for d in &mixed {
157 output.push(*d as u8);
159 if output.len() >= n_samples {
160 break;
161 }
162 output.push((*d >> 8) as u8);
164 if output.len() >= n_samples {
165 break;
166 }
167 }
168
169 if output.len() < n_samples && !output.is_empty() {
171 let base = output.clone();
172 let mut idx = 0;
173 while output.len() < n_samples {
174 let a = base[idx % base.len()];
175 let b = base[(idx + 1) % base.len()];
176 output.push(a ^ b ^ (idx as u8));
177 idx += 1;
178 }
179 }
180
181 output.truncate(n_samples);
182 output
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn sensor_noise_info() {
192 let src = SensorNoiseSource;
193 assert_eq!(src.name(), "sensor_noise");
194 assert_eq!(src.info().category, SourceCategory::Hardware);
195 assert_eq!(src.info().entropy_rate_estimate, 100.0);
196 }
197
198 #[test]
199 fn parse_ioreg_numerics_works() {
200 let sample = r#"
201 | | "Temperature" = 45
202 | | "FanSpeed" = 1200
203 | | "Name" = "some string"
204 "#;
205 let map = parse_ioreg_numerics(sample);
206 assert_eq!(map.get("Temperature"), Some(&45));
207 assert_eq!(map.get("FanSpeed"), Some(&1200));
208 assert!(!map.contains_key("Name"));
209 }
210}