447 lines
17 KiB
C#
447 lines
17 KiB
C#
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
|