use sha2::{Digest, Sha256};
#[derive(Debug, Clone, crate::serialization::Encode, crate::serialization::Decode, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
pub struct Signature {
pub platform: String,
pub system_serial: Option<String>,
pub hardware_uuid: Option<String>,
pub board_serial: Option<String>,
pub disk_serial: Option<String>,
pub fingerprint: [u8; 32],
}
impl Signature {
pub fn fingerprint_hex(&self) -> String {
self.fingerprint.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::new();
let push = |buf: &mut Vec<u8>, v: &Option<String>| {
if let Some(s) = v {
buf.extend_from_slice(s.as_bytes());
}
buf.push(0u8);
};
buf.extend_from_slice(self.platform.as_bytes());
buf.push(0u8);
push(&mut buf, &self.system_serial);
push(&mut buf, &self.hardware_uuid);
push(&mut buf, &self.board_serial);
push(&mut buf, &self.disk_serial);
buf.extend_from_slice(&self.fingerprint);
buf
}
pub fn from_bytes(data: &[u8]) -> Option<Self> {
let mut fields: Vec<&[u8]> = Vec::new();
let mut start = 0usize;
for (i, &b) in data.iter().enumerate() {
if b == 0 {
fields.push(&data[start..i]);
start = i + 1;
if fields.len() == 5 {
break;
}
}
}
if fields.len() < 5 {
return None;
}
if data.len() < start + 32 {
return None;
}
let fingerprint: [u8; 32] = data[start..start + 32].try_into().ok()?;
let to_opt = |b: &[u8]| -> Option<String> {
if b.is_empty() {
None
} else {
String::from_utf8(b.to_vec()).ok()
}
};
let platform_str = std::str::from_utf8(fields[0]).ok()?;
let platform: &'static str = match platform_str {
"macos" => "macos",
"windows" => "windows",
"linux" => "linux",
_ => "unknown",
};
Some(Signature {
platform: platform.to_string(),
system_serial: to_opt(fields[1]),
hardware_uuid: to_opt(fields[2]),
board_serial: to_opt(fields[3]),
disk_serial: to_opt(fields[4]),
fingerprint,
})
}
pub fn verify(&self) -> bool {
let current = extract();
let mismatch = self.fingerprint
.iter()
.zip(current.fingerprint.iter())
.fold(0u8, |acc, (a, b)| acc | (a ^ b));
mismatch == 0
}
}
impl PartialEq for Signature {
fn eq(&self, other: &Self) -> bool {
let mismatch = self.fingerprint
.iter()
.zip(other.fingerprint.iter())
.fold(0u8, |acc, (a, b)| acc | (a ^ b));
mismatch == 0
}
}
impl Eq for Signature {}
pub fn extract() -> Signature {
extract_impl()
}
#[cfg(target_os = "macos")]
fn extract_impl() -> Signature {
let hw = run("system_profiler", &["SPHardwareDataType"]);
let system_serial = parse_colon_value(&hw, "Serial Number (system)");
let hardware_uuid = parse_colon_value(&hw, "Hardware UUID");
let nvme = run("system_profiler", &["SPNVMeDataType"]);
let sata = run("system_profiler", &["SPSerialATADataType"]);
let disk_serial = parse_colon_value(&nvme, "Serial Number")
.or_else(|| parse_colon_value(&sata, "Serial Number"));
let fingerprint = fingerprint("macos", &system_serial, &hardware_uuid, &None, &disk_serial);
Signature {
platform: "macos".to_string(),
system_serial,
hardware_uuid,
board_serial: None,
disk_serial,
fingerprint,
}
}
#[cfg(target_os = "windows")]
fn extract_impl() -> Signature {
let system_serial = wmic_get("bios", "SerialNumber")
.or_else(|| ps_get("(Get-WmiObject Win32_BIOS).SerialNumber"));
let hardware_uuid = wmic_get("csproduct", "UUID")
.or_else(|| ps_get("(Get-WmiObject Win32_ComputerSystemProduct).UUID"));
let board_serial = wmic_get("baseboard", "SerialNumber")
.or_else(|| ps_get("(Get-WmiObject Win32_BaseBoard).SerialNumber"));
let disk_serial = wmic_disk()
.or_else(|| ps_get("(Get-WmiObject Win32_DiskDrive | Select-Object -First 1).SerialNumber"));
let fingerprint = fingerprint("windows", &system_serial, &hardware_uuid, &board_serial, &disk_serial);
Signature {
platform: "windows".to_string(),
system_serial,
hardware_uuid,
board_serial,
disk_serial,
fingerprint,
}
}
#[cfg(target_os = "linux")]
fn extract_impl() -> Signature {
let dmi = |name: &str| read_file(&format!("/sys/class/dmi/id/{name}"));
let hardware_uuid = dmi("product_uuid");
let system_serial = dmi("product_serial");
let board_serial = dmi("board_serial");
let disk_serial = linux_disk_serial();
let fingerprint = fingerprint("linux", &system_serial, &hardware_uuid, &board_serial, &disk_serial);
Signature {
platform: "linux".to_string(),
system_serial,
hardware_uuid,
board_serial,
disk_serial,
fingerprint,
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn extract_impl() -> Signature {
let fp = fingerprint("unknown", &None, &None, &None, &None);
Signature {
platform: "unknown".to_string(),
system_serial: None,
hardware_uuid: None,
board_serial: None,
disk_serial: None,
fingerprint: fp,
}
}
fn fingerprint(
platform: &str,
system_serial: &Option<String>,
hardware_uuid: &Option<String>,
board_serial: &Option<String>,
disk_serial: &Option<String>,
) -> [u8; 32] {
let mut h = Sha256::new();
let feed = |h: &mut Sha256, v: &Option<String>| {
if let Some(s) = v {
h.update(s.as_bytes());
}
h.update(b"\0");
};
h.update(platform.as_bytes());
h.update(b"\0");
feed(&mut h, system_serial);
feed(&mut h, hardware_uuid);
feed(&mut h, board_serial);
feed(&mut h, disk_serial);
h.finalize().into()
}
#[cfg(target_os = "windows")]
fn wmic_get(alias: &str, field: &str) -> Option<String> {
let out = run("wmic", &[alias, "get", field, "/value"]);
for line in out.lines() {
if let Some(rest) = line.split_once('=') {
let v = clean(rest.1);
if v.is_some() {
return v;
}
}
}
None
}
#[cfg(target_os = "windows")]
fn wmic_disk() -> Option<String> {
let out = run("wmic", &["diskdrive", "get", "SerialNumber", "/value"]);
for line in out.lines() {
if let Some(rest) = line.split_once('=') {
let v = clean(rest.1);
if v.is_some() {
return v;
}
}
}
None
}
#[cfg(target_os = "windows")]
fn ps_get(script: &str) -> Option<String> {
let out = run("powershell", &["-NoProfile", "-NonInteractive", "-Command", script]);
clean(&out)
}
#[cfg(target_os = "linux")]
fn linux_disk_serial() -> Option<String> {
let Ok(entries) = std::fs::read_dir("/sys/block") else {
return None;
};
let mut names: Vec<_> = entries.filter_map(|e| e.ok()).collect();
names.sort_by_key(|e| e.file_name());
for entry in names {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("loop")
|| name.starts_with("ram")
|| name.starts_with("zram")
|| name.starts_with("dm-")
{
continue;
}
let path = entry.path().join("device/serial");
if let Some(s) = read_file(&path.to_string_lossy()) {
return Some(s);
}
}
None
}
fn run(program: &str, args: &[&str]) -> String {
std::process::Command::new(program)
.args(args)
.stderr(std::process::Stdio::null())
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.unwrap_or_default()
}
#[cfg(target_os = "macos")]
fn parse_colon_value(text: &str, key: &str) -> Option<String> {
for line in text.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix(key) {
if let Some(rest) = rest.trim_start().strip_prefix(':') {
return clean(rest);
}
}
}
None
}
fn read_file(path: &str) -> Option<String> {
std::fs::read_to_string(path)
.ok()
.as_deref()
.and_then(clean)
}
fn clean(s: &str) -> Option<String> {
let v = s.trim().to_uppercase();
if v.is_empty() {
return None;
}
const PLACEHOLDERS: &[&str] = &[
"N/A",
"NA",
"NONE",
"NOT AVAILABLE",
"NOT SPECIFIED",
"TO BE FILLED BY O.E.M.",
"TO BE FILLED BY OEM",
"DEFAULT STRING",
"UNDEFINED",
"UNKNOWN",
"00000000",
"0000000000",
"FFFFFFFF",
"FFFFFFFFFFFFFFFF",
];
if PLACEHOLDERS.contains(&v.as_str()) {
return None;
}
Some(v)
}
#[cfg(feature = "backend-deps")]
pub mod backend_deps;