更新密码数据123
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user