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}