Files
FootwearTest-20260602/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Services/MachineLicenseService.cs
2026-06-15 10:28:16 +08:00

447 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<LicenseData>(
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