diff --git a/LanMountainDesktop.Launcher/AppJsonContext.cs b/LanMountainDesktop.Launcher/AppJsonContext.cs index cb8571e..40d9814 100644 --- a/LanMountainDesktop.Launcher/AppJsonContext.cs +++ b/LanMountainDesktop.Launcher/AppJsonContext.cs @@ -39,4 +39,6 @@ namespace LanMountainDesktop.Launcher; [JsonSerializable(typeof(GitHubAsset))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(StartupAttemptRecord))] +[JsonSerializable(typeof(PrivacyConfig))] +[JsonSerializable(typeof(PrivacyAgreementState))] internal sealed partial class AppJsonContext : JsonSerializerContext; diff --git a/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj b/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj index 382404a..7b5f45f 100644 --- a/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj +++ b/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj @@ -22,12 +22,12 @@ - - - - - - + + + + + + diff --git a/LanMountainDesktop.Launcher/Models/PrivacyAgreementState.cs b/LanMountainDesktop.Launcher/Models/PrivacyAgreementState.cs new file mode 100644 index 0000000..4fac668 --- /dev/null +++ b/LanMountainDesktop.Launcher/Models/PrivacyAgreementState.cs @@ -0,0 +1,42 @@ +namespace LanMountainDesktop.Launcher.Models; + +/// +/// 隐私协议同意状态模型(带防篡改保护) +/// +public class PrivacyAgreementState +{ + /// + /// 用户是否同意隐私协议 + /// + public bool IsAgreed { get; set; } = false; + + /// + /// 同意时间(UTC) + /// + public DateTime AgreedAtUtc { get; set; } + + /// + /// 同意的协议版本 + /// + public string AgreementVersion { get; set; } = "1.0"; + + /// + /// 用户标识(匿名) + /// + public string UserId { get; set; } = string.Empty; + + /// + /// 设备标识 + /// + public string DeviceId { get; set; } = string.Empty; + + /// + /// 数据完整性校验哈希(HMAC-SHA256) + /// + public string IntegrityHash { get; set; } = string.Empty; + + /// + /// 用于生成哈希的随机盐值 + /// + public string Salt { get; set; } = string.Empty; +} diff --git a/LanMountainDesktop.Launcher/Models/PrivacyConfig.cs b/LanMountainDesktop.Launcher/Models/PrivacyConfig.cs new file mode 100644 index 0000000..b52ae9c --- /dev/null +++ b/LanMountainDesktop.Launcher/Models/PrivacyConfig.cs @@ -0,0 +1,22 @@ +namespace LanMountainDesktop.Launcher.Models; + +/// +/// 隐私配置模型 +/// +public class PrivacyConfig +{ + /// + /// 是否启用崩溃报告遥测 + /// + public bool CrashTelemetryEnabled { get; set; } = true; + + /// + /// 是否启用使用统计遥测 + /// + public bool UsageTelemetryEnabled { get; set; } = true; + + /// + /// 隐私追踪 ID + /// + public string TelemetryId { get; set; } = string.Empty; +} diff --git a/LanMountainDesktop.Launcher/Services/DataLocationResolver.cs b/LanMountainDesktop.Launcher/Services/DataLocationResolver.cs index 67e573b..31b3c3c 100644 --- a/LanMountainDesktop.Launcher/Services/DataLocationResolver.cs +++ b/LanMountainDesktop.Launcher/Services/DataLocationResolver.cs @@ -141,7 +141,9 @@ internal sealed class DataLocationResolver { try { - var configPath = ResolveConfigPath(); + // 配置文件必须位于默认系统数据路径下的 Launcher 目录中 + // 避免循环依赖:不能调用 ResolveConfigPath() -> ResolveLauncherDataPath() -> ResolveDataRoot() -> LoadConfig() + var configPath = Path.Combine(_defaultSystemDataPath, LauncherFolderName, ConfigFileName); if (!File.Exists(configPath)) { return null; diff --git a/LanMountainDesktop.Launcher/Services/PrivacyAgreementService.cs b/LanMountainDesktop.Launcher/Services/PrivacyAgreementService.cs new file mode 100644 index 0000000..bca07a6 --- /dev/null +++ b/LanMountainDesktop.Launcher/Services/PrivacyAgreementService.cs @@ -0,0 +1,245 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using LanMountainDesktop.Launcher.Models; + +namespace LanMountainDesktop.Launcher.Services; + +/// +/// 隐私协议同意状态管理服务(带防篡改保护) +/// +internal sealed class PrivacyAgreementService +{ + private readonly string _storagePath; + private readonly string _secretKey; + private const string ConfigFileName = "privacy-agreement.state.json"; + private const string CurrentAgreementVersion = "1.0"; + + public PrivacyAgreementService(string launcherDataPath) + { + _storagePath = Path.Combine(launcherDataPath, ConfigFileName); + // 使用机器特定信息生成密钥,增加篡改难度 + _secretKey = GenerateMachineSpecificKey(); + } + + /// + /// 检查用户是否已同意隐私协议 + /// + public bool HasUserAgreed() + { + try + { + if (!File.Exists(_storagePath)) + { + Logger.Info("[PrivacyAgreementService] 未找到隐私协议同意状态文件"); + return false; + } + + var json = File.ReadAllText(_storagePath); + var state = JsonSerializer.Deserialize(json, AppJsonContext.Default.PrivacyAgreementState); + + if (state == null) + { + Logger.Warn("[PrivacyAgreementService] 无法解析隐私协议状态文件"); + return false; + } + + // 验证数据完整性 + if (!VerifyIntegrity(state)) + { + Logger.Warn("[PrivacyAgreementService] 隐私协议状态文件已被篡改!"); + // 删除被篡改的文件 + try + { + File.Delete(_storagePath); + } + catch (Exception ex) + { + Logger.Warn($"[PrivacyAgreementService] 删除被篡改文件失败: {ex.Message}"); + } + return false; + } + + // 检查协议版本是否匹配 + if (state.AgreementVersion != CurrentAgreementVersion) + { + Logger.Info($"[PrivacyAgreementService] 隐私协议版本已更新: {state.AgreementVersion} -> {CurrentAgreementVersion}"); + return false; + } + + Logger.Info($"[PrivacyAgreementService] 用户已于 {state.AgreedAtUtc:yyyy-MM-dd HH:mm:ss} UTC 同意隐私协议"); + return state.IsAgreed; + } + catch (Exception ex) + { + Logger.Warn($"[PrivacyAgreementService] 检查同意状态时出错: {ex.Message}"); + return false; + } + } + + /// + /// 保存用户同意隐私协议的状态 + /// + public bool SaveAgreement(bool isAgreed, string userId, string deviceId) + { + try + { + // 确保目录存在 + var directory = Path.GetDirectoryName(_storagePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // 生成随机盐值 + var salt = GenerateRandomSalt(); + + var state = new PrivacyAgreementState + { + IsAgreed = isAgreed, + AgreedAtUtc = DateTime.UtcNow, + AgreementVersion = CurrentAgreementVersion, + UserId = userId, + DeviceId = deviceId, + Salt = salt + }; + + // 计算完整性哈希 + state.IntegrityHash = CalculateIntegrityHash(state); + + // 保存到文件 + var json = JsonSerializer.Serialize(state, AppJsonContext.Default.PrivacyAgreementState); + File.WriteAllText(_storagePath, json); + + Logger.Info($"[PrivacyAgreementService] 隐私协议同意状态已保存: IsAgreed={isAgreed}"); + return true; + } + catch (Exception ex) + { + Logger.Warn($"[PrivacyAgreementService] 保存同意状态失败: {ex.Message}"); + return false; + } + } + + /// + /// 获取当前的协议版本 + /// + public string GetCurrentAgreementVersion() => CurrentAgreementVersion; + + /// + /// 清除同意状态(用于测试或重置) + /// + public bool ClearAgreement() + { + try + { + if (File.Exists(_storagePath)) + { + File.Delete(_storagePath); + Logger.Info("[PrivacyAgreementService] 隐私协议同意状态已清除"); + } + return true; + } + catch (Exception ex) + { + Logger.Warn($"[PrivacyAgreementService] 清除同意状态失败: {ex.Message}"); + return false; + } + } + + /// + /// 生成机器特定的密钥 + /// + private string GenerateMachineSpecificKey() + { + try + { + // 组合多个机器特定信息生成密钥 + var machineName = Environment.MachineName; + var userName = Environment.UserName; + var osVersion = Environment.OSVersion.Version.ToString(); + var processorCount = Environment.ProcessorCount.ToString(); + + // 使用硬件信息(如果可用) + var hardwareId = GetHardwareIdentifier(); + + var keyData = $"{machineName}:{userName}:{osVersion}:{processorCount}:{hardwareId}:LanMountainDesktop"; + + // 使用 SHA-256 生成固定长度的密钥 + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData)); + return Convert.ToHexString(hash); + } + catch + { + // 如果无法获取机器信息,使用备用密钥 + return "LanMountainDesktop-Privacy-Agreement-Fallback-Key-2026"; + } + } + + /// + /// 获取硬件标识符 + /// + private string GetHardwareIdentifier() + { + try + { + // 尝试使用系统目录创建时间作为硬件标识的一部分 + var systemDir = Environment.SystemDirectory; + var dirInfo = new DirectoryInfo(systemDir); + return dirInfo.CreationTimeUtc.ToString("yyyyMMddHHmmss"); + } + catch + { + return "Unknown"; + } + } + + /// + /// 生成随机盐值 + /// + private string GenerateRandomSalt() + { + var saltBytes = new byte[32]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(saltBytes); + return Convert.ToHexString(saltBytes); + } + + /// + /// 计算完整性哈希(HMAC-SHA256) + /// + private string CalculateIntegrityHash(PrivacyAgreementState state) + { + // 构建需要哈希的数据字符串 + var dataToHash = $"{state.IsAgreed}:{state.AgreedAtUtc:o}:{state.AgreementVersion}:{state.UserId}:{state.DeviceId}:{state.Salt}"; + + // 使用 HMAC-SHA256 计算哈希 + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secretKey)); + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(dataToHash)); + return Convert.ToHexString(hash); + } + + /// + /// 验证数据完整性 + /// + private bool VerifyIntegrity(PrivacyAgreementState state) + { + try + { + if (string.IsNullOrEmpty(state.IntegrityHash) || string.IsNullOrEmpty(state.Salt)) + { + return false; + } + + var expectedHash = CalculateIntegrityHash(state); + return CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(state.IntegrityHash), + Encoding.UTF8.GetBytes(expectedHash)); + } + catch + { + return false; + } + } +} diff --git a/LanMountainDesktop.Launcher/Views/OobeWindow.axaml b/LanMountainDesktop.Launcher/Views/OobeWindow.axaml index b8b2f48..eb55cfa 100644 --- a/LanMountainDesktop.Launcher/Views/OobeWindow.axaml +++ b/LanMountainDesktop.Launcher/Views/OobeWindow.axaml @@ -6,13 +6,13 @@ xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:fi="using:FluentIcons.Avalonia" mc:Ignorable="d" - d:DesignWidth="700" - d:DesignHeight="500" + d:DesignWidth="850" + d:DesignHeight="650" x:Class="LanMountainDesktop.Launcher.Views.OobeWindow" x:DataType="views:OobeWindow" Title="欢迎使用阑山桌面" - Width="700" - Height="500" + Width="850" + Height="650" CanResize="False" WindowStartupLocation="CenterScreen" Background="{DynamicResource SolidBackgroundFillColorBaseBrush}" @@ -441,116 +441,137 @@ - + - + - - - + + + + - - - + + + + + - + + + + - - - + + + + + - + + + + - - + + @@ -561,17 +582,156 @@ Orientation="Horizontal" HorizontalAlignment="Right" Spacing="12" - Margin="0,24,0,0"> + Margin="0,32,0,0"> + + + + + + + + + + +