Skip to main content

receipt_bench/
fingerprint.rs

1//! Machine fingerprint generation using SHA-256 of local system characteristics.
2
3use sha2::{Digest, Sha256};
4use std::env;
5use std::fs;
6use std::process::Command;
7
8/// A unique fingerprint of the current machine environment.
9///
10/// Combines hostname, username, architecture, and OS kernel info
11/// to produce a reproducible 64-character hex string.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
13pub struct MachineFingerprint(String);
14
15impl MachineFingerprint {
16    /// Generate a new machine fingerprint by hashing local system characteristics.
17    pub fn generate() -> Self {
18        let mut hasher = Sha256::new();
19
20        // Hostname
21        let hostname = hostname().unwrap_or_else(|| "unknown".to_string());
22        hasher.update(hostname.as_bytes());
23
24        // Username
25        let username = username().unwrap_or_else(|| "unknown".to_string());
26        hasher.update(username.as_bytes());
27
28        // Architecture
29        let arch = env::consts::ARCH;
30        hasher.update(arch.as_bytes());
31
32        // OS family
33        let os = env::consts::OS;
34        hasher.update(os.as_bytes());
35
36        // Number of CPUs (cap at reasonable limit to avoid DoS)
37        let cpus = num_cpus().min(256);
38        hasher.update([cpus as u8]);
39
40        // Page size logarithm (log2, capped)
41        let page_size = page_size_log();
42        hasher.update([page_size as u8]);
43
44        // Machine ID if available (avoids hostname collisions)
45        if let Some(machine_id) = machine_id() {
46            hasher.update(machine_id.as_bytes());
47        }
48
49        let result = hasher.finalize();
50        let hex = encode_hex(&result);
51
52        // Truncate to 64 chars for readability (256-bit → 64 hex chars)
53        MachineFingerprint(hex[..64].to_string())
54    }
55
56    /// Create a fingerprint from a pre-existing hex string (for testing).
57    #[cfg(test)]
58    pub fn from_hex(hex: &str) -> Self {
59        MachineFingerprint(hex[..64].to_string())
60    }
61
62    /// Returns the raw fingerprint string (64 hex characters).
63    #[inline]
64    pub fn as_str(&self) -> &str {
65        &self.0
66    }
67}
68
69impl Default for MachineFingerprint {
70    fn default() -> Self {
71        Self::generate()
72    }
73}
74
75impl std::fmt::Display for MachineFingerprint {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "{}", self.0)
78    }
79}
80
81// Internal helpers
82
83fn hostname() -> Option<String> {
84    #[cfg(unix)]
85    {
86        Command::new("hostname")
87            .output()
88            .ok()
89            .filter(|o| o.status.success())
90            .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
91    }
92    #[cfg(not(unix))]
93    {
94        None
95    }
96}
97
98fn username() -> Option<String> {
99    env::var("USER").or_else(|_| env::var("USERNAME")).ok()
100}
101
102fn num_cpus() -> usize {
103    std::thread::available_parallelism()
104        .map(|p| p.get())
105        .unwrap_or(1)
106}
107
108fn page_size_log() -> usize {
109    // Standard page size is 4096, log2(4096) = 12
110    // If we can't detect, assume 12
111    #[cfg(unix)]
112    {
113        use std::fs;
114        if let Ok(content) = fs::read_to_string("/proc/sys/kernel/osureset_page_size") {
115            if let Ok(size) = content.trim().parse::<usize>() {
116                let mut log = 0usize;
117                let mut s = size;
118                while s > 1 {
119                    s /= 2;
120                    log += 1;
121                }
122                return log.min(16);
123            }
124        }
125        12
126    }
127    #[cfg(not(unix))]
128    {
129        12
130    }
131}
132
133fn machine_id() -> Option<String> {
134    #[cfg(unix)]
135    {
136        let paths = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
137        for path in &paths {
138            if let Ok(content) = fs::read_to_string(path) {
139                let trimmed = content.trim();
140                if !trimmed.is_empty() {
141                    return Some(trimmed.to_string());
142                }
143            }
144        }
145        None
146    }
147    #[cfg(not(unix))]
148    {
149        None
150    }
151}
152
153// Pure Rust hex encoder (no dependencies)
154fn encode_hex(data: &[u8]) -> String {
155    const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
156    let mut s = String::with_capacity(data.len() * 2);
157    for &b in data {
158        s.push(HEX_CHARS[(b >> 4) as usize] as char);
159        s.push(HEX_CHARS[(b & 0xf) as usize] as char);
160    }
161    s
162}