use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::process::Command;
use sysinfo::System;
use tracing::{debug, info};
#[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,
}
pub struct HardwareFingerprint;
impl HardwareFingerprint {
pub fn generate() -> Result<String, crate::error::LicenseError> {
info!("Generating hardware fingerprint");
let mut system = System::new_all();
system.refresh_all();
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);
let fingerprint = Self::calculate_fingerprint(&info);
info!("Hardware fingerprint generated: {}", fingerprint);
Ok(fingerprint)
}
fn calculate_fingerprint(info: &HardwareInfo) -> String {
let mut hasher = Sha256::new();
if let Some(cpu_id) = &info.cpu_id {
hasher.update(cpu_id.as_bytes());
}
if let Some(mb_id) = &info.motherboard_id {
hasher.update(mb_id.as_bytes());
}
for mac in &info.mac_addresses {
hasher.update(mac.as_bytes());
}
if let Some(disk) = &info.disk_serial {
hasher.update(disk.as_bytes());
}
if let Some(uuid) = &info.system_uuid {
hasher.update(uuid.as_bytes());
}
hasher.update(info.total_memory.to_be_bytes());
hasher.update(info.os_name.as_bytes());
hasher.update(info.os_version.as_bytes());
hex::encode(hasher.finalize())
}
fn get_cpu_id(system: &System) -> Option<String> {
#[cfg(target_os = "linux")]
{
use std::fs;
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())
})
.or_else(|| system.cpus().get(0).map(|c| c.brand().to_string()))
}
#[cfg(target_os = "windows")]
{
use wmi::{COMLibrary, WMIConnection};
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")]
{
let output = Command::new("sysctl")
.arg("-n")
.arg("machdep.cpu.brand_string")
.output()
.ok()?;
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
}
fn get_motherboard_id() -> Option<String> {
#[cfg(target_os = "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};
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")]
{
let output = Command::new("ioreg").args(&["-l"]).output().ok()?;
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
}
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_map(|iface| iface.mac)
.map(|mac| {
format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac.0, mac.1, mac.2, mac.3, mac.4, mac.5
)
})
.collect::<Vec<String>>()
}
}
fn get_disk_serial() -> Option<String> {
#[cfg(target_os = "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};
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")]
{
let output = Command::new("system_profiler")
.args(&["SPStorageDataType"])
.output()
.ok()?;
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
}
fn get_system_uuid() -> Option<String> {
#[cfg(target_os = "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};
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")]
{
let output = Command::new("ioreg")
.args(&["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
.ok()?;
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
}
}