349 lines
11 KiB
C#
349 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.NetworkInformation;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.Win32;
|
|
|
|
namespace Deploymentcenter.Client;
|
|
|
|
public class HardwareIdResult
|
|
{
|
|
public string HardwareId { get; set; } = string.Empty;
|
|
public int HwidVersion { get; set; } = 2;
|
|
public string HwidSource { get; set; } = string.Empty;
|
|
public string Platform { get; set; } = string.Empty;
|
|
public string LegacyHardwareId { get; set; } = string.Empty;
|
|
}
|
|
|
|
public static class HardwareId
|
|
{
|
|
private static readonly string[] MacStopWords = new[]
|
|
{
|
|
"docker", "veth", "br-", "virbr", "cni", "flannel", "cali", "weave",
|
|
"zt", "tailscale", "ipsec", "sit", "gre", "dummy", "bond", "macvlan", "ovs"
|
|
};
|
|
|
|
public static HardwareIdResult GetHardwareId(string productSlug = "default_app")
|
|
{
|
|
string platform = OperatingSystemHelpers.GetPlatformCode();
|
|
string rawValue = string.Empty;
|
|
string source = string.Empty;
|
|
|
|
// 3.1 Override (all platforms, highest priority)
|
|
var overrideValue = LicenseConfig.HardwareIdOverride;
|
|
if (!string.IsNullOrWhiteSpace(overrideValue))
|
|
{
|
|
rawValue = overrideValue!.Trim();
|
|
source = "override";
|
|
}
|
|
else if (OperatingSystemHelpers.IsWindows())
|
|
{
|
|
// 3.2 Windows Sources
|
|
string? machineGuid = GetWindowsMachineGuid();
|
|
if (IsPlausibleMachineId(machineGuid))
|
|
{
|
|
rawValue = machineGuid!;
|
|
source = "machine-guid";
|
|
}
|
|
else
|
|
{
|
|
string? macs = GetStablePhysicalMacs();
|
|
if (!string.IsNullOrWhiteSpace(macs))
|
|
{
|
|
rawValue = macs!;
|
|
source = "mac";
|
|
}
|
|
else
|
|
{
|
|
rawValue = GetOrCreateKeyfile(productSlug);
|
|
source = "keyfile";
|
|
}
|
|
}
|
|
}
|
|
else if (OperatingSystemHelpers.IsLinux())
|
|
{
|
|
// 3.3 Linux Sources
|
|
string? systemdId = ReadTextFileFile("/etc/machine-id");
|
|
if (IsPlausibleMachineId(systemdId))
|
|
{
|
|
rawValue = systemdId!;
|
|
source = "machine-id";
|
|
}
|
|
else
|
|
{
|
|
string? dbusId = ReadTextFileFile("/var/lib/dbus/machine-id");
|
|
if (IsPlausibleMachineId(dbusId))
|
|
{
|
|
rawValue = dbusId!;
|
|
source = "dbus-machine-id";
|
|
}
|
|
else
|
|
{
|
|
string? dmiUuid = ReadTextFileFile("/sys/class/dmi/id/product_uuid");
|
|
if (IsPlausibleMachineId(dmiUuid))
|
|
{
|
|
rawValue = dmiUuid!;
|
|
source = "dmi-uuid";
|
|
}
|
|
else
|
|
{
|
|
string? macs = GetStablePhysicalMacs();
|
|
if (!string.IsNullOrWhiteSpace(macs))
|
|
{
|
|
rawValue = macs!;
|
|
source = "mac";
|
|
}
|
|
else
|
|
{
|
|
rawValue = GetOrCreateKeyfile(productSlug);
|
|
source = "keyfile";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Generic fallback
|
|
string? macs = GetStablePhysicalMacs();
|
|
if (!string.IsNullOrWhiteSpace(macs))
|
|
{
|
|
rawValue = macs!;
|
|
source = "mac";
|
|
}
|
|
else
|
|
{
|
|
rawValue = GetOrCreateKeyfile(productSlug);
|
|
source = "keyfile";
|
|
}
|
|
}
|
|
|
|
string v2Hash = ComputeV2Hash(platform, source, rawValue);
|
|
string v2HardwareId = $"2:{platform}:{v2Hash}";
|
|
string legacyHardwareId = GetLegacyHardwareId();
|
|
|
|
return new HardwareIdResult
|
|
{
|
|
HardwareId = v2HardwareId,
|
|
HwidVersion = 2,
|
|
HwidSource = source,
|
|
Platform = platform,
|
|
LegacyHardwareId = legacyHardwareId
|
|
};
|
|
}
|
|
|
|
public static string ComputeV2Hash(string platform, string source, string rawValue)
|
|
{
|
|
string domainString = "LicenseLabrador-HWID-v2";
|
|
string payload = $"{domainString}\n{platform}\n{source}\n{rawValue}";
|
|
|
|
using var sha256 = SHA256.Create();
|
|
byte[] bytes = Encoding.UTF8.GetBytes(payload);
|
|
byte[] hash = sha256.ComputeHash(bytes);
|
|
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
|
}
|
|
|
|
public static string GetLegacyHardwareId()
|
|
{
|
|
// Legacy v1 algorithm: SHA256 of MachineName + First MAC
|
|
string firstMac = GetFirstPhysicalMacLegacy();
|
|
string raw = $"{Environment.MachineName}:{firstMac}";
|
|
|
|
using var sha256 = SHA256.Create();
|
|
byte[] bytes = Encoding.UTF8.GetBytes(raw);
|
|
byte[] hash = sha256.ComputeHash(bytes);
|
|
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
|
}
|
|
|
|
public static bool IsPlausibleMachineId(string? v)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(v))
|
|
return false;
|
|
|
|
string trimmed = v!.Trim();
|
|
if (trimmed.Length < 16)
|
|
return false;
|
|
|
|
if (trimmed.Equals("uninitialized", StringComparison.OrdinalIgnoreCase))
|
|
return false;
|
|
|
|
if (trimmed.Trim('0').Length == 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private static string? GetWindowsMachineGuid()
|
|
{
|
|
if (!OperatingSystemHelpers.IsWindows())
|
|
return null;
|
|
|
|
try
|
|
{
|
|
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
|
using var subKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
|
|
return subKey?.GetValue("MachineGuid")?.ToString();
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string? GetStablePhysicalMacs()
|
|
{
|
|
try
|
|
{
|
|
var validMacs = new List<string>();
|
|
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
|
|
|
foreach (var ni in interfaces)
|
|
{
|
|
if (ni.OperationalStatus != OperationalStatus.Up && ni.OperationalStatus != OperationalStatus.Unknown)
|
|
continue;
|
|
|
|
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback ||
|
|
ni.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
|
|
continue;
|
|
|
|
string name = ni.Name.ToLowerInvariant();
|
|
string desc = ni.Description.ToLowerInvariant();
|
|
|
|
if (MacStopWords.Any(sw => name.Contains(sw) || desc.Contains(sw)))
|
|
continue;
|
|
|
|
var physAddr = ni.GetPhysicalAddress();
|
|
var bytes = physAddr.GetAddressBytes();
|
|
if (bytes.Length == 0)
|
|
continue;
|
|
|
|
// Check locally administered bit (mac[0] & 0x02) != 0
|
|
if ((bytes[0] & 0x02) != 0)
|
|
continue;
|
|
|
|
// On Linux, check /sys/class/net/<name>/device
|
|
if (OperatingSystemHelpers.IsLinux())
|
|
{
|
|
string sysDevicePath = $"/sys/class/net/{ni.Name}/device";
|
|
if (!Directory.Exists(sysDevicePath) && !File.Exists(sysDevicePath))
|
|
continue;
|
|
}
|
|
|
|
string macHex = BitConverter.ToString(bytes).Replace("-", "").ToUpperInvariant();
|
|
if (!string.IsNullOrWhiteSpace(macHex) && macHex.Trim('0').Length > 0)
|
|
{
|
|
validMacs.Add(macHex);
|
|
}
|
|
}
|
|
|
|
if (validMacs.Count == 0)
|
|
return null;
|
|
|
|
validMacs.Sort();
|
|
return string.Join(":", validMacs);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string GetFirstPhysicalMacLegacy()
|
|
{
|
|
try
|
|
{
|
|
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
|
foreach (var ni in interfaces)
|
|
{
|
|
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback)
|
|
continue;
|
|
|
|
var bytes = ni.GetPhysicalAddress().GetAddressBytes();
|
|
if (bytes.Length > 0)
|
|
{
|
|
return BitConverter.ToString(bytes).Replace("-", "").ToUpperInvariant();
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
return "000000000000";
|
|
}
|
|
|
|
private static string? ReadTextFileFile(string path)
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(path))
|
|
return null;
|
|
return File.ReadAllText(path).Trim();
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string GetOrCreateKeyfile(string productSlug)
|
|
{
|
|
string dir = LicenseConfig.GetStorageDirectory(productSlug);
|
|
Directory.CreateDirectory(dir);
|
|
string keyfilePath = Path.Combine(dir, "machine.key");
|
|
|
|
if (File.Exists(keyfilePath))
|
|
{
|
|
try
|
|
{
|
|
string existing = File.ReadAllText(keyfilePath).Trim();
|
|
if (!string.IsNullOrWhiteSpace(existing))
|
|
return existing;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
byte[] randomBytes = new byte[32];
|
|
using (var rng = RandomNumberGenerator.Create())
|
|
{
|
|
rng.GetBytes(randomBytes);
|
|
}
|
|
string keyBase64 = Convert.ToBase64String(randomBytes);
|
|
|
|
File.WriteAllText(keyfilePath, keyBase64, Encoding.UTF8);
|
|
|
|
// Set 0600 permissions on Linux/macOS
|
|
SetUnixPermissions(keyfilePath, "600");
|
|
|
|
return keyBase64;
|
|
}
|
|
|
|
private static void SetUnixPermissions(string filePath, string mode)
|
|
{
|
|
if (OperatingSystemHelpers.IsWindows())
|
|
return;
|
|
|
|
#if NET8_0_OR_GREATER
|
|
try
|
|
{
|
|
File.SetUnixFileMode(filePath, UnixFileMode.UserRead | UnixFileMode.UserWrite);
|
|
}
|
|
catch { }
|
|
#else
|
|
try
|
|
{
|
|
var proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = "chmod",
|
|
Arguments = $"{mode} \"{filePath}\"",
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
});
|
|
proc?.WaitForExit();
|
|
}
|
|
catch { }
|
|
#endif
|
|
}
|
|
}
|