librnxengine 1.1.0

implement robust software licensing, activation, and validation systems.
Documentation
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::process::Command;
use sysinfo::System;
use tracing::{debug, info};

/// Contains information about the system's hardware components.
///
/// This struct collects various hardware identifiers and system information
/// that are used to generate a unique fingerprint for the machine.
///
/// # Fields
/// - `cpu_id`: Unique identifier of the CPU
/// - `motherboard_id`: Unique identifier of the motherboard
/// - `mac_addresses`: List of MAC addresses from network interfaces
/// - `disk_serial`: Serial number of the primary disk drive
/// - `system_uuid`: Unique identifier of the system (SMBIOS UUID)
/// - `total_memory`: Total physical memory in bytes
/// - `os_name`: Operating system name (e.g., "Windows", "Linux")
/// - `os_version`: Operating system version string
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HardwareInfo {
    pub cpu_id: Option<String>,
    pub motherboard_id: Option<String>,
    pub mac_addresses: Vec<String>,
    pub disk_serial: Option<String>,
    pub system_uuid: Option<String>,
    pub total_memory: u64,
    pub os_name: String,
    pub os_version: String,
}

/// Generates hardware fingerprints for device identification and license binding.
///
/// This struct provides methods to collect hardware information and generate
/// a unique SHA-256 hash that identifies a specific machine. The fingerprint
/// is designed to be relatively stable across reboots but unique enough to
/// prevent license sharing between different machines.
///
/// # Security Considerations
/// - The fingerprint should not be used as a sole authentication mechanism
/// - Some hardware identifiers may change (e.g., network interfaces)
/// - Virtual machines may have identical or missing hardware identifiers
pub struct HardwareFingerprint;

impl HardwareFingerprint {
    /// Generates a hardware fingerprint for the current machine.
    ///
    /// Collects various hardware identifiers and computes a SHA-256 hash
    /// that uniquely identifies this system. The fingerprint can be used
    /// for license activation and device binding.
    ///
    /// # Returns
    /// - `Ok(String)`: Hexadecimal string of the SHA-256 hash
    /// - `Err(LicenseError)`: If hardware information cannot be collected
    ///
    /// # Example
    /// ```rust
    /// match HardwareFingerprint::generate() {
    ///     Ok(fingerprint) => println!("Hardware fingerprint: {}", fingerprint),
    ///     Err(e) => eprintln!("Failed to generate fingerprint: {}", e),
    /// }
    /// ```
    pub fn generate() -> Result<String, crate::error::LicenseError> {
        info!("Generating hardware fingerprint");

        // Initialize system information collector
        let mut system = System::new_all();
        system.refresh_all();

        // Collect hardware information from various sources
        let info = HardwareInfo {
            cpu_id: Self::get_cpu_id(&system),
            motherboard_id: Self::get_motherboard_id(),
            mac_addresses: Self::get_mac_addresses(),
            disk_serial: Self::get_disk_serial(),
            system_uuid: Self::get_system_uuid(),
            total_memory: system.total_memory(),
            os_name: sysinfo::System::name().unwrap_or_else(|| "unknown".to_string()),
            os_version: sysinfo::System::os_version().unwrap_or_else(|| "unknown".to_string()),
        };

        debug!("Hardware info collected: {:?}", info);

        // Calculate SHA-256 hash of the collected information
        let fingerprint = Self::calculate_fingerprint(&info);
        info!("Hardware fingerprint generated: {}", fingerprint);

        Ok(fingerprint)
    }

    /// Calculates SHA-256 hash from hardware information.
    ///
    /// Creates a deterministic hash by concatenating hardware identifiers
    /// in a specific order. The order is important to ensure consistent
    /// fingerprints across different runs.
    ///
    /// # Parameters
    /// - `info`: Collected hardware information
    ///
    /// # Returns
    /// Hexadecimal string representation of the SHA-256 hash
    ///
    /// # Algorithm
    /// 1. CPU ID (if available)
    /// 2. Motherboard ID (if available)
    /// 3. All MAC addresses (sorted by interface)
    /// 4. Disk serial number (if available)
    /// 5. System UUID (if available)
    /// 6. Total memory (big-endian bytes)
    /// 7. Operating system name
    /// 8. Operating system version
    fn calculate_fingerprint(info: &HardwareInfo) -> String {
        let mut hasher = Sha256::new();

        // Add CPU identifier (unique per physical CPU)
        if let Some(cpu_id) = &info.cpu_id {
            hasher.update(cpu_id.as_bytes());
        }

        // Add motherboard identifier (unique per system board)
        if let Some(mb_id) = &info.motherboard_id {
            hasher.update(mb_id.as_bytes());
        }

        // Add all MAC addresses (for network interface identification)
        for mac in &info.mac_addresses {
            hasher.update(mac.as_bytes());
        }

        // Add disk serial (unique per storage device)
        if let Some(disk) = &info.disk_serial {
            hasher.update(disk.as_bytes());
        }

        // Add system UUID (unique per system from SMBIOS)
        if let Some(uuid) = &info.system_uuid {
            hasher.update(uuid.as_bytes());
        }

        // Add total memory (helps differentiate similar systems)
        hasher.update(info.total_memory.to_be_bytes());

        // Add OS information (for virtualization detection)
        hasher.update(info.os_name.as_bytes());
        hasher.update(info.os_version.as_bytes());

        // Finalize hash and convert to hexadecimal
        hex::encode(hasher.finalize())
    }

