use sha2::{Digest, Sha256}; #[derive(Debug, Clone)] pub struct FingerprintLayer { pub source: &'static str, pub value: Option, } #[derive(Debug, Clone)] pub struct DeviceFingerprint { pub composite_hash: String, pub stability_score: u8, pub layers: Vec, } pub fn collect() -> DeviceFingerprint { let mut layers = Vec::new(); let l1 = try_dmi_uuid(); layers.push(FingerprintLayer { source: "hw_uuid", value: l1 }); let l2 = try_machine_id(); layers.push(FingerprintLayer { source: "os_id", value: l2 }); let l3 = try_rootfs_uuid(); layers.push(FingerprintLayer { source: "fs_uuid", value: l3 }); let l4 = Some(try_physical_mac()); layers.push(FingerprintLayer { source: "mac", value: l4 }); let mut hasher = Sha256::new(); for layer in &layers { if let Some(ref v) = layer.value { hasher.update(v.as_bytes()); } hasher.update(b"|"); } let hash = format!("HC-{:x}", hasher.finalize()); let stability = compute_stability(&layers); DeviceFingerprint { composite_hash: hash, stability_score: stability, layers, } } fn compute_stability(layers: &[FingerprintLayer]) -> u8 { let mut score: u8 = 0; if layers[0].value.is_some() { score += 40; } if layers[1].value.is_some() { score += 30; } if layers[2].value.is_some() { score += 20; } if layers[3].value.is_some() { score += 10; } score } #[cfg(target_os = "linux")] fn try_dmi_uuid() -> Option { std::fs::read_to_string("/sys/class/dmi/id/product_uuid") .map(|s| s.trim().to_string()) .ok() .filter(|s| !s.is_empty() && s != "00000000-0000-0000-0000-000000000000") } #[cfg(not(target_os = "linux"))] fn try_dmi_uuid() -> Option { None } #[cfg(target_os = "linux")] fn try_machine_id() -> Option { std::fs::read_to_string("/etc/machine-id") .or_else(|_| std::fs::read_to_string("/var/lib/dbus/machine-id")) .map(|s| s.trim().to_string()) .ok() .filter(|s| !s.is_empty()) } #[cfg(not(target_os = "linux"))] fn try_machine_id() -> Option { None } #[cfg(target_os = "linux")] fn try_rootfs_uuid() -> Option { std::fs::read_to_string("/proc/cmdline") .ok() .and_then(|cmdline| { for part in cmdline.split_whitespace() { if part.starts_with("root=") { return Some(part.trim_start_matches("root=").to_string()); } } None }) } #[cfg(not(target_os = "linux"))] fn try_rootfs_uuid() -> Option { None } fn try_physical_mac() -> String { #[cfg(target_os = "linux")] { let mut macs: Vec = Vec::new(); if let Ok(entries) = std::fs::read_dir("/sys/class/net") { for entry in entries.flatten() { let iface = entry.file_name().to_string_lossy().to_string(); if iface == "lo" || iface.starts_with("docker") || iface.starts_with("veth") || iface.starts_with("tap") { continue; } if let Ok(addr) = std::fs::read_to_string(entry.path().join("address")) { let a = addr.trim().to_string(); if !a.is_empty() && a != "00:00:00:00:00:00" { macs.push(a); } } } } if macs.is_empty() { "unknown-mac".to_string() } else { macs.sort(); macs.join(",") } } #[cfg(not(target_os = "linux"))] { "unknown-mac".to_string() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_collect_returns_valid_structure() { let fp = collect(); assert!(fp.composite_hash.starts_with("HC-")); assert_eq!(fp.layers.len(), 4); for layer in &fp.layers { assert!(matches!(layer.source, "hw_uuid" | "os_id" | "fs_uuid" | "mac")); } } #[test] fn test_stability_score_never_exceeds_100() { let fp = collect(); assert!(fp.stability_score <= 100); } #[test] fn test_composite_hash_deterministic() { let fp1 = collect(); let fp2 = collect(); assert_eq!(fp1.composite_hash, fp2.composite_hash); } }