//! Device ID generation //! //! Provides a stable, unique identifier for each machine that: //! - Survives agent reinstalls //! - Is hardware-derived when possible //! - Falls back to a persisted UUID if hardware IDs are unavailable use anyhow::Result; use std::fs; use std::path::PathBuf; use tracing::{debug, info, warn}; /// Get the device ID for this machine /// /// Priority: /// 1. Hardware-based ID (MachineGuid on Windows, machine-id on Linux) /// 2. Previously persisted ID /// 3. Generate and persist a new UUID pub fn get_device_id() -> String { // Try hardware-based ID first if let Some(id) = get_hardware_device_id() { debug!("Using hardware-based device ID"); return id; } // Try to read a persisted ID let persist_path = get_persist_path(); if let Some(id) = read_persisted_id(&persist_path) { debug!("Using persisted device ID from {:?}", persist_path); return id; } // Generate and persist a new ID let new_id = generate_device_id(); info!("Generated new device ID, persisting to {:?}", persist_path); if let Err(e) = persist_device_id(&persist_path, &new_id) { warn!("Failed to persist device ID: {}", e); } new_id } /// Generate a new device ID (UUID v4) fn generate_device_id() -> String { uuid::Uuid::new_v4().to_string() } /// Get the path where device ID should be persisted fn get_persist_path() -> PathBuf { #[cfg(target_os = "windows")] { // %ProgramData%\GuruRMM\.device-id let program_data = std::env::var("ProgramData") .unwrap_or_else(|_| "C:\\ProgramData".to_string()); PathBuf::from(program_data).join("GuruRMM").join(".device-id") } #[cfg(not(target_os = "windows"))] { // /var/lib/gururmm/.device-id PathBuf::from("/var/lib/gururmm/.device-id") } } /// Read a persisted device ID from disk fn read_persisted_id(path: &PathBuf) -> Option { fs::read_to_string(path) .ok() .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty() && s.len() < 100) } /// Persist device ID to disk fn persist_device_id(path: &PathBuf, id: &str) -> Result<()> { // Create parent directory if needed if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } fs::write(path, id)?; Ok(()) } /// Get hardware-based device ID #[cfg(target_os = "windows")] fn get_hardware_device_id() -> Option { // Try MachineGuid from registry // HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid use std::process::Command; let output = Command::new("reg") .args([ "query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid", ]) .output() .ok()?; if !output.status.success() { return None; } let stdout = String::from_utf8_lossy(&output.stdout); // Parse the output: "MachineGuid REG_SZ " for line in stdout.lines() { if line.contains("MachineGuid") { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 3 { let guid = parts.last()?.trim(); if !guid.is_empty() && guid.len() > 20 { return Some(format!("win-{}", guid)); } } } } None } /// Get hardware-based device ID #[cfg(target_os = "linux")] fn get_hardware_device_id() -> Option { // Try /etc/machine-id first (systemd) if let Ok(id) = fs::read_to_string("/etc/machine-id") { let id = id.trim(); if !id.is_empty() && id.len() >= 32 { return Some(format!("linux-{}", id)); } } // Try /var/lib/dbus/machine-id (older systems) if let Ok(id) = fs::read_to_string("/var/lib/dbus/machine-id") { let id = id.trim(); if !id.is_empty() && id.len() >= 32 { return Some(format!("linux-{}", id)); } } // Try SMBIOS product UUID (requires root usually) if let Ok(id) = fs::read_to_string("/sys/class/dmi/id/product_uuid") { let id = id.trim(); if !id.is_empty() && id.len() > 20 { return Some(format!("hw-{}", id)); } } None } /// Get hardware-based device ID #[cfg(target_os = "macos")] fn get_hardware_device_id() -> Option { use std::process::Command; // Try IOPlatformUUID let output = Command::new("ioreg") .args(["-rd1", "-c", "IOPlatformExpertDevice"]) .output() .ok()?; if !output.status.success() { return None; } let stdout = String::from_utf8_lossy(&output.stdout); // Parse: "IOPlatformUUID" = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" for line in stdout.lines() { if line.contains("IOPlatformUUID") { if let Some(start) = line.find('"') { let rest = &line[start + 1..]; if let Some(end) = rest.find('"') { let uuid = &rest[..end]; // Skip the first quote if double-quoted let uuid = uuid.trim_start_matches('"'); if !uuid.is_empty() && uuid.len() > 20 { return Some(format!("mac-{}", uuid)); } } } } } None } /// Fallback for unsupported platforms #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] fn get_hardware_device_id() -> Option { None } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_device_id() { let id = get_device_id(); assert!(!id.is_empty()); println!("Device ID: {}", id); } #[test] fn test_generate_device_id() { let id1 = generate_device_id(); let id2 = generate_device_id(); assert_ne!(id1, id2); assert!(id1.len() >= 32); } }