    /// Retrieves CPU identifier using platform-specific methods.
    ///
    /// # Platform-specific implementations:
    /// - **Linux**: Reads from `/proc/cpuinfo` or uses CPU brand string
    /// - **Windows**: Uses WMI to query `Win32_Processor.ProcessorId`
    /// - **macOS**: Uses `sysctl` command to get CPU brand string
    ///
    /// # Parameters
    /// - `system`: Reference to initialized sysinfo::System
    ///
    /// # Returns
    /// - `Some(String)`: CPU identifier if available
    /// - `None`: If CPU information cannot be retrieved
    fn get_cpu_id(system: &System) -> Option<String> {
        #[cfg(target_os = "linux")]
        {
            use std::fs;
            // Try to read CPU serial from /proc/cpuinfo
            fs::read_to_string("/proc/cpuinfo")
                .ok()
                .and_then(|content| {
                    content
                        .lines()
                        .find(|line| line.to_lowercase().starts_with("serial"))
                        .map(|line| line.split(':').nth(1).unwrap_or("").trim().to_string())
                })
                // Fallback to CPU brand string if serial not available
                .or_else(|| system.cpus().get(0).map(|c| c.brand().to_string()))
        }

        #[cfg(target_os = "windows")]
        {
            use wmi::{COMLibrary, WMIConnection};
            // Use Windows Management Instrumentation (WMI) to query CPU info
            let com_con = COMLibrary::new().ok()?;
            let wmi_con = WMIConnection::new(com_con.into()).ok()?;
            let results: Vec<wmi::Variant> = wmi_con
                .raw_query("SELECT ProcessorId FROM Win32_Processor")
                .ok()?;
            results.get(0)?.as_string()
        }

        #[cfg(target_os = "macos")]
        {
            // Use sysctl command to get CPU information on macOS
            let output = Command::new("sysctl")
                .arg("-n")
                .arg("machdep.cpu.brand_string")
                .output()
                .ok()?;
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        }
    }

    /// Retrieves motherboard serial number using platform-specific methods.
    ///
    /// # Platform-specific implementations:
    /// - **Linux**: Reads from `/sys/class/dmi/id/board_serial`
    /// - **Windows**: Uses WMI to query `Win32_BaseBoard.SerialNumber`
    /// - **macOS**: Uses `ioreg` command to get board information
    ///
    /// # Returns
    /// - `Some(String)`: Motherboard serial number if available
    /// - `None`: If motherboard information cannot be retrieved
    fn get_motherboard_id() -> Option<String> {
        #[cfg(target_os = "linux")]
        {
            // Read motherboard serial from sysfs on Linux
            let output = Command::new("cat")
                .arg("/sys/class/dmi/id/board_serial")
                .output()
                .ok()?;
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        }

        #[cfg(target_os = "windows")]
        {
            use wmi::{COMLibrary, WMIConnection};
            // Query motherboard serial via WMI on Windows
            let com_con = COMLibrary::new().ok()?;
            let wmi_con = WMIConnection::new(com_con.into()).ok()?;
            let results: Vec<wmi::Variant> = wmi_con
                .raw_query("SELECT SerialNumber FROM Win32_BaseBoard")
                .ok()?;
            results.get(0)?.as_string()
        }

        #[cfg(target_os = "macos")]
        {
            // Use ioreg to get motherboard information on macOS
            let output = Command::new("ioreg").args(&["-l"]).output().ok()?;
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        }
    }

    /// Retrieves MAC addresses from all network interfaces.
    ///
    /// Uses `pnet_datalink` crate to enumerate network interfaces
    /// and collect their MAC addresses. Virtual interfaces are included.
    ///
    /// # Returns
    /// Vector of MAC addresses formatted as "xx:xx:xx:xx:xx:xx"
    ///
    /// # Note
    /// MAC addresses can change if network hardware is replaced or
    /// if virtualization creates new virtual interfaces.
    fn get_mac_addresses() -> Vec<String> {
        #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
        {
            use pnet_datalink::interfaces;

            interfaces()
                .into_iter()
                // Filter out interfaces without MAC addresses
                .filter_map(|iface| iface.mac)
                // Format MAC address to standard hex notation
                .map(|mac| {
                    format!(
                        "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
                        mac.0, mac.1, mac.2, mac.3, mac.4, mac.5
                    )
                })
                .collect::<Vec<String>>()
        }
    }

