更新密码数据123

This commit is contained in:
GukSang.Jin
2026-06-15 10:28:16 +08:00
parent 2fc1dd89a2
commit 147ab67ea8
9 changed files with 1038 additions and 10 deletions

View File

@@ -0,0 +1,446 @@
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