shield_core/
fingerprint.rs1use crate::error::{Result, ShieldError};
7#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
8use std::process::Command;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum FingerprintMode {
13 #[default]
15 None,
16 Motherboard,
18 CPU,
20 Combined,
22}
23
24pub fn collect_fingerprint(mode: FingerprintMode) -> Result<String> {
34 match mode {
35 FingerprintMode::None => Ok(String::new()),
36 FingerprintMode::Motherboard => get_motherboard_serial(),
37 FingerprintMode::CPU => get_cpu_id(),
38 FingerprintMode::Combined => {
39 let mut components = Vec::new();
40
41 if let Ok(mb) = get_motherboard_serial() {
42 components.push(mb);
43 }
44
45 if let Ok(cpu) = get_cpu_id() {
46 components.push(cpu);
47 }
48
49 if components.is_empty() {
50 return Err(ShieldError::FingerprintUnavailable);
51 }
52
53 let combined = components.join("-");
55 Ok(format!("{:x}", md5::compute(combined.as_bytes())))
56 }
57 }
58}
59
60#[cfg(target_os = "windows")]
62fn get_motherboard_serial() -> Result<String> {
63 let output = Command::new("wmic")
64 .args(&["baseboard", "get", "serialnumber", "/value"])
65 .output()
66 .map_err(|_| ShieldError::FingerprintUnavailable)?;
67
68 let output_str = String::from_utf8_lossy(&output.stdout);
69 for line in output_str.lines() {
70 if line.starts_with("SerialNumber=") {
71 let serial = line.replace("SerialNumber=", "").trim().to_string();
72 if !serial.is_empty() && serial != "To be filled by O.E.M." {
73 return Ok(serial);
74 }
75 }
76 }
77 Err(ShieldError::FingerprintUnavailable)
78}
79
80#[cfg(target_os = "linux")]
81fn get_motherboard_serial() -> Result<String> {
82 if let Ok(content) = std::fs::read_to_string("/sys/class/dmi/id/board_serial") {
84 let serial = content.trim();
85 if !serial.is_empty() && serial != "To be filled by O.E.M." {
86 return Ok(serial.to_string());
87 }
88 }
89
90 let output = Command::new("dmidecode")
92 .args(["-s", "baseboard-serial-number"])
93 .output()
94 .map_err(|_| ShieldError::FingerprintUnavailable)?;
95
96 let serial = String::from_utf8_lossy(&output.stdout).trim().to_string();
97 if !serial.is_empty() && serial != "To be filled by O.E.M." {
98 Ok(serial)
99 } else {
100 Err(ShieldError::FingerprintUnavailable)
101 }
102}
103
104#[cfg(target_os = "macos")]
105fn get_motherboard_serial() -> Result<String> {
106 let output = Command::new("system_profiler")
107 .args(&["SPHardwareDataType"])
108 .output()
109 .map_err(|_| ShieldError::FingerprintUnavailable)?;
110
111 let output_str = String::from_utf8_lossy(&output.stdout);
112 for line in output_str.lines() {
113 if line.contains("Serial Number") {
114 if let Some(serial) = line.split(':').nth(1) {
115 return Ok(serial.trim().to_string());
116 }
117 }
118 }
119 Err(ShieldError::FingerprintUnavailable)
120}
121
122#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
123fn get_motherboard_serial() -> Result<String> {
124 Err(ShieldError::FingerprintUnavailable)
125}
126
127#[cfg(target_os = "windows")]
129fn get_cpu_id() -> Result<String> {
130 let output = Command::new("wmic")
131 .args(&["cpu", "get", "ProcessorId", "/value"])
132 .output()
133 .map_err(|_| ShieldError::FingerprintUnavailable)?;
134
135 let output_str = String::from_utf8_lossy(&output.stdout);
136 for line in output_str.lines() {
137 if line.starts_with("ProcessorId=") {
138 let cpu_id = line.replace("ProcessorId=", "").trim().to_string();
139 if !cpu_id.is_empty() {
140 return Ok(cpu_id);
141 }
142 }
143 }
144 Err(ShieldError::FingerprintUnavailable)
145}
146
147#[cfg(target_os = "linux")]
148fn get_cpu_id() -> Result<String> {
149 if let Ok(content) = std::fs::read_to_string("/proc/cpuinfo") {
150 for line in content.lines() {
152 if line.starts_with("processor") && line.contains('0') {
153 return Ok(format!("{:x}", md5::compute(line.as_bytes())));
154 }
155 }
156 }
157 Err(ShieldError::FingerprintUnavailable)
158}
159
160#[cfg(target_os = "macos")]
161fn get_cpu_id() -> Result<String> {
162 let output = Command::new("sysctl")
163 .args(&["-n", "machdep.cpu.brand_string"])
164 .output()
165 .map_err(|_| ShieldError::FingerprintUnavailable)?;
166
167 let cpu_info = String::from_utf8_lossy(&output.stdout).trim().to_string();
168 if !cpu_info.is_empty() {
169 Ok(format!("{:x}", md5::compute(cpu_info.as_bytes())))
170 } else {
171 Err(ShieldError::FingerprintUnavailable)
172 }
173}
174
175#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
176fn get_cpu_id() -> Result<String> {
177 Err(ShieldError::FingerprintUnavailable)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_none_mode() {
186 let fp = collect_fingerprint(FingerprintMode::None).unwrap();
187 assert_eq!(fp, "");
188 }
189
190 #[test]
191 fn test_combined_mode() {
192 match collect_fingerprint(FingerprintMode::Combined) {
195 Ok(fp) => {
196 assert!(!fp.is_empty());
197 assert_eq!(fp.len(), 32); }
199 Err(ShieldError::FingerprintUnavailable) => {
200 }
202 Err(e) => panic!("Unexpected error: {e:?}"),
203 }
204 }
205}