    /// Retrieves disk serial number using platform-specific methods.
    ///
    /// # Platform-specific implementations:
    /// - **Linux**: Uses `lsblk` command to get disk serial
    /// - **Windows**: Uses WMI to query `Win32_PhysicalMedia.SerialNumber`
    /// - **macOS**: Uses `system_profiler` to get storage information
    ///
    /// # Returns
    /// - `Some(String)`: Disk serial number if available
    /// - `None`: If disk information cannot be retrieved
    ///
    /// # Note
    /// In RAID configurations or virtual disks, this may return
    /// the controller serial or a virtual serial number.
    fn get_disk_serial() -> Option<String> {
        #[cfg(target_os = "linux")]
        {
            // Use lsblk command to get disk serial on Linux
            let output = Command::new("lsblk")
                .args(&["-o", "SERIAL", "-dn"])
                .output()
                .ok()?;
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        }

        #[cfg(target_os = "windows")]
        {
            use wmi::{COMLibrary, WMIConnection};
            // Query physical media serial numbers via WMI
            let com_con = COMLibrary::new().ok()?;
            let wmi_con = WMIConnection::new(com_con.into()).ok()?;
            let results: Vec<wmi::Variant> = wmi_con
                .raw_query("SELECT SerialNumber FROM Win32_PhysicalMedia")
                .ok()?;
            results.get(0)?.as_string()
        }

        #[cfg(target_os = "macos")]
        {
            // Use system_profiler to get storage information on macOS
            let output = Command::new("system_profiler")
                .args(&["SPStorageDataType"])
                .output()
                .ok()?;
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        }
    }

    /// Retrieves system UUID (SMBIOS UUID) using platform-specific methods.
    ///
    /// The system UUID is a unique identifier assigned by the system
    /// manufacturer and stored in SMBIOS.
    ///
    /// # Platform-specific implementations:
    /// - **Linux**: Reads from `/sys/class/dmi/id/product_uuid`
    /// - **Windows**: Uses WMI to query `Win32_ComputerSystemProduct.UUID`
    /// - **macOS**: Uses `ioreg` command to get platform UUID
    ///
    /// # Returns
    /// - `Some(String)`: System UUID if available
    /// - `None`: If UUID cannot be retrieved
    ///
    /// # Note
    /// In virtual machines, this may be a virtual UUID assigned
    /// by the hypervisor.
    fn get_system_uuid() -> Option<String> {
        #[cfg(target_os = "linux")]
        {
            // Read system UUID from sysfs on Linux
            let output = Command::new("cat")
                .arg("/sys/class/dmi/id/product_uuid")
                .output()
                .ok()?;
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        }

        #[cfg(target_os = "windows")]
        {
            use wmi::{COMLibrary, WMIConnection};
            // Query system UUID via WMI on Windows
            let com_con = COMLibrary::new().ok()?;
            let wmi_con = WMIConnection::new(com_con.into()).ok()?;
            let results: Vec<wmi::Variant> = wmi_con
                .raw_query("SELECT UUID FROM Win32_ComputerSystemProduct")
                .ok()?;
            results.get(0)?.as_string()
        }

        #[cfg(target_os = "macos")]
        {
            // Use ioreg to get platform UUID on macOS
            let output = Command::new("ioreg")
                .args(&["-rd1", "-c", "IOPlatformExpertDevice"])
                .output()
                .ok()?;
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        }
    }
}

// Note: The hardware fingerprint is designed to balance stability and uniqueness:
// - Stable: Should not change frequently (e.g., across reboots)
// - Unique: Should be different for different physical machines
//
// However, there are edge cases:
// 1. Virtual machines may have identical or missing hardware identifiers
// 2. Hardware upgrades (RAM, disks) may change the fingerprint
// 3. Network interface changes (adding/removing NICs) affect MAC addresses
// 4. Some systems may not expose certain identifiers (especially in cloud VMs)
//
// For these reasons, hardware fingerprinting should be used with grace periods
// and the ability for users to re-activate when legitimate hardware changes occur.

// Note: The fingerprint algorithm includes multiple components to increase
// robustness. If one component is missing (e.g., no motherboard serial),
// the fingerprint still works using other available components.

// Note: Platform-specific code is conditionally compiled using #[cfg]
// attributes. This ensures that only relevant code is compiled for
// each target operating system, reducing binary size and avoiding
// compilation errors on unsupported platforms.