device_fingerprint/lib.rs
1//! # Device Fingerprint
2//!
3//! Generate a unique device fingerprint by collecting hardware identifiers from Windows devices.
4//!
5//! ## Features
6//!
7//! - No admin rights required
8//! - Uses only Windows native APIs
9//! - Generates SHA256 format device fingerprint
10//!
11//! ## Usage Example
12//!
13//! ```rust,no_run
14//! use device_fingerprint::{generate, verify};
15//!
16//! // Generate device fingerprint
17//! let fingerprint = generate();
18//! println!("Device Fingerprint: {}", fingerprint);
19//!
20//! // Verify device fingerprint
21//! let is_valid = verify(&fingerprint);
22//! assert!(is_valid);
23//! ```
24//!
25//! ## Collected Information
26//!
27//! | Component | Source | Description |
28//! |-----------|--------|-------------|
29//! | Machine GUID | Registry | Unique identifier generated during Windows installation |
30//! | CPU ID | CPUID instruction | Processor vendor and feature information |
31//!
32//! ## Optional Features
33//!
34//! Provides `wmic_uuid()` method to get SMBIOS UUID via WMI, which can be manually integrated into fingerprint calculation.
35
36use sha2::{Digest, Sha256};
37
38/// Hardware information collectors module
39pub mod collectors {
40 use std::process::Command;
41 use windows_sys::Win32::Foundation::ERROR_SUCCESS;
42 use windows_sys::Win32::System::Registry::{
43 HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY, REG_SZ, RegCloseKey, RegOpenKeyExW,
44 RegQueryValueExW,
45 };
46
47 /// Get Machine GUID
48 ///
49 /// Reads `MachineGuid` from registry `HKLM\SOFTWARE\Microsoft\Cryptography`.
50 /// This value is generated during Windows installation and changes after OS reinstall.
51 ///
52 /// # Returns
53 ///
54 /// - `Some(String)` - Successfully retrieved GUID, format like `c7d53fd7-f424-4d67-a851-2cda6a2efce8`
55 /// - `None` - Failed to retrieve
56 ///
57 /// # Example
58 ///
59 /// ```rust,no_run
60 /// use device_fingerprint::collectors::machine_guid;
61 ///
62 /// if let Some(guid) = machine_guid() {
63 /// println!("Machine GUID: {}", guid);
64 /// }
65 /// ```
66 pub fn machine_guid() -> Option<String> {
67 const SUBKEY: &[u16] = &[
68 'S' as u16,
69 'O' as u16,
70 'F' as u16,
71 'T' as u16,
72 'W' as u16,
73 'A' as u16,
74 'R' as u16,
75 'E' as u16,
76 '\\' as u16,
77 'M' as u16,
78 'i' as u16,
79 'c' as u16,
80 'r' as u16,
81 'o' as u16,
82 's' as u16,
83 'o' as u16,
84 'f' as u16,
85 't' as u16,
86 '\\' as u16,
87 'C' as u16,
88 'r' as u16,
89 'y' as u16,
90 'p' as u16,
91 't' as u16,
92 'o' as u16,
93 'g' as u16,
94 'r' as u16,
95 'a' as u16,
96 'p' as u16,
97 'h' as u16,
98 'y' as u16,
99 0,
100 ];
101
102 const VALUE_NAME: &[u16] = &[
103 'M' as u16, 'a' as u16, 'c' as u16, 'h' as u16, 'i' as u16, 'n' as u16, 'e' as u16,
104 'G' as u16, 'u' as u16, 'i' as u16, 'd' as u16, 0,
105 ];
106
107 unsafe {
108 let mut hkey = std::ptr::null_mut();
109 let result = RegOpenKeyExW(
110 HKEY_LOCAL_MACHINE,
111 SUBKEY.as_ptr(),
112 0,
113 KEY_READ | KEY_WOW64_64KEY,
114 &mut hkey,
115 );
116
117 if result != ERROR_SUCCESS {
118 return None;
119 }
120
121 let mut data_type: u32 = 0;
122 let mut data_size: u32 = 0;
123 let result = RegQueryValueExW(
124 hkey,
125 VALUE_NAME.as_ptr(),
126 std::ptr::null_mut(),
127 &mut data_type,
128 std::ptr::null_mut(),
129 &mut data_size,
130 );
131
132 if result != ERROR_SUCCESS {
133 RegCloseKey(hkey);
134 return None;
135 }
136
137 if data_type != REG_SZ {
138 RegCloseKey(hkey);
139 return None;
140 }
141
142 let mut buffer = vec![0u16; (data_size / 2) as usize];
143 let result = RegQueryValueExW(
144 hkey,
145 VALUE_NAME.as_ptr(),
146 std::ptr::null_mut(),
147 &mut data_type,
148 buffer.as_mut_ptr().cast(),
149 &mut data_size,
150 );
151
152 RegCloseKey(hkey);
153
154 if result != ERROR_SUCCESS {
155 return None;
156 }
157
158 let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
159 let guid = String::from_utf16_lossy(&buffer[..len]);
160 Some(guid)
161 }
162 }
163
164 /// Get CPU ID
165 ///
166 /// Uses CPUID instruction to get processor vendor and feature information.
167 /// This value is bound to the physical CPU and changes after CPU replacement.
168 ///
169 /// # Returns
170 ///
171 /// - `Some(String)` - CPU information string
172 /// - `None` - Failed to retrieve (non x86/x86_64 architecture)
173 ///
174 /// # Example
175 ///
176 /// ```rust,no_run
177 /// use device_fingerprint::collectors::cpu_id;
178 ///
179 /// if let Some(cpu) = cpu_id() {
180 /// println!("CPU ID: {}", cpu);
181 /// }
182 /// ```
183 pub fn cpu_id() -> Option<String> {
184 #[cfg(target_arch = "x86")]
185 use std::arch::x86::__cpuid;
186 #[cfg(target_arch = "x86_64")]
187 use std::arch::x86_64::__cpuid;
188
189 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
190 {
191 unsafe {
192 // CPUID EAX=0: Get vendor ID
193 let cpuid0 = __cpuid(0);
194 let vendor = format!(
195 "{}{}{}",
196 std::str::from_utf8(&cpuid0.ebx.to_le_bytes()).unwrap_or(""),
197 std::str::from_utf8(&cpuid0.edx.to_le_bytes()).unwrap_or(""),
198 std::str::from_utf8(&cpuid0.ecx.to_le_bytes()).unwrap_or("")
199 );
200
201 // CPUID EAX=1: Get processor signature and features
202 let cpuid1 = __cpuid(1);
203 let processor_signature = cpuid1.eax;
204 let processor_features = cpuid1.edx;
205
206 let cpu_info = format!(
207 "{}-{:08X}-{:08X}",
208 vendor, processor_signature, processor_features
209 );
210
211 Some(cpu_info)
212 }
213 }
214
215 #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
216 {
217 None
218 }
219 }
220
221 /// Get SMBIOS UUID via WMIC command
222 ///
223 /// Calls `wmic csproduct get UUID` to get motherboard UUID.
224 ///
225 /// > **Note**: This method is implemented by calling an external process, essentially using WMI.
226 ///
227 /// # Returns
228 ///
229 /// - `Some(String)` - UUID string
230 /// - `None` - Failed to retrieve
231 ///
232 /// # Example
233 ///
234 /// ```rust,no_run
235 /// use device_fingerprint::collectors::wmic_uuid;
236 ///
237 /// if let Some(uuid) = wmic_uuid() {
238 /// println!("SMBIOS UUID: {}", uuid);
239 /// }
240 /// ```
241 pub fn wmic_uuid() -> Option<String> {
242 let output = Command::new("wmic")
243 .args(["csproduct", "get", "UUID"])
244 .output()
245 .ok()?;
246
247 if !output.status.success() {
248 return None;
249 }
250
251 let stdout = String::from_utf8_lossy(&output.stdout);
252 for line in stdout.lines() {
253 let trimmed = line.trim();
254 // Skip header and empty lines, UUID format is 8-4-4-4-12
255 if !trimmed.is_empty() && trimmed != "UUID" && trimmed.len() == 36 {
256 return Some(trimmed.to_string());
257 }
258 }
259
260 None
261 }
262}
263
264/// Generate unique device fingerprint
265///
266/// Combines Machine GUID and CPU ID to generate a SHA256 hash.
267///
268/// # Returns
269///
270/// Returns a 64-character hexadecimal SHA256 hash string.
271///
272/// # Example
273///
274/// ```rust,no_run
275/// use device_fingerprint::generate;
276///
277/// let fingerprint = generate();
278/// println!("Device Fingerprint: {}", fingerprint);
279/// assert_eq!(fingerprint.len(), 64);
280/// ```
281pub fn generate() -> String {
282 let mut hasher = Sha256::new();
283
284 if let Some(guid) = collectors::machine_guid() {
285 hasher.update(guid.as_bytes());
286 }
287
288 if let Some(cpu) = collectors::cpu_id() {
289 hasher.update(cpu.as_bytes());
290 }
291
292 format!("{:x}", hasher.finalize())
293}
294
295/// Verify if device fingerprint matches
296///
297/// Regenerates the current device's fingerprint and performs strict comparison with the provided fingerprint.
298///
299/// # Arguments
300///
301/// * `expected` - Expected device fingerprint string
302///
303/// # Returns
304///
305/// - `true` - Fingerprint matches
306/// - `false` - Fingerprint does not match
307///
308/// # Example
309///
310/// ```rust,no_run
311/// use device_fingerprint::{generate, verify};
312///
313/// let fingerprint = generate();
314/// assert!(verify(&fingerprint));
315/// assert!(!verify("invalid_fingerprint"));
316/// ```
317pub fn verify(expected: &str) -> bool {
318 let current = generate();
319 current == expected
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_generate_fingerprint() {
328 let fp = generate();
329 assert_eq!(
330 fp.len(),
331 64,
332 "Fingerprint should be a 64-character SHA256 hash"
333 );
334 assert!(
335 fp.chars().all(|c| c.is_ascii_hexdigit()),
336 "Fingerprint should be a hexadecimal string"
337 );
338 }
339
340 #[test]
341 fn test_verify_fingerprint() {
342 let fp = generate();
343 assert!(
344 verify(&fp),
345 "Verifying current device fingerprint should succeed"
346 );
347 assert!(
348 !verify("0000000000000000000000000000000000000000000000000000000000000000"),
349 "Verifying incorrect fingerprint should fail"
350 );
351 }
352
353 #[test]
354 fn test_fingerprint_consistency() {
355 let fp1 = generate();
356 let fp2 = generate();
357 assert_eq!(
358 fp1, fp2,
359 "Multiple generations should produce consistent fingerprints"
360 );
361 }
362
363 #[test]
364 fn test_machine_guid() {
365 let guid = collectors::machine_guid();
366 assert!(guid.is_some(), "Machine GUID should be retrievable");
367 if let Some(g) = guid {
368 assert_eq!(g.len(), 36, "GUID format should be 36 characters");
369 }
370 }
371
372 #[test]
373 fn test_cpu_id() {
374 let cpu = collectors::cpu_id();
375 assert!(cpu.is_some(), "CPU ID should be retrievable");
376 }
377}