feat(rust): add device fingerprint and selfhosted provider (cache, license verify, offline validation)

This commit is contained in:
2026-05-18 22:12:49 +08:00
parent f9203e077e
commit 8b90a71077
5 changed files with 631 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
use sha2::{Digest, Sha256};
#[derive(Debug, Clone)]
pub struct FingerprintLayer {
pub source: &'static str,
pub value: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DeviceFingerprint {
pub composite_hash: String,
pub stability_score: u8,
pub layers: Vec<FingerprintLayer>,
}
pub fn collect() -> DeviceFingerprint {
let mut layers = Vec::new();
let l1 = try_dmi_uuid();
layers.push(FingerprintLayer { source: "hw_uuid", value: l1 });
let l2 = try_machine_id();
layers.push(FingerprintLayer { source: "os_id", value: l2 });
let l3 = try_rootfs_uuid();
layers.push(FingerprintLayer { source: "fs_uuid", value: l3 });
let l4 = Some(try_physical_mac());
layers.push(FingerprintLayer { source: "mac", value: l4 });
let mut hasher = Sha256::new();
for layer in &layers {
if let Some(ref v) = layer.value {
hasher.update(v.as_bytes());
}
hasher.update(b"|");
}
let hash = format!("HC-{:x}", hasher.finalize());
let stability = compute_stability(&layers);
DeviceFingerprint {
composite_hash: hash,
stability_score: stability,
layers,
}
}
fn compute_stability(layers: &[FingerprintLayer]) -> u8 {
let mut score: u8 = 0;
if layers[0].value.is_some() { score += 40; }
if layers[1].value.is_some() { score += 30; }
if layers[2].value.is_some() { score += 20; }
if layers[3].value.is_some() { score += 10; }
score
}
#[cfg(target_os = "linux")]
fn try_dmi_uuid() -> Option<String> {
std::fs::read_to_string("/sys/class/dmi/id/product_uuid")
.map(|s| s.trim().to_string())
.ok()
.filter(|s| !s.is_empty() && s != "00000000-0000-0000-0000-000000000000")
}
#[cfg(not(target_os = "linux"))]
fn try_dmi_uuid() -> Option<String> {
None
}
#[cfg(target_os = "linux")]
fn try_machine_id() -> Option<String> {
std::fs::read_to_string("/etc/machine-id")
.or_else(|_| std::fs::read_to_string("/var/lib/dbus/machine-id"))
.map(|s| s.trim().to_string())
.ok()
.filter(|s| !s.is_empty())
}
#[cfg(not(target_os = "linux"))]
fn try_machine_id() -> Option<String> {
None
}
#[cfg(target_os = "linux")]
fn try_rootfs_uuid() -> Option<String> {
std::fs::read_to_string("/proc/cmdline")
.ok()
.and_then(|cmdline| {
for part in cmdline.split_whitespace() {
if part.starts_with("root=") {
return Some(part.trim_start_matches("root=").to_string());
}
}
None
})
}
#[cfg(not(target_os = "linux"))]
fn try_rootfs_uuid() -> Option<String> {
None
}
fn try_physical_mac() -> String {
#[cfg(target_os = "linux")]
{
let mut macs: Vec<String> = Vec::new();
if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
for entry in entries.flatten() {
let iface = entry.file_name().to_string_lossy().to_string();
if iface == "lo" || iface.starts_with("docker") || iface.starts_with("veth") || iface.starts_with("tap") {
continue;
}
if let Ok(addr) = std::fs::read_to_string(entry.path().join("address")) {
let a = addr.trim().to_string();
if !a.is_empty() && a != "00:00:00:00:00:00" {
macs.push(a);
}
}
}
}
if macs.is_empty() { "unknown-mac".to_string() } else { macs.sort(); macs.join(",") }
}
#[cfg(not(target_os = "linux"))]
{ "unknown-mac".to_string() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_returns_valid_structure() {
let fp = collect();
assert!(fp.composite_hash.starts_with("HC-"));
assert_eq!(fp.layers.len(), 4);
for layer in &fp.layers {
assert!(matches!(layer.source, "hw_uuid" | "os_id" | "fs_uuid" | "mac"));
}
}
#[test]
fn test_stability_score_never_exceeds_100() {
let fp = collect();
assert!(fp.stability_score <= 100);
}
#[test]
fn test_composite_hash_deterministic() {
let fp1 = collect();
let fp2 = collect();
assert_eq!(fp1.composite_hash, fp2.composite_hash);
}
}