using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models; using Microsoft.Win32; using Serilog; using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Principal; using System.Text.Json; #pragma warning disable CA1416 namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services { public sealed class MachineLicenseService { public const string InstallArgument = "--install-license"; private const string RegistryPath = @"SOFTWARE\FootwearSlipResistance\License"; private const string RegistryInstallIdName = "InstallId"; private static readonly TimeSpan ClockRollbackTolerance = TimeSpan.FromMinutes(5); private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromMinutes(10); private static readonly byte[] AdditionalEntropy = "FootwearSlipResistance-License-v1"u8.ToArray(); private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; private LicenseData? current; public LicenseData? Current => current; public static string LicenseFilePath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "FootwearSlipResistance", "license.dat"); public LicenseCheckResult Check(bool updateHeartbeat = false) { var loadResult = TryLoad(out var data); if (loadResult is not null) { current = null; return loadResult; } current = data; var now = DateTime.UtcNow; if (now + ClockRollbackTolerance < data!.LastTrustedUtc) { return new LicenseCheckResult( LicenseCheckState.Tampered, data.Stage, GetExpiry(data), "检测到系统时间回退,软件已锁定,请联系管理员处理。"); } if (updateHeartbeat && now - data.LastTrustedUtc >= HeartbeatInterval) { try { current = data with { LastTrustedUtc = now }; SaveCurrent(); data = current; } catch (Exception ex) { Log.Warning(ex, "更新授权可信时间失败"); return new LicenseCheckResult( LicenseCheckState.Tampered, data.Stage, GetExpiry(data), "授权状态无法安全更新,软件已锁定,请联系管理员处理。"); } } var expiry = GetExpiry(data); if (expiry.HasValue && now >= expiry.Value) { return new LicenseCheckResult( LicenseCheckState.Expired, data.Stage, expiry, data.Stage == LicenseStage.FirstPeriod ? "第一阶段使用时效已到,请输入第一次时效密码。" : "第二阶段使用时效已到,请输入第二次时效密码。"); } return new LicenseCheckResult( LicenseCheckState.Valid, data.Stage, expiry, data.Stage == LicenseStage.Permanent ? "软件已永久授权。" : "软件授权有效。"); } public void Initialize( string adminPassword, string firstUnlockPassword, string secondUnlockPassword, int firstPeriodMonths, int secondPeriodMonths) { ValidateSettings(adminPassword, firstUnlockPassword, secondUnlockPassword, firstPeriodMonths, secondPeriodMonths); var now = DateTime.UtcNow; var data = new LicenseData( Guid.NewGuid().ToString("N"), LicenseStage.FirstPeriod, now, now, firstPeriodMonths, secondPeriodMonths, CreateSecret(adminPassword), CreateSecret(firstUnlockPassword), CreateSecret(secondUnlockPassword)); if (IsAdministrator()) { InstallInitialLicense(data); current = data; return; } var tempPath = Path.Combine(Path.GetTempPath(), $"footwear-license-{Guid.NewGuid():N}.dat"); try { File.WriteAllBytes(tempPath, Protect(data)); var executable = Environment.ProcessPath ?? throw new InvalidOperationException("无法确定当前程序路径。"); using var process = Process.Start(new ProcessStartInfo { FileName = executable, Arguments = $"{InstallArgument} \"{tempPath}\"", UseShellExecute = true, Verb = "runas" }) ?? throw new InvalidOperationException("无法启动授权初始化程序。"); process.WaitForExit(); if (process.ExitCode != 0) { throw new InvalidOperationException("授权初始化未完成或管理员权限确认被取消。"); } var result = Check(); if (!result.CanUseSoftware) { throw new InvalidOperationException(result.Message); } } finally { TryDelete(tempPath); } } public static int RunElevatedInstall(string packagePath) { try { if (!IsAdministrator()) { return 2; } var bytes = File.ReadAllBytes(packagePath); var data = Unprotect(bytes); InstallInitialLicense(data); return 0; } catch (Exception ex) { Log.Error(ex, "整机授权初始化失败"); return 1; } } public bool VerifyAdminPassword(string password) => EnsureCurrent() && VerifySecret(password, current!.AdminPassword); public bool UnlockCurrentStage(string password) { if (!EnsureCurrent() || current!.Stage == LicenseStage.Permanent) { return current?.Stage == LicenseStage.Permanent; } var secret = current.Stage == LicenseStage.FirstPeriod ? current.FirstUnlockPassword : current.SecondUnlockPassword; if (!VerifySecret(password, secret)) { return false; } var now = DateTime.UtcNow; current = current.Stage == LicenseStage.FirstPeriod ? current with { Stage = LicenseStage.SecondPeriod, StageStartedUtc = now, LastTrustedUtc = now } : current with { Stage = LicenseStage.Permanent, StageStartedUtc = now, LastTrustedUtc = now }; SaveCurrent(); return true; } public void UpdateSettings( string? newAdminPassword, string? newFirstUnlockPassword, string? newSecondUnlockPassword, int firstPeriodMonths, int secondPeriodMonths) { if (!EnsureCurrent()) { throw new InvalidOperationException("授权状态未加载。"); } ValidateMonths(firstPeriodMonths, secondPeriodMonths); current = current! with { FirstPeriodMonths = firstPeriodMonths, SecondPeriodMonths = secondPeriodMonths, AdminPassword = string.IsNullOrWhiteSpace(newAdminPassword) ? current.AdminPassword : CreateSecret(newAdminPassword), FirstUnlockPassword = string.IsNullOrWhiteSpace(newFirstUnlockPassword) ? current.FirstUnlockPassword : CreateSecret(newFirstUnlockPassword), SecondUnlockPassword = string.IsNullOrWhiteSpace(newSecondUnlockPassword) ? current.SecondUnlockPassword : CreateSecret(newSecondUnlockPassword), LastTrustedUtc = DateTime.UtcNow }; SaveCurrent(); } public void RestartTiming() { if (!EnsureCurrent()) { throw new InvalidOperationException("授权状态未加载。"); } var now = DateTime.UtcNow; current = current! with { Stage = LicenseStage.FirstPeriod, StageStartedUtc = now, LastTrustedUtc = now }; SaveCurrent(); } public string DescribeCurrent() { if (!EnsureCurrent()) { return "授权状态不可用"; } var expiry = GetExpiry(current!); var stageText = current!.Stage switch { LicenseStage.FirstPeriod => "第一阶段", LicenseStage.SecondPeriod => "第二阶段", _ => "永久授权" }; return expiry.HasValue ? $"{stageText};开始:{current.StageStartedUtc.ToLocalTime():yyyy-MM-dd HH:mm};到期:{expiry.Value.ToLocalTime():yyyy-MM-dd HH:mm}" : stageText; } private LicenseCheckResult? TryLoad(out LicenseData? data) { data = null; var registryInstallId = ReadRegistryInstallId(); var fileExists = File.Exists(LicenseFilePath); if (!fileExists && string.IsNullOrWhiteSpace(registryInstallId)) { return new LicenseCheckResult(LicenseCheckState.NotInitialized, LicenseStage.FirstPeriod, null, "首次使用,请先完成时效授权初始化。"); } if (!fileExists || string.IsNullOrWhiteSpace(registryInstallId)) { return new LicenseCheckResult(LicenseCheckState.Tampered, LicenseStage.FirstPeriod, null, "授权文件或整机安装标记缺失,软件已锁定。"); } try { data = Unprotect(File.ReadAllBytes(LicenseFilePath)); if (!string.Equals(data.InstallId, registryInstallId, StringComparison.Ordinal)) { data = null; return new LicenseCheckResult(LicenseCheckState.Tampered, LicenseStage.FirstPeriod, null, "授权文件与整机安装标记不匹配,软件已锁定。"); } return null; } catch (Exception ex) { Log.Warning(ex, "读取整机授权失败:Path={Path}", LicenseFilePath); return new LicenseCheckResult(LicenseCheckState.Tampered, LicenseStage.FirstPeriod, null, "授权文件损坏或无法读取,软件已锁定。"); } } private bool EnsureCurrent() { if (current is not null) { return true; } var result = Check(); return result.State is LicenseCheckState.Valid or LicenseCheckState.Expired; } private void SaveCurrent() { if (current is null) { return; } var directory = Path.GetDirectoryName(LicenseFilePath)!; Directory.CreateDirectory(directory); var tempPath = Path.Combine(directory, $"license-{Guid.NewGuid():N}.tmp"); File.WriteAllBytes(tempPath, Protect(current)); File.Move(tempPath, LicenseFilePath, true); } private static void InstallInitialLicense(LicenseData data) { var directory = Path.GetDirectoryName(LicenseFilePath)!; Directory.CreateDirectory(directory); File.WriteAllBytes(LicenseFilePath, Protect(data)); using var key = Registry.LocalMachine.CreateSubKey(RegistryPath, true) ?? throw new InvalidOperationException("无法创建整机授权注册表标记。"); key.SetValue(RegistryInstallIdName, data.InstallId, RegistryValueKind.String); if (OperatingSystem.IsWindows()) { using var permissions = Process.Start(new ProcessStartInfo { FileName = "icacls.exe", Arguments = $"\"{directory}\" /grant *S-1-5-32-545:(OI)(CI)M /T /C", UseShellExecute = false, CreateNoWindow = true }); permissions?.WaitForExit(); } } private static byte[] Protect(LicenseData data) => ProtectedData.Protect( JsonSerializer.SerializeToUtf8Bytes(data, JsonOptions), AdditionalEntropy, DataProtectionScope.LocalMachine); private static LicenseData Unprotect(byte[] protectedBytes) => JsonSerializer.Deserialize( ProtectedData.Unprotect(protectedBytes, AdditionalEntropy, DataProtectionScope.LocalMachine)) ?? throw new InvalidDataException("授权数据为空。"); private static string? ReadRegistryInstallId() { try { using var key = Registry.LocalMachine.OpenSubKey(RegistryPath, false); return key?.GetValue(RegistryInstallIdName) as string; } catch (Exception ex) { Log.Warning(ex, "读取整机授权注册表标记失败"); return null; } } private static PasswordSecret CreateSecret(string password) { ValidatePassword(password, "密码"); var salt = RandomNumberGenerator.GetBytes(16); var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, 100_000, HashAlgorithmName.SHA256, 32); return new PasswordSecret(Convert.ToBase64String(salt), Convert.ToBase64String(hash)); } private static bool VerifySecret(string password, PasswordSecret secret) { try { var salt = Convert.FromBase64String(secret.Salt); var expected = Convert.FromBase64String(secret.Hash); var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, 100_000, HashAlgorithmName.SHA256, expected.Length); return CryptographicOperations.FixedTimeEquals(actual, expected); } catch { return false; } } private static DateTime? GetExpiry(LicenseData data) => data.Stage switch { LicenseStage.FirstPeriod => data.StageStartedUtc.AddMonths(data.FirstPeriodMonths), LicenseStage.SecondPeriod => data.StageStartedUtc.AddMonths(data.SecondPeriodMonths), _ => null }; private static void ValidateSettings(string admin, string first, string second, int firstMonths, int secondMonths) { ValidatePassword(admin, "管理密码"); ValidatePassword(first, "第一次时效密码"); ValidatePassword(second, "第二次时效密码"); ValidateMonths(firstMonths, secondMonths); } private static void ValidatePassword(string password, string label) { if (string.IsNullOrWhiteSpace(password) || password.Length is < 4 or > 64) { throw new ArgumentException($"{label}长度必须为 4-64 个字符。"); } } private static void ValidateMonths(int firstMonths, int secondMonths) { if (firstMonths is < 1 or > 120 || secondMonths is < 1 or > 120) { throw new ArgumentException("两段时效必须为 1-120 个自然月。"); } } private static bool IsAdministrator() { if (!OperatingSystem.IsWindows()) { return false; } using var identity = WindowsIdentity.GetCurrent(); return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } } #pragma warning restore CA1416