promocrypt_core/
machine_id.rs1use ring::digest::{Context, SHA256};
7
8use crate::error::{PromocryptError, Result};
9
10const MACHINE_ID_SALT: &[u8] = b"promocrypt-machine-id-v1";
12
13pub fn get_machine_id() -> Result<[u8; 32]> {
29 let components = get_machine_id_components();
30
31 if components.is_empty() {
32 return Err(PromocryptError::MachineIdUnavailable(
33 "No hardware identifiers available".to_string(),
34 ));
35 }
36
37 let combined = components.join(":");
39 let mut context = Context::new(&SHA256);
40 context.update(combined.as_bytes());
41 context.update(MACHINE_ID_SALT);
42 let digest = context.finish();
43
44 let mut id = [0u8; 32];
45 id.copy_from_slice(digest.as_ref());
46 Ok(id)
47}
48
49pub fn is_machine_id_available() -> bool {
51 !get_machine_id_components().is_empty()
52}
53
54pub fn get_machine_id_components() -> Vec<String> {
58 let mut components = Vec::new();
59
60 if let Some(macs) = get_mac_addresses() {
62 components.extend(macs);
63 }
64
65 if let Some(cpu_id) = get_cpu_id() {
67 components.push(cpu_id);
68 }
69
70 if let Some(disk_id) = get_disk_serial() {
72 components.push(disk_id);
73 }
74
75 components
76}
77
78fn get_mac_addresses() -> Option<Vec<String>> {
80 #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
81 {
82 use mac_address::mac_address_by_name;
83 use sysinfo::Networks;
84
85 let networks = Networks::new_with_refreshed_list();
86 let mut macs: Vec<String> = Vec::new();
87
88 for (interface_name, _) in networks.iter() {
89 if interface_name.starts_with("lo")
91 || interface_name.starts_with("veth")
92 || interface_name.starts_with("docker")
93 || interface_name.starts_with("br-")
94 {
95 continue;
96 }
97
98 if let Ok(Some(mac)) = mac_address_by_name(interface_name) {
99 let mac_str = mac.to_string();
100 if mac_str != "00:00:00:00:00:00" && mac_str != "ff:ff:ff:ff:ff:ff" {
102 macs.push(mac_str);
103 }
104 }
105 }
106
107 macs.sort();
109 macs.dedup();
110
111 if macs.is_empty() { None } else { Some(macs) }
112 }
113
114 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
115 {
116 None
117 }
118}
119
120fn get_cpu_id() -> Option<String> {
122 #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
123 {
124 use sysinfo::System;
125
126 let sys = System::new_all();
127
128 let cpus = sys.cpus();
130 if let Some(cpu) = cpus.first() {
131 let brand = cpu.brand().trim();
132 if !brand.is_empty() {
133 return Some(format!("{}:{}", brand, cpus.len()));
134 }
135 }
136
137 None
138 }
139
140 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
141 {
142 None
143 }
144}
145
146fn get_disk_serial() -> Option<String> {
148 #[cfg(target_os = "linux")]
149 {
150 use std::fs;
152
153 for disk in ["sda", "nvme0n1", "vda", "xvda"] {
155 let serial_path = format!("/sys/block/{}/device/serial", disk);
156 if let Ok(serial) = fs::read_to_string(&serial_path) {
157 let serial = serial.trim();
158 if !serial.is_empty() {
159 return Some(format!("disk:{}", serial));
160 }
161 }
162
163 let serial_path = format!("/sys/block/{}/serial", disk);
165 if let Ok(serial) = fs::read_to_string(&serial_path) {
166 let serial = serial.trim();
167 if !serial.is_empty() {
168 return Some(format!("disk:{}", serial));
169 }
170 }
171 }
172
173 if let Ok(machine_id) = fs::read_to_string("/etc/machine-id") {
175 let machine_id = machine_id.trim();
176 if !machine_id.is_empty() {
177 return Some(format!("machine-id:{}", machine_id));
178 }
179 }
180
181 None
182 }
183
184 #[cfg(target_os = "macos")]
185 {
186 use std::process::Command;
187
188 if let Ok(output) = Command::new("system_profiler")
190 .args(["SPHardwareDataType"])
191 .output()
192 {
193 let stdout = String::from_utf8_lossy(&output.stdout);
194 for line in stdout.lines() {
195 if line.contains("Hardware UUID:")
196 && let Some(uuid) = line.split(':').nth(1)
197 {
198 let uuid = uuid.trim();
199 if !uuid.is_empty() {
200 return Some(format!("hw-uuid:{}", uuid));
201 }
202 }
203 }
204 }
205
206 if let Ok(output) = Command::new("ioreg")
208 .args(["-rd1", "-c", "IOPlatformExpertDevice"])
209 .output()
210 {
211 let stdout = String::from_utf8_lossy(&output.stdout);
212 for line in stdout.lines() {
213 if line.contains("IOPlatformSerialNumber")
214 && let Some(serial) = line.split('"').nth(3)
215 && !serial.is_empty()
216 {
217 return Some(format!("serial:{}", serial));
218 }
219 }
220 }
221
222 None
223 }
224
225 #[cfg(target_os = "windows")]
226 {
227 use std::process::Command;
228
229 if let Ok(output) = Command::new("wmic")
231 .args(["baseboard", "get", "serialnumber"])
232 .output()
233 {
234 let stdout = String::from_utf8_lossy(&output.stdout);
235 for line in stdout.lines().skip(1) {
236 let serial = line.trim();
237 if !serial.is_empty() && serial != "To be filled by O.E.M." {
238 return Some(format!("baseboard:{}", serial));
239 }
240 }
241 }
242
243 if let Ok(output) = Command::new("reg")
245 .args([
246 "query",
247 "HKLM\\SOFTWARE\\Microsoft\\Cryptography",
248 "/v",
249 "MachineGuid",
250 ])
251 .output()
252 {
253 let stdout = String::from_utf8_lossy(&output.stdout);
254 for line in stdout.lines() {
255 if line.contains("MachineGuid") {
256 if let Some(guid) = line.split_whitespace().last() {
257 return Some(format!("machine-guid:{}", guid));
258 }
259 }
260 }
261 }
262
263 None
264 }
265
266 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
267 {
268 None
269 }
270}
271
272pub fn machine_id_from_components(components: &[&str]) -> [u8; 32] {
276 let combined = components.join(":");
277 let mut context = Context::new(&SHA256);
278 context.update(combined.as_bytes());
279 context.update(MACHINE_ID_SALT);
280 let digest = context.finish();
281
282 let mut id = [0u8; 32];
283 id.copy_from_slice(digest.as_ref());
284 id
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_machine_id_from_components() {
293 let components = ["mac:00:11:22:33:44:55", "cpu:Intel:8"];
294 let id1 = machine_id_from_components(&components);
295 let id2 = machine_id_from_components(&components);
296
297 assert_eq!(id1, id2);
298 assert_eq!(id1.len(), 32);
299 }
300
301 #[test]
302 fn test_different_components_different_id() {
303 let id1 = machine_id_from_components(&["component1"]);
304 let id2 = machine_id_from_components(&["component2"]);
305
306 assert_ne!(id1, id2);
307 }
308
309 #[test]
310 fn test_get_machine_id_components() {
311 let components = get_machine_id_components();
313 println!("Found {} components", components.len());
315 for comp in &components {
316 println!(" {}", comp);
317 }
318 }
319
320 #[test]
321 fn test_get_machine_id() {
322 match get_machine_id() {
324 Ok(id) => {
325 assert_eq!(id.len(), 32);
326 println!("Machine ID: {}", hex::encode(id));
327 }
328 Err(e) => {
329 println!("Could not get machine ID: {}", e);
330 }
331 }
332 }
333
334 #[test]
335 fn test_is_machine_id_available() {
336 let available = is_machine_id_available();
337 println!("Machine ID available: {}", available);
338 }
340
341 #[test]
342 fn test_machine_id_deterministic() {
343 if is_machine_id_available() {
345 let id1 = get_machine_id().unwrap();
346 let id2 = get_machine_id().unwrap();
347 assert_eq!(id1, id2);
348 }
349 }
350}