diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index cdd03821..9058cc57 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -42,6 +42,8 @@ namespace Ink_Canvas public static Process watchdogProcess; // 新增:标记是否为软件内主动退出 public static bool IsAppExitByUser; + // 新增:标记是否正在触发安装更新(用于跳过某些交互确认) + public static bool IsUpdateInstalling; // 新增:标记是否启用了UIA置顶功能 public static bool IsUIAccessTopMostEnabled; // 新增:标记是否正在显示 OOBE(首次启动向导),看门狗在此期间不判定为卡死/假死 diff --git a/Ink Canvas/Helpers/AutoBackupManager.cs b/Ink Canvas/Helpers/AutoBackupManager.cs index bac26b53..602eef89 100644 --- a/Ink Canvas/Helpers/AutoBackupManager.cs +++ b/Ink Canvas/Helpers/AutoBackupManager.cs @@ -59,7 +59,7 @@ namespace Ink_Canvas.Helpers // 确保备份目录存在 if (!Directory.Exists(BackupDir)) { - Directory.CreateDirectory(BackupDir); + ProcessProtectionManager.WithWriteAccess(BackupDir, () => Directory.CreateDirectory(BackupDir)); } // 检查主配置文件是否存在 @@ -74,7 +74,7 @@ namespace Ink_Canvas.Helpers string backupPath = Path.Combine(BackupDir, backupFileName); // 复制主配置文件到备份位置 - File.Copy(SettingsFile, backupPath, true); + ProcessProtectionManager.WithWriteAccess(backupPath, () => File.Copy(SettingsFile, backupPath, true)); // 更新最后备份时间 settings.Advanced.LastAutoBackupTime = DateTime.Now; @@ -138,11 +138,11 @@ namespace Ink_Canvas.Helpers if (File.Exists(SettingsFile)) { string corruptedBackup = Path.Combine(BackupDir, $"Settings_Corrupted_{DateTime.Now:yyyyMMdd_HHmmss}.json"); - File.Copy(SettingsFile, corruptedBackup, true); + ProcessProtectionManager.WithWriteAccess(corruptedBackup, () => File.Copy(SettingsFile, corruptedBackup, true)); } // 从备份恢复配置文件 - File.Copy(latestBackup, SettingsFile, true); + ProcessProtectionManager.WithWriteAccess(SettingsFile, () => File.Copy(latestBackup, SettingsFile, true)); return true; } catch (Exception ex) @@ -173,7 +173,7 @@ namespace Ink_Canvas.Helpers { if (File.GetCreationTime(file) < cutoffDate) { - File.Delete(file); + ProcessProtectionManager.WithWriteAccess(file, () => File.Delete(file)); deletedCount++; } } diff --git a/Ink Canvas/Helpers/AutoUpdateHelper.cs b/Ink Canvas/Helpers/AutoUpdateHelper.cs index ab24bcdb..b11cc574 100644 --- a/Ink Canvas/Helpers/AutoUpdateHelper.cs +++ b/Ink Canvas/Helpers/AutoUpdateHelper.cs @@ -1346,6 +1346,9 @@ namespace Ink_Canvas.Helpers { try { + App.IsUpdateInstalling = true; + try { ProcessProtectionManager.SetEnabled(false); } catch { } + // 在更新前备份设置文件 try { diff --git a/Ink Canvas/Helpers/DeviceIdentifier.cs b/Ink Canvas/Helpers/DeviceIdentifier.cs index f0821d4d..dd217bb6 100644 --- a/Ink Canvas/Helpers/DeviceIdentifier.cs +++ b/Ink Canvas/Helpers/DeviceIdentifier.cs @@ -368,11 +368,11 @@ namespace Ink_Canvas.Helpers var directory = Path.GetDirectoryName(filePath); if (!Directory.Exists(directory)) { - Directory.CreateDirectory(directory); + ProcessProtectionManager.WithWriteAccess(directory, () => Directory.CreateDirectory(directory)); } string json = JsonConvert.SerializeObject(info, Formatting.Indented); - File.WriteAllText(filePath, json); + ProcessProtectionManager.WithWriteAccess(filePath, () => File.WriteAllText(filePath, json)); LogHelper.WriteLogToFile($"DeviceIdentifier | 设备ID已保存到: {filePath}"); } diff --git a/Ink Canvas/Helpers/LogHelper.cs b/Ink Canvas/Helpers/LogHelper.cs index ce628ca8..10fe8cca 100644 --- a/Ink Canvas/Helpers/LogHelper.cs +++ b/Ink Canvas/Helpers/LogHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Threading; @@ -62,7 +62,7 @@ namespace Ink_Canvas.Helpers if (!Directory.Exists(App.RootPath)) { - Directory.CreateDirectory(App.RootPath); + ProcessProtectionManager.WithWriteAccess(App.RootPath, () => Directory.CreateDirectory(App.RootPath)); } var threadId = Thread.CurrentThread.ManagedThreadId; @@ -78,10 +78,13 @@ namespace Ink_Canvas.Helpers } } string logLine = string.Format("{0} [T{1}] [{2}] [{3}] {4}", DateTime.Now.ToString("O"), threadId, strLogType, callerInfo, str); - using (StreamWriter sw = new StreamWriter(file, true)) + ProcessProtectionManager.WithWriteAccess(file, () => { - sw.WriteLine(logLine); - } + using (StreamWriter sw = new StreamWriter(file, true)) + { + sw.WriteLine(logLine); + } + }); } catch { } } @@ -116,10 +119,14 @@ namespace Ink_Canvas.Helpers // 记录清理操作 string cleanupMessage = $"Logs folder exceeded size limit ({totalSize / 1024.0 / 1024.0:F2} MB > {MaxLogsFolderSizeBytes / 1024.0 / 1024.0:F2} MB). Folder cleaned."; - using (StreamWriter sw = new StreamWriter(Path.Combine(logsPath, $"Log_{AppStartTime}.txt"), true)) + var logFile = Path.Combine(logsPath, $"Log_{AppStartTime}.txt"); + ProcessProtectionManager.WithWriteAccess(logFile, () => { - sw.WriteLine($"{DateTime.Now:O} [Cleanup] {cleanupMessage}"); - } + using (StreamWriter sw = new StreamWriter(logFile, true)) + { + sw.WriteLine($"{DateTime.Now:O} [Cleanup] {cleanupMessage}"); + } + }); } } catch { } diff --git a/Ink Canvas/Helpers/PPTInkManager.cs b/Ink Canvas/Helpers/PPTInkManager.cs index 8e895ab7..13ad96ed 100644 --- a/Ink Canvas/Helpers/PPTInkManager.cs +++ b/Ink Canvas/Helpers/PPTInkManager.cs @@ -1,4 +1,4 @@ -using Microsoft.Office.Interop.PowerPoint; +using Microsoft.Office.Interop.PowerPoint; using System; using System.IO; using System.Runtime.InteropServices; diff --git a/Ink Canvas/Helpers/ProcessProtectionManager.cs b/Ink Canvas/Helpers/ProcessProtectionManager.cs new file mode 100644 index 00000000..978407a7 --- /dev/null +++ b/Ink Canvas/Helpers/ProcessProtectionManager.cs @@ -0,0 +1,379 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Ink_Canvas.Helpers +{ + internal static class ProcessProtectionManager + { + private static readonly object _lock = new object(); + private static readonly Dictionary _lockedFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary _lockedDirs = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static bool _enabled; + private static int _writeGate; + private static readonly string[] _excludedSubDirectories = new[] + { + "Configs", + "Saves", + "Backups", + "Logs", + "AutoUpdate" + }; + + public static bool Enabled + { + get { lock (_lock) return _enabled; } + } + + public static void ApplyFromSettings() + { + try + { + var settings = MainWindow.Settings; + var enabled = settings?.Security != null && settings.Security.EnableProcessProtection; + SetEnabled(enabled); + } + catch + { + } + } + + public static void SetEnabled(bool enabled) + { + lock (_lock) + { + if (_enabled == enabled) return; + _enabled = enabled; + } + + if (enabled) Enable(); + else Disable(); + } + + public static void WithWriteAccess(string targetPath, Action action) + { + if (action == null) return; + + if (!Enabled) + { + action(); + return; + } + + if (Interlocked.Exchange(ref _writeGate, 1) == 1) + { + var start = Environment.TickCount; + while (Interlocked.CompareExchange(ref _writeGate, 1, 1) == 1 && Environment.TickCount - start < 2000) + { + Thread.Sleep(10); + } + } + + var normalized = NormalizePath(targetPath); + var dirsToToggle = GetDirChainToRoot(normalized); + + Dictionary releasedDirs = null; + Dictionary releasedFiles = null; + + try + { + lock (_lock) + { + releasedDirs = new Dictionary(StringComparer.OrdinalIgnoreCase); + releasedFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var dir in dirsToToggle) + { + if (_lockedDirs.TryGetValue(dir, out var handle)) + { + _lockedDirs.Remove(dir); + releasedDirs[dir] = handle; + } + } + + if (!string.IsNullOrWhiteSpace(normalized) && File.Exists(normalized) && _lockedFiles.TryGetValue(normalized, out var fs)) + { + _lockedFiles.Remove(normalized); + releasedFiles[normalized] = fs; + } + } + + if (releasedFiles != null) + { + foreach (var kv in releasedFiles) + { + try { kv.Value.Dispose(); } catch { } + } + } + if (releasedDirs != null) + { + foreach (var kv in releasedDirs) + { + try { kv.Value.Dispose(); } catch { } + } + } + + action(); + } + finally + { + try + { + if (Enabled) + { + Enable(rescanRoot: false, rescanDirs: dirsToToggle); + } + } + catch + { + } + + Interlocked.Exchange(ref _writeGate, 0); + } + } + + private static void Enable() + { + Enable(rescanRoot: true, rescanDirs: null); + } + + private static void Enable(bool rescanRoot, IEnumerable rescanDirs) + { + try + { + var root = App.RootPath; + if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) return; + root = NormalizePath(root); + + if (rescanRoot) + { + LockDirectoryRecursive(root); + } + else if (rescanDirs != null) + { + foreach (var d in rescanDirs) + { + if (Directory.Exists(d)) + { + LockDirectory(d); + } + } + } + + if (rescanRoot) + { + LockFilesRecursive(root); + } + else if (rescanDirs != null) + { + foreach (var d in rescanDirs) + { + if (Directory.Exists(d)) + { + LockFilesRecursive(d); + } + else if (File.Exists(d)) + { + LockFile(d); + } + } + } + } + catch + { + } + } + + private static void Disable() + { + lock (_lock) + { + foreach (var kv in _lockedFiles) + { + try { kv.Value.Dispose(); } catch { } + } + _lockedFiles.Clear(); + + foreach (var kv in _lockedDirs) + { + try { kv.Value.Dispose(); } catch { } + } + _lockedDirs.Clear(); + } + } + + private static void LockDirectoryRecursive(string root) + { + try + { + if (!IsExcludedPath(root)) + { + LockDirectory(root); + } + foreach (var dir in Directory.GetDirectories(root, "*", SearchOption.AllDirectories)) + { + if (!IsExcludedPath(dir)) + { + LockDirectory(dir); + } + } + } + catch + { + } + } + + private static void LockFilesRecursive(string root) + { + try + { + foreach (var file in Directory.GetFiles(root, "*", SearchOption.AllDirectories)) + { + if (!IsExcludedPath(file)) + { + var ext = Path.GetExtension(file); + if (string.Equals(ext, ".exe", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, ".config", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, ".manifest", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, ".dat", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, ".enc", StringComparison.OrdinalIgnoreCase)) + { + LockFile(file); + } + } + } + } + catch + { + } + } + + private static void LockFile(string filePath) + { + filePath = NormalizePath(filePath); + lock (_lock) + { + if (_lockedFiles.ContainsKey(filePath)) return; + try + { + var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + _lockedFiles[filePath] = fs; + } + catch + { + } + } + } + + private static void LockDirectory(string dirPath) + { + dirPath = NormalizePath(dirPath); + lock (_lock) + { + if (_lockedDirs.ContainsKey(dirPath)) return; + try + { + var handle = CreateDirectoryHandle(dirPath); + if (handle != null && !handle.IsInvalid) + { + _lockedDirs[dirPath] = handle; + } + } + catch + { + } + } + } + + private static string NormalizePath(string p) + { + try + { + if (string.IsNullOrWhiteSpace(p)) return p; + return Path.GetFullPath(p.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + } + catch + { + return p; + } + } + + private static List GetDirChainToRoot(string path) + { + var list = new List(); + try + { + var root = NormalizePath(App.RootPath); + if (string.IsNullOrWhiteSpace(root)) return list; + + string dir = Directory.Exists(path) ? NormalizePath(path) : NormalizePath(Path.GetDirectoryName(path)); + while (!string.IsNullOrWhiteSpace(dir)) + { + if (!dir.StartsWith(root, StringComparison.OrdinalIgnoreCase)) break; + list.Add(dir); + if (string.Equals(dir, root, StringComparison.OrdinalIgnoreCase)) break; + dir = NormalizePath(Path.GetDirectoryName(dir)); + } + } + catch + { + } + return list; + } + + private static bool IsExcludedPath(string path) + { + try + { + var root = NormalizePath(App.RootPath); + if (string.IsNullOrWhiteSpace(root)) return false; + path = NormalizePath(path); + foreach (var name in _excludedSubDirectories) + { + var prefix = Path.Combine(root, name); + if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + catch + { + } + return false; + } + + private static SafeFileHandle CreateDirectoryHandle(string dirPath) + { + const uint GENERIC_READ = 0x80000000; + const uint FILE_SHARE_READ = 0x00000001; + const uint OPEN_EXISTING = 3; + const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + + return CreateFile( + dirPath, + GENERIC_READ, + FILE_SHARE_READ, + IntPtr.Zero, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern SafeFileHandle CreateFile( + string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr lpSecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile); + } +} + diff --git a/Ink Canvas/Helpers/SecurityManager.cs b/Ink Canvas/Helpers/SecurityManager.cs new file mode 100644 index 00000000..9dc221a6 --- /dev/null +++ b/Ink Canvas/Helpers/SecurityManager.cs @@ -0,0 +1,254 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using Ink_Canvas.Windows; +using iNKORE.UI.WPF.Modern.Controls; +using MessageBox=iNKORE.UI.WPF.Modern.Controls.MessageBox; +using System.Threading.Tasks; + +namespace Ink_Canvas.Helpers +{ + internal static class SecurityManager + { + private const int Pbkdf2Iterations = 120_000; + private const int SaltSizeBytes = 16; + private const int HashSizeBytes = 32; + + public static bool IsPasswordFeatureEnabled(Settings settings) + => settings?.Security != null && settings.Security.PasswordEnabled; + + public static bool HasPasswordConfigured(Settings settings) + => settings?.Security != null + && !string.IsNullOrWhiteSpace(settings.Security.PasswordSalt) + && !string.IsNullOrWhiteSpace(settings.Security.PasswordHash); + + public static bool IsPasswordRequiredForExit(Settings settings) + => IsPasswordFeatureEnabled(settings) && HasPasswordConfigured(settings) && settings.Security.RequirePasswordOnExit; + + public static bool IsPasswordRequiredForEnterSettings(Settings settings) + => IsPasswordFeatureEnabled(settings) && HasPasswordConfigured(settings) && settings.Security.RequirePasswordOnEnterSettings; + + public static bool IsPasswordRequiredForResetConfig(Settings settings) + => IsPasswordFeatureEnabled(settings) && HasPasswordConfigured(settings) && settings.Security.RequirePasswordOnResetConfig; + + public static bool VerifyPassword(Settings settings, string password) + { + if (!HasPasswordConfigured(settings)) return false; + if (password == null) return false; + + try + { + var salt = Convert.FromBase64String(settings.Security.PasswordSalt); + var expected = Convert.FromBase64String(settings.Security.PasswordHash); + + var actual = DeriveKey(password, salt, expected.Length); + return FixedTimeEquals(actual, expected); + } + catch + { + return false; + } + } + + public static async Task PromptAndVerifyAsync(Settings settings, Window owner, string title, string message) + { + if (!HasPasswordConfigured(settings)) return true; + + var dialog = new ContentDialog + { + Title = title, + PrimaryButtonText = "确定", + SecondaryButtonText = "取消" + }; + + var panel = new SimpleStackPanel + { + Spacing = 12, + Margin = new Thickness(0, 10, 0, 0) + }; + + var textBlock = new TextBlock + { + Text = message, + TextWrapping = TextWrapping.Wrap + }; + + var passwordBox = new PasswordBox + { + Height = 32 + }; + + panel.Children.Add(textBlock); + panel.Children.Add(passwordBox); + dialog.Content = panel; + + var result = await dialog.ShowAsync(); + if (result != ContentDialogResult.Primary) return false; + + return VerifyPassword(settings, passwordBox.Password); + } + + public static async Task PromptSetNewPasswordAsync(Window owner) + { + var dialog = new ContentDialog + { + Title = "设置安全密码", + PrimaryButtonText = "确定", + SecondaryButtonText = "取消" + }; + + var panel = new SimpleStackPanel + { + Spacing = 12, + Margin = new Thickness(0, 10, 0, 0) + }; + + var tipText = new TextBlock + { + Text = "请输入新密码", + TextWrapping = TextWrapping.Wrap + }; + + var newPwdBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) }; + var confirmPwdBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) }; + + panel.Children.Add(tipText); + panel.Children.Add(new TextBlock { Text = "新密码", Margin = new Thickness(0, 4, 0, 0) }); + panel.Children.Add(newPwdBox); + panel.Children.Add(new TextBlock { Text = "确认新密码", Margin = new Thickness(0, 8, 0, 0) }); + panel.Children.Add(confirmPwdBox); + dialog.Content = panel; + + var result = await dialog.ShowAsync(); + if (result != ContentDialogResult.Primary) return null; + + var pwd = newPwdBox.Password ?? ""; + var confirm = confirmPwdBox.Password ?? ""; + + if (string.IsNullOrWhiteSpace(pwd) || pwd.Length < 4) + { + MessageBox.Show("密码长度过短。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return null; + } + if (!string.Equals(pwd, confirm, StringComparison.Ordinal)) + { + MessageBox.Show("两次输入的密码不一致。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return null; + } + + return pwd; + } + + public static async Task PromptChangePasswordAsync(Settings settings, Window owner) + { + if (!HasPasswordConfigured(settings)) + { + return await PromptSetNewPasswordAsync(owner); + } + + var dialog = new ContentDialog + { + Title = "修改安全密码", + PrimaryButtonText = "确定", + SecondaryButtonText = "取消" + }; + + var panel = new SimpleStackPanel + { + Spacing = 12, + Margin = new Thickness(0, 10, 0, 0) + }; + + var tipText = new TextBlock + { + Text = "请输入当前密码,并设置新密码。", + TextWrapping = TextWrapping.Wrap + }; + + var currentBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) }; + var newPwdBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) }; + var confirmPwdBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) }; + + panel.Children.Add(tipText); + panel.Children.Add(new TextBlock { Text = "当前密码", Margin = new Thickness(0, 4, 0, 0) }); + panel.Children.Add(currentBox); + panel.Children.Add(new TextBlock { Text = "新密码", Margin = new Thickness(0, 8, 0, 0) }); + panel.Children.Add(newPwdBox); + panel.Children.Add(new TextBlock { Text = "确认新密码", Margin = new Thickness(0, 8, 0, 0) }); + panel.Children.Add(confirmPwdBox); + dialog.Content = panel; + + var result = await dialog.ShowAsync(); + if (result != ContentDialogResult.Primary) return null; + + var current = currentBox.Password ?? ""; + var newPwd = newPwdBox.Password ?? ""; + var confirm = confirmPwdBox.Password ?? ""; + + if (!VerifyPassword(settings, current)) + { + MessageBox.Show("当前密码错误。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return null; + } + + if (string.IsNullOrWhiteSpace(newPwd) || newPwd.Length < 4) + { + MessageBox.Show("新密码长度过短。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return null; + } + if (!string.Equals(newPwd, confirm, StringComparison.Ordinal)) + { + MessageBox.Show("两次输入的新密码不一致。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return null; + } + + return newPwd; + } + + public static void SetPassword(Settings settings, string password) + { + if (settings?.Security == null) return; + + var salt = new byte[SaltSizeBytes]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + var hash = DeriveKey(password, salt, HashSizeBytes); + + settings.Security.PasswordSalt = Convert.ToBase64String(salt); + settings.Security.PasswordHash = Convert.ToBase64String(hash); + } + + public static void ClearPassword(Settings settings) + { + if (settings?.Security == null) return; + settings.Security.PasswordSalt = ""; + settings.Security.PasswordHash = ""; + } + + private static byte[] DeriveKey(string password, byte[] salt, int keyBytes) + { + // 注意:Rfc2898DeriveBytes 在 net472 默认 HMACSHA1 + using (var kdf = new Rfc2898DeriveBytes(password, salt, Pbkdf2Iterations)) + { + return kdf.GetBytes(keyBytes); + } + } + + private static bool FixedTimeEquals(byte[] a, byte[] b) + { + if (a == null || b == null) return false; + if (a.Length != b.Length) return false; + var diff = 0; + for (int i = 0; i < a.Length; i++) + { + diff |= a[i] ^ b[i]; + } + return diff == 0; + } + } +} + diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs index c5b2f2f3..18ba6c60 100644 --- a/Ink Canvas/MainWindow.xaml.cs +++ b/Ink Canvas/MainWindow.xaml.cs @@ -1388,7 +1388,7 @@ namespace Ink_Canvas } } - private void Window_Closing(object sender, CancelEventArgs e) + private async void Window_Closing(object sender, CancelEventArgs e) { LogHelper.WriteLogToFile("Ink Canvas closing", LogHelper.LogType.Event); try @@ -1400,6 +1400,23 @@ namespace Ink_Canvas LogHelper.WriteLogToFile($"关闭快抽悬浮按钮时出错: {ex.Message}", LogHelper.LogType.Error); } + try + { + if (!App.IsUpdateInstalling && SecurityManager.IsPasswordRequiredForExit(Settings)) + { + bool ok = await SecurityManager.PromptAndVerifyAsync(Settings, this, "退出验证", "请输入安全密码以退出软件。"); + if (!ok) + { + e.Cancel = true; + LogHelper.WriteLogToFile("Ink Canvas closing cancelled by security password", LogHelper.LogType.Event); + return; + } + } + } + catch + { + } + if (!CloseIsFromButton && Settings.Advanced.IsSecondConfirmWhenShutdownApp) { // 第一个确认对话框 @@ -2498,11 +2515,23 @@ namespace Ink_Canvas #region 新设置窗口 // 添加打开新设置窗口按钮点击事件 - private void BtnOpenNewSettings_Click(object sender, RoutedEventArgs e) + private async void BtnOpenNewSettings_Click(object sender, RoutedEventArgs e) { if (isOpeningOrHidingSettingsPane) return; HideSubPanels(); { + try + { + if (SecurityManager.IsPasswordRequiredForEnterSettings(Settings)) + { + bool ok = await SecurityManager.PromptAndVerifyAsync(Settings, this, "进入设置", "请输入安全密码以进入设置。"); + if (!ok) return; + } + } + catch + { + } + var settingsWindow = new SettingsWindow(); settingsWindow.Owner = this; settingsWindow.ShowDialog(); diff --git a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs index 8cfe4bc3..edae54b2 100644 --- a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs +++ b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs @@ -2831,7 +2831,7 @@ namespace Ink_Canvas private bool wasNoFocusModeBeforeSettings; private bool userChangedNoFocusModeInSettings; - private void BtnSettings_Click(object sender, RoutedEventArgs e) + private async void BtnSettings_Click(object sender, RoutedEventArgs e) { if (BorderSettings.Visibility == Visibility.Visible) { @@ -2839,6 +2839,18 @@ namespace Ink_Canvas } else { + try + { + if (Ink_Canvas.Helpers.SecurityManager.IsPasswordRequiredForEnterSettings(Settings)) + { + bool ok = await Ink_Canvas.Helpers.SecurityManager.PromptAndVerifyAsync(Settings, this, "进入设置", "请输入安全密码以进入设置。"); + if (!ok) return; + } + } + catch + { + } + BorderSettings.Visibility = Visibility.Visible; wasNoFocusModeBeforeSettings = Settings.Advanced.IsNoFocusMode; userChangedNoFocusModeInSettings = false; // 重置用户修改标志 diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index 7d659dbf..8996bdc2 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -3320,8 +3320,20 @@ namespace Ink_Canvas Settings.Startup.IsFoldAtStartup = false; } - public void BtnResetToSuggestion_Click(object sender, RoutedEventArgs e) + public async void BtnResetToSuggestion_Click(object sender, RoutedEventArgs e) { + try + { + if (sender != null && Ink_Canvas.Helpers.SecurityManager.IsPasswordRequiredForResetConfig(Settings)) + { + bool ok = await Ink_Canvas.Helpers.SecurityManager.PromptAndVerifyAsync(Settings, this, "重置配置验证", "请输入安全密码以确认重置配置。"); + if (!ok) return; + } + } + catch + { + } + try { isLoaded = false; @@ -4405,10 +4417,11 @@ namespace Ink_Canvas string configsDir = Path.Combine(App.RootPath, "Configs"); if (!Directory.Exists(configsDir)) { - Directory.CreateDirectory(configsDir); + ProcessProtectionManager.WithWriteAccess(configsDir, () => Directory.CreateDirectory(configsDir)); } - File.WriteAllText(App.RootPath + settingsFileName, text); + var path = App.RootPath + settingsFileName; + ProcessProtectionManager.WithWriteAccess(path, () => File.WriteAllText(path, text)); } catch { } } diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index d184882a..c497b57b 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -136,6 +136,14 @@ namespace Ink_Canvas LogHelper.WriteLogToFile(ex.ToString(), LogHelper.LogType.Error); } + try + { + ProcessProtectionManager.ApplyFromSettings(); + } + catch + { + } + // Startup if (isStartup) { diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index 10d6e341..04be989e 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -31,6 +31,26 @@ namespace Ink_Canvas public CameraSettings Camera { get; set; } = new CameraSettings(); [JsonProperty("dlass")] public DlassSettings Dlass { get; set; } = new DlassSettings(); + [JsonProperty("security")] + public Security Security { get; set; } = new Security(); + } + + public class Security + { + [JsonProperty("passwordEnabled")] + public bool PasswordEnabled { get; set; } = false; + [JsonProperty("passwordSalt")] + public string PasswordSalt { get; set; } = ""; + [JsonProperty("passwordHash")] + public string PasswordHash { get; set; } = ""; + [JsonProperty("requirePasswordOnExit")] + public bool RequirePasswordOnExit { get; set; } = false; + [JsonProperty("requirePasswordOnEnterSettings")] + public bool RequirePasswordOnEnterSettings { get; set; } = false; + [JsonProperty("requirePasswordOnResetConfig")] + public bool RequirePasswordOnResetConfig { get; set; } = false; + [JsonProperty("enableProcessProtection")] + public bool EnableProcessProtection { get; set; } = true; } public class Canvas diff --git a/Ink Canvas/Windows/SettingsViews/SettingsViews/SecurityPanel.xaml b/Ink Canvas/Windows/SettingsViews/SettingsViews/SecurityPanel.xaml new file mode 100644 index 00000000..66d85eb0 --- /dev/null +++ b/Ink Canvas/Windows/SettingsViews/SettingsViews/SecurityPanel.xaml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +