Skip to main content

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}