feat: initial CertifiEd licensing platform
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.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user