49725ddad5
Self-hosted, multitenant licensing platform for on-prem software. - Backend: .NET 10 Clean Architecture (Domain/Application/Infrastructure/Api), PostgreSQL 16 with ltree company tree, Ed25519 signing (NSec), cookie auth with Argon2id, role enforcement (Operator/Owner/Admin/Viewer over subtree), webhooks, audit log, rate limiting. - Panel: Next.js 15 enterprise admin (RU/EN/TG), Ofarandagon light theme, command palette, license issue wizard, server-side pagination. - Landing: Next.js 15 static export, awwwards-grade, RU/EN/TG + full SEO. - Client SDK (CertifiEd.Client) with offline Ed25519 validation, heartbeat, feature gating; QuickStart example; usage docs.
125 lines
3.9 KiB
C#
125 lines
3.9 KiB
C#
using System.Net.NetworkInformation;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace CertifiEd.Client;
|
|
|
|
/// <summary>
|
|
/// Computes a stable hardware fingerprint for the current machine — a SHA-256 hex
|
|
/// digest over machine name, OS description, the first stable MAC address, and the
|
|
/// platform machine id when available.
|
|
/// </summary>
|
|
public static class HwFingerprint
|
|
{
|
|
private static string? _cached;
|
|
|
|
/// <summary>Returns the fingerprint, computed once per process.</summary>
|
|
public static string Get()
|
|
{
|
|
if (_cached is not null)
|
|
return _cached;
|
|
|
|
var raw = new StringBuilder()
|
|
.Append(Environment.MachineName).Append('|')
|
|
.Append(RuntimeInformation.OSDescription).Append('|')
|
|
.Append(GetStableMac() ?? "no-mac");
|
|
|
|
var platformId = GetPlatformMachineId();
|
|
if (platformId is not null)
|
|
raw.Append('|').Append(platformId);
|
|
|
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(raw.ToString()));
|
|
_cached = Convert.ToHexStringLower(hash);
|
|
return _cached;
|
|
}
|
|
|
|
private static string? GetStableMac()
|
|
{
|
|
try
|
|
{
|
|
return NetworkInterface.GetAllNetworkInterfaces()
|
|
.Where(n => n.NetworkInterfaceType != NetworkInterfaceType.Loopback
|
|
&& n.OperationalStatus == OperationalStatus.Up)
|
|
.Select(n => n.GetPhysicalAddress().ToString())
|
|
.Where(mac => mac.Length > 0)
|
|
.Order(StringComparer.Ordinal)
|
|
.FirstOrDefault();
|
|
}
|
|
catch (NetworkInformationException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string? GetPlatformMachineId()
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
return GetWindowsMachineGuid();
|
|
|
|
if (OperatingSystem.IsLinux())
|
|
return TryReadFile("/etc/machine-id") ?? TryReadFile("/var/lib/dbus/machine-id");
|
|
|
|
if (OperatingSystem.IsMacOS())
|
|
return GetMacSerialNumber();
|
|
|
|
return null;
|
|
}
|
|
|
|
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
|
private static string? GetWindowsMachineGuid()
|
|
{
|
|
try
|
|
{
|
|
using var key = Microsoft.Win32.Registry.LocalMachine
|
|
.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
|
|
return key?.GetValue("MachineGuid")?.ToString();
|
|
}
|
|
catch (Exception ex) when (ex is System.Security.SecurityException or UnauthorizedAccessException or IOException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string? TryReadFile(string path)
|
|
{
|
|
try
|
|
{
|
|
return File.Exists(path) ? File.ReadAllText(path).Trim() : null;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return null;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string? GetMacSerialNumber()
|
|
{
|
|
try
|
|
{
|
|
var psi = new System.Diagnostics.ProcessStartInfo("ioreg", "-l")
|
|
{
|
|
RedirectStandardOutput = true,
|
|
UseShellExecute = false,
|
|
};
|
|
using var process = System.Diagnostics.Process.Start(psi);
|
|
var output = process?.StandardOutput.ReadToEnd() ?? string.Empty;
|
|
var line = output.Split('\n').FirstOrDefault(l => l.Contains("IOPlatformSerialNumber", StringComparison.Ordinal));
|
|
if (line is null)
|
|
return null;
|
|
|
|
var start = line.IndexOf('"', line.IndexOf('=', StringComparison.Ordinal) + 1);
|
|
var end = start >= 0 ? line.IndexOf('"', start + 1) : -1;
|
|
return start >= 0 && end > start ? line[(start + 1)..end] : null;
|
|
}
|
|
catch (Exception ex) when (ex is System.ComponentModel.Win32Exception or InvalidOperationException or IOException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|