modkit_node_info/hardware_uuid.rs
1use uuid::Uuid;
2
3/// Neutral namespace identifier for hardware-based UUIDs
4const NAMESPACE_BYTES: &[u8] = b"node-hardware-id";
5
6/// Get a permanent hardware-based UUID for this machine.
7/// This UUID is the actual hardware identifier and will remain consistent
8/// across reboots and application restarts.
9///
10/// Platform-specific implementations:
11/// - macOS: Uses `IOPlatformUUID` from `IOKit` (already a UUID)
12/// - Linux: Uses /etc/machine-id or /var/lib/dbus/machine-id (converted to UUID)
13/// - Windows: Uses `MachineGuid` from registry (already a UUID)
14///
15/// Returns a hybrid UUID (00000000-0000-0000-xxxx-xxxxxxxxxxxx) if detection fails,
16/// where the left part is all zeros and the right part is random for uniqueness.
17pub fn get_hardware_uuid() -> Uuid {
18 match machine_uid::get() {
19 Ok(machine_id) => {
20 // Try to parse the machine_id as a UUID directly
21 match Uuid::parse_str(&machine_id) {
22 Ok(uuid) => {
23 tracing::debug!(
24 machine_id = %machine_id,
25 node_uuid = %uuid,
26 "Using hardware UUID"
27 );
28 uuid
29 }
30 Err(parse_err) => {
31 // If it's not a valid UUID format, hash it to create one
32 tracing::warn!(
33 machine_id = %machine_id,
34 error = %parse_err,
35 "Machine ID is not a valid UUID, using hash-based UUID"
36 );
37
38 // Use UUID v5 to create a deterministic UUID from the machine ID
39 // Combine namespace identifier with machine ID for hashing
40 let combined = [NAMESPACE_BYTES, b":", machine_id.as_bytes()].concat();
41 Uuid::new_v5(&uuid::Uuid::NAMESPACE_DNS, &combined)
42 }
43 }
44 }
45 Err(e) => {
46 // Return a hybrid UUID: zeros on the left (00000000-0000-0000), random on the right
47 // This indicates hardware detection failed while still providing uniqueness
48 let random_uuid = Uuid::new_v4();
49 let random_bytes = random_uuid.as_bytes();
50
51 // Create hybrid: first 8 bytes are zeros, last 8 bytes are random
52 let hybrid_bytes = [
53 0,
54 0,
55 0,
56 0,
57 0,
58 0,
59 0,
60 0, // Left part: all zeros
61 random_bytes[8],
62 random_bytes[9],
63 random_bytes[10],
64 random_bytes[11],
65 random_bytes[12],
66 random_bytes[13],
67 random_bytes[14],
68 random_bytes[15],
69 ];
70
71 let hybrid_uuid = Uuid::from_bytes(hybrid_bytes);
72
73 tracing::error!(
74 error = %e,
75 fallback_uuid = %hybrid_uuid,
76 "Failed to get hardware machine ID, using hybrid UUID (00000000-0000-0000-xxxx-xxxxxxxxxxxx)"
77 );
78
79 hybrid_uuid
80 }
81 }
82}
83
84#[cfg(test)]
85#[cfg_attr(coverage_nightly, coverage(off))]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_hardware_uuid_is_consistent() {
91 // The UUID should be the same across multiple calls
92 let uuid1 = get_hardware_uuid();
93 let uuid2 = get_hardware_uuid();
94
95 assert_eq!(uuid1, uuid2, "Hardware UUID should be consistent");
96 }
97
98 #[test]
99 fn test_hardware_uuid_format() {
100 let uuid = get_hardware_uuid();
101
102 // Check if it's a fallback UUID (first 8 bytes are zeros)
103 let uuid_bytes = uuid.as_bytes();
104 let is_fallback = uuid_bytes[0..8].iter().all(|&b| b == 0);
105
106 if is_fallback {
107 // If fallback, the right part should be random (not all zeros)
108 let right_part_all_zeros = uuid_bytes[8..16].iter().all(|&b| b == 0);
109 assert!(
110 !right_part_all_zeros,
111 "Fallback UUID should have random right part"
112 );
113 } else {
114 // On real hardware, should have a valid hardware UUID
115 assert!(
116 !is_fallback,
117 "Real hardware should not produce fallback UUID"
118 );
119 }
120 }
121}