add:安全中心
This commit is contained in:
@@ -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(首次启动向导),看门狗在此期间不判定为卡死/假死
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1346,6 +1346,9 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
App.IsUpdateInstalling = true;
|
||||
try { ProcessProtectionManager.SetEnabled(false); } catch { }
|
||||
|
||||
// 在更新前备份设置文件
|
||||
try
|
||||
{
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -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<string, FileStream> _lockedFiles = new Dictionary<string, FileStream>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, SafeFileHandle> _lockedDirs = new Dictionary<string, SafeFileHandle>(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<string, SafeFileHandle> releasedDirs = null;
|
||||
Dictionary<string, FileStream> releasedFiles = null;
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
releasedDirs = new Dictionary<string, SafeFileHandle>(StringComparer.OrdinalIgnoreCase);
|
||||
releasedFiles = new Dictionary<string, FileStream>(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<string> 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<string> GetDirChainToRoot(string path)
|
||||
{
|
||||
var list = new List<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<bool> 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<string> 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<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; // 重置用户修改标志
|
||||
|
||||
@@ -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 { }
|
||||
}
|
||||
|
||||
@@ -136,6 +136,14 @@ namespace Ink_Canvas
|
||||
LogHelper.WriteLogToFile(ex.ToString(), LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProcessProtectionManager.ApplyFromSettings();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// Startup
|
||||
if (isStartup)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<local:SettingsPanelBase x:Class="Ink_Canvas.Windows.SettingsViews.SecurityPanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Ink_Canvas.Windows.SettingsViews"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="950" d:DesignWidth="640">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="ToggleSwitchStyle" TargetType="Border">
|
||||
<Setter Property="Width" Value="48"/>
|
||||
<Setter Property="Height" Value="25"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
<Setter Property="Padding" Value="3,0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
<Setter Property="Margin" Value="0,0,15,0"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<ScrollViewer ScrollChanged="ScrollViewerEx_ScrollChanged"
|
||||
IsManipulationEnabled="True"
|
||||
Name="ScrollViewerEx"
|
||||
IsDeferredScrollingEnabled="True"
|
||||
VerticalScrollBarVisibility="Visible"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
IsTabStop="False"
|
||||
TabIndex="-1"
|
||||
Margin="0,0,2,2">
|
||||
<StackPanel Margin="60,12,60,24">
|
||||
<Border BorderBrush="#e6e6e6" BorderThickness="1.25,1.25,1.25,4" CornerRadius="8">
|
||||
<StackPanel Orientation="Vertical" Margin="18,18,18,18">
|
||||
<TextBlock Text="安全密码默认关闭。开启后可用于退出软件、进入设置与重置配置时的二次验证。"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="#9a9996"
|
||||
FontSize="11"
|
||||
Margin="0,0,0,12"/>
|
||||
|
||||
<Grid Height="54">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
<TextBlock Foreground="#2e3436" FontSize="14.5" Text="启用安全密码" HorizontalAlignment="Left"/>
|
||||
<TextBlock Foreground="#9a9996" FontSize="11" Margin="0,3.5,0,0" Text="开启后需要设置密码" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="ToggleSwitchPasswordEnabled"
|
||||
Style="{StaticResource ToggleSwitchStyle}"
|
||||
Background="#e1e1e1"
|
||||
Tag="PasswordEnabled"
|
||||
MouseLeftButtonDown="ToggleSwitch_Click">
|
||||
<Border Width="19" Height="19" Background="White" CornerRadius="10" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="4" Direction="-45" Color="Black" Opacity="0.3" ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Background="#ebebeb" Margin="0,8,0,8"/>
|
||||
|
||||
<Grid Height="54">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
<TextBlock Foreground="#2e3436" FontSize="14.5" Text="密码管理" HorizontalAlignment="Left"/>
|
||||
<TextBlock Foreground="#9a9996" FontSize="11" Margin="0,3.5,0,0" Text="设置或修改安全密码" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,15,0">
|
||||
<Button x:Name="BtnSetOrChangePassword" Width="120" Height="32" Click="BtnSetOrChangePassword_Click" Content="设置/修改密码"/>
|
||||
<Button x:Name="BtnDisablePassword" Width="90" Height="32" Margin="8,0,0,0" Click="BtnDisablePassword_Click" Content="关闭密码"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Background="#ebebeb" Margin="0,8,0,8"/>
|
||||
|
||||
<Grid Height="54">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
<TextBlock Foreground="#2e3436" FontSize="14.5" Text="退出软件需要密码" HorizontalAlignment="Left"/>
|
||||
<TextBlock Foreground="#9a9996" FontSize="11" Margin="0,3.5,0,0" Text="关闭 InkCanvasForClass 时进行密码验证" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="ToggleSwitchRequirePasswordOnExit" Style="{StaticResource ToggleSwitchStyle}" Background="#e1e1e1" Tag="RequirePasswordOnExit" MouseLeftButtonDown="ToggleSwitch_Click">
|
||||
<Border Width="19" Height="19" Background="White" CornerRadius="10" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="4" Direction="-45" Color="Black" Opacity="0.3" ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Background="#ebebeb" Margin="0,8,0,8"/>
|
||||
|
||||
<Grid Height="54">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
<TextBlock Foreground="#2e3436" FontSize="14.5" Text="进入设置需要密码" HorizontalAlignment="Left"/>
|
||||
<TextBlock Foreground="#9a9996" FontSize="11" Margin="0,3.5,0,0" Text="打开设置面板/新设置窗口时进行验证" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="ToggleSwitchRequirePasswordOnEnterSettings" Style="{StaticResource ToggleSwitchStyle}" Background="#e1e1e1" Tag="RequirePasswordOnEnterSettings" MouseLeftButtonDown="ToggleSwitch_Click">
|
||||
<Border Width="19" Height="19" Background="White" CornerRadius="10" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="4" Direction="-45" Color="Black" Opacity="0.3" ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Background="#ebebeb" Margin="0,8,0,8"/>
|
||||
|
||||
<Grid Height="54">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
<TextBlock Foreground="#2e3436" FontSize="14.5" Text="重置配置需要密码" HorizontalAlignment="Left"/>
|
||||
<TextBlock Foreground="#9a9996" FontSize="11" Margin="0,3.5,0,0" Text="重置 Settings.json 前进行二次验证" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="ToggleSwitchRequirePasswordOnResetConfig" Style="{StaticResource ToggleSwitchStyle}" Background="#e1e1e1" Tag="RequirePasswordOnResetConfig" MouseLeftButtonDown="ToggleSwitch_Click">
|
||||
<Border Width="19" Height="19" Background="White" CornerRadius="10" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="4" Direction="-45" Color="Black" Opacity="0.3" ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Margin="0,25,0,0" BorderBrush="#e6e6e6" BorderThickness="1.25,1.25,1.25,4" CornerRadius="8">
|
||||
<StackPanel Orientation="Vertical" Margin="18,18,18,18">
|
||||
<TextBlock Text="进程保护默认开启:运行期间保护根目录下的程序文件(exe/dll/config)不被外部修改。"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="#9a9996"
|
||||
FontSize="11"
|
||||
Margin="0,0,0,12"/>
|
||||
<Grid Height="54">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
<TextBlock Foreground="#2e3436" FontSize="14.5" Text="进程保护" HorizontalAlignment="Left"/>
|
||||
<TextBlock Foreground="#9a9996" FontSize="11" Margin="0,3.5,0,0" Text="开启后关键文件会被只读占用" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="ToggleSwitchEnableProcessProtection" Style="{StaticResource ToggleSwitchStyle}" Background="#3584e4" Tag="EnableProcessProtection" MouseLeftButtonDown="ToggleSwitch_Click">
|
||||
<Border Width="19" Height="19" Background="White" CornerRadius="10" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="4" Direction="-45" Color="Black" Opacity="0.3" ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</local:SettingsPanelBase>
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Helpers;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Windows.SettingsViews
|
||||
{
|
||||
public partial class SecurityPanel : SettingsPanelBase
|
||||
{
|
||||
public SecurityPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public override void LoadSettings()
|
||||
{
|
||||
if (MainWindow.Settings == null) return;
|
||||
if (MainWindow.Settings.Security == null) MainWindow.Settings.Security = new Security();
|
||||
|
||||
_isLoaded = false;
|
||||
try
|
||||
{
|
||||
var sec = MainWindow.Settings.Security;
|
||||
|
||||
SetToggleSwitchState(FindToggleSwitch("ToggleSwitchPasswordEnabled"), sec.PasswordEnabled);
|
||||
SetToggleSwitchState(FindToggleSwitch("ToggleSwitchRequirePasswordOnExit"), sec.RequirePasswordOnExit);
|
||||
SetToggleSwitchState(FindToggleSwitch("ToggleSwitchRequirePasswordOnEnterSettings"), sec.RequirePasswordOnEnterSettings);
|
||||
SetToggleSwitchState(FindToggleSwitch("ToggleSwitchRequirePasswordOnResetConfig"), sec.RequirePasswordOnResetConfig);
|
||||
SetToggleSwitchState(FindToggleSwitch("ToggleSwitchEnableProcessProtection"), sec.EnableProcessProtection);
|
||||
|
||||
UpdatePasswordUiState();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
_isLoaded = true;
|
||||
}
|
||||
|
||||
private void UpdatePasswordUiState()
|
||||
{
|
||||
var sec = MainWindow.Settings?.Security;
|
||||
var enabled = sec != null && sec.PasswordEnabled;
|
||||
|
||||
if (BtnSetOrChangePassword != null) BtnSetOrChangePassword.IsEnabled = enabled;
|
||||
if (BtnDisablePassword != null) BtnDisablePassword.IsEnabled = enabled;
|
||||
|
||||
// 用途开关:仅在启用密码功能时可操作
|
||||
var usageEnabled = enabled;
|
||||
var t1 = FindToggleSwitch("ToggleSwitchRequirePasswordOnExit");
|
||||
var t2 = FindToggleSwitch("ToggleSwitchRequirePasswordOnEnterSettings");
|
||||
var t3 = FindToggleSwitch("ToggleSwitchRequirePasswordOnResetConfig");
|
||||
if (t1 != null) t1.IsEnabled = usageEnabled;
|
||||
if (t2 != null) t2.IsEnabled = usageEnabled;
|
||||
if (t3 != null) t3.IsEnabled = usageEnabled;
|
||||
}
|
||||
|
||||
protected override async void HandleToggleSwitchChange(string tag, bool newState)
|
||||
{
|
||||
if (MainWindow.Settings == null) return;
|
||||
if (MainWindow.Settings.Security == null) MainWindow.Settings.Security = new Security();
|
||||
var sec = MainWindow.Settings.Security;
|
||||
|
||||
switch (tag)
|
||||
{
|
||||
case "PasswordEnabled":
|
||||
if (newState)
|
||||
{
|
||||
var havePassword = SecurityManager.HasPasswordConfigured(MainWindow.Settings);
|
||||
|
||||
if (!havePassword)
|
||||
{
|
||||
var pwd = await SecurityManager.PromptSetNewPasswordAsync(Window.GetWindow(this));
|
||||
if (string.IsNullOrEmpty(pwd))
|
||||
{
|
||||
_isLoaded = false;
|
||||
SetToggleSwitchState(FindToggleSwitch("ToggleSwitchPasswordEnabled"), false);
|
||||
_isLoaded = true;
|
||||
return;
|
||||
}
|
||||
SecurityManager.SetPassword(MainWindow.Settings, pwd);
|
||||
}
|
||||
|
||||
sec.PasswordEnabled = true;
|
||||
MainWindow.SaveSettingsToFile();
|
||||
UpdatePasswordUiState();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 关闭:需要输入当前密码确认(已设置密码时)
|
||||
if (SecurityManager.HasPasswordConfigured(MainWindow.Settings))
|
||||
{
|
||||
bool ok = await SecurityManager.PromptAndVerifyAsync(MainWindow.Settings, Window.GetWindow(this),
|
||||
"关闭安全密码", "请输入当前密码以关闭安全密码功能。");
|
||||
if (!ok)
|
||||
{
|
||||
_isLoaded = false;
|
||||
SetToggleSwitchState(FindToggleSwitch("ToggleSwitchPasswordEnabled"), true);
|
||||
_isLoaded = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sec.PasswordEnabled = false;
|
||||
SecurityManager.ClearPassword(MainWindow.Settings);
|
||||
MainWindow.SaveSettingsToFile();
|
||||
UpdatePasswordUiState();
|
||||
}
|
||||
break;
|
||||
|
||||
case "RequirePasswordOnExit":
|
||||
sec.RequirePasswordOnExit = newState;
|
||||
MainWindow.SaveSettingsToFile();
|
||||
break;
|
||||
case "RequirePasswordOnEnterSettings":
|
||||
sec.RequirePasswordOnEnterSettings = newState;
|
||||
MainWindow.SaveSettingsToFile();
|
||||
break;
|
||||
case "RequirePasswordOnResetConfig":
|
||||
sec.RequirePasswordOnResetConfig = newState;
|
||||
MainWindow.SaveSettingsToFile();
|
||||
break;
|
||||
case "EnableProcessProtection":
|
||||
sec.EnableProcessProtection = newState;
|
||||
MainWindow.SaveSettingsToFile();
|
||||
ProcessProtectionManager.SetEnabled(newState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleOptionChange(string group, string value)
|
||||
{
|
||||
// 本面板无选项按钮组
|
||||
}
|
||||
|
||||
protected override void ToggleSwitch_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
base.ToggleSwitch_Click(sender, e);
|
||||
}
|
||||
|
||||
private async void BtnSetOrChangePassword_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (MainWindow.Settings == null) return;
|
||||
if (MainWindow.Settings.Security == null) MainWindow.Settings.Security = new Security();
|
||||
|
||||
var owner = Window.GetWindow(this);
|
||||
|
||||
var newPwd = await SecurityManager.PromptChangePasswordAsync(MainWindow.Settings, owner);
|
||||
if (!string.IsNullOrEmpty(newPwd))
|
||||
{
|
||||
SecurityManager.SetPassword(MainWindow.Settings, newPwd);
|
||||
MainWindow.Settings.Security.PasswordEnabled = true;
|
||||
MainWindow.SaveSettingsToFile();
|
||||
UpdatePasswordUiState();
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnDisablePassword_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 触发和开关一致的逻辑
|
||||
if (FindToggleSwitch("ToggleSwitchPasswordEnabled") is Border b)
|
||||
{
|
||||
// 模拟点击到 off
|
||||
if (MainWindow.Settings?.Security?.PasswordEnabled == true)
|
||||
{
|
||||
_isLoaded = true;
|
||||
SetToggleSwitchState(b, false);
|
||||
HandleToggleSwitchChange("PasswordEnabled", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<RoutedEventArgs> IsTopBarNeedShadowEffect;
|
||||
public event EventHandler<RoutedEventArgs> IsTopBarNeedNoShadowEffect;
|
||||
|
||||
private void ScrollViewerEx_ScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
var scrollViewer = (ScrollViewer)sender;
|
||||
if (scrollViewer.VerticalOffset >= 10) IsTopBarNeedShadowEffect?.Invoke(this, new RoutedEventArgs());
|
||||
else IsTopBarNeedNoShadowEffect?.Invoke(this, new RoutedEventArgs());
|
||||
}
|
||||
|
||||
public void ApplyTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
ThemeHelper.ApplyThemeToControl(this);
|
||||
LoadSettings();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Ink_Canvas.Windows.SettingsViews
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"SettingsPanelBase 应用主题时出错: {ex.Message}");
|
||||
}
|
||||
LoadSettings();
|
||||
_isLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Window x:Class="Ink_Canvas.Windows.SettingsWindow"
|
||||
<Window x:Class="Ink_Canvas.Windows.SettingsWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
@@ -196,6 +196,20 @@
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
<DrawingImage x:Key="SecurityIcon">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
|
||||
<GeometryDrawing Brush="Transparent">
|
||||
<GeometryDrawing.Geometry>
|
||||
<PathGeometry Figures="M12,3 C14.3358,5.06658 17.3844,6.14257 20.5,6 C20.9535,7.54302 21.0923,9.16147 20.9081,10.7592 C20.7238,12.3569 20.2203,13.9013 19.4274,15.3005 C18.6344,16.6998 17.5683,17.9254 16.2924,18.9045 C15.0165,19.8836 13.5567,20.5962 12,21 C10.4432,20.5962 8.98344,19.8836 7.7075,18.9045 C6.43157,17.9254 5.36547,16.6998 4.57255,15.3005 C3.77964,13.9013 3.27609,12.3569 3.09183,10.7592 C2.90757,9.16147 3.04636,7.54302 3.49996,6 C6.61548,6.14257 9.66413,5.06658 12,3 Z" />
|
||||
</GeometryDrawing.Geometry>
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="#FF222222" Thickness="2" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
<DrawingImage x:Key="UpdateCenterIcon">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V16 H16 V0 H0 Z">
|
||||
@@ -632,7 +646,11 @@
|
||||
<!--Sidebar-->
|
||||
<Border x:Name="SidebarBorder" Width="250" Background="#ebebeb" BorderBrush="#e1e1e1" CornerRadius="7,0,0,7" BorderThickness="0,0,2,0" HorizontalAlignment="Left">
|
||||
<Grid>
|
||||
<Grid Height="48" VerticalAlignment="Top">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Height="48" VerticalAlignment="Top" Grid.Row="0">
|
||||
<Border x:Name="SearchButtonBorder" CornerRadius="8" Background="#d9d9d9" Width="34" Height="34" Margin="8,0,0,0" HorizontalAlignment="Left" MouseLeftButtonDown="SearchButton_Click" Cursor="Hand">
|
||||
<Image Width="16" Height="16">
|
||||
<Image.Source>
|
||||
@@ -725,32 +743,45 @@
|
||||
</Border.ContextMenu>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ItemsControl Name="SidebarItemsControl">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Margin="0,54,0,6"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Line Visibility="{Binding _spVisibility}" Margin="0,6" HorizontalAlignment="Center" X1="0" X2="234" Y1="0" Y2="0" Stroke="{Binding _spStroke}" StrokeThickness="1" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
|
||||
<Border MouseDown="SidebarItem_MouseDown" MouseUp="SidebarItem_MouseUp" MouseLeave="SidebarItem_MouseLeave"
|
||||
Tag="{Binding}" Visibility="{Binding _siVisibility}" Background="{Binding _siBackground}" Height="40" Margin="6,0,6,0" CornerRadius="7">
|
||||
<Grid>
|
||||
<Border Height="40" CornerRadius="7" Background="#09090b" IsHitTestVisible="false" Opacity="0" Name="MouseFeedbackBorder"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Width="18" Height="18" Margin="14,0,9,0" VerticalAlignment="Center" Source="{Binding IconSource}"/>
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Text="{Binding Title}" Foreground="{Binding _siForeground}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ScrollViewer Margin="0,0,0,6"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
IsManipulationEnabled="True"
|
||||
PanningMode="VerticalOnly"
|
||||
CanContentScroll="True"
|
||||
Grid.Row="1">
|
||||
<ItemsControl Name="SidebarItemsControl">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Margin="0,6,0,0"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Line Visibility="{Binding _spVisibility}" Margin="0,6" HorizontalAlignment="Center" X1="0" X2="234" Y1="0" Y2="0" Stroke="{Binding _spStroke}" StrokeThickness="1" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
|
||||
<Border MouseDown="SidebarItem_MouseDown" MouseUp="SidebarItem_MouseUp" MouseLeave="SidebarItem_MouseLeave"
|
||||
Tag="{Binding}" Visibility="{Binding _siVisibility}" Background="{Binding _siBackground}" Height="40" Margin="6,0,6,0" CornerRadius="7">
|
||||
<Grid>
|
||||
<Border Height="40" CornerRadius="7" Background="#09090b" IsHitTestVisible="false" Opacity="0" Name="MouseFeedbackBorder"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Width="18" Height="18" Margin="14,0,9,0" VerticalAlignment="Center" Source="{Binding IconSource}"/>
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Text="{Binding Title}" Foreground="{Binding _siForeground}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!--SecurityPanel-->
|
||||
<Grid Margin="250,48,0,0" Visibility="Collapsed" Name="SecurityPane">
|
||||
<settingsViews:SecurityPanel x:Name="SecurityPanel"/>
|
||||
</Grid>
|
||||
|
||||
<!--UpdateCenterPanel-->
|
||||
<Grid Margin="250,48,0,0" Visibility="Collapsed" Name="UpdateCenterPane">
|
||||
<settingsViews:UpdateCenterPanel x:Name="UpdateCenterPanel"/>
|
||||
|
||||
@@ -153,6 +153,14 @@ namespace Ink_Canvas.Windows
|
||||
Type = SidebarItemType.Separator
|
||||
});
|
||||
SidebarItems.Add(new SidebarItem()
|
||||
{
|
||||
Type = SidebarItemType.Item,
|
||||
Title = "安全",
|
||||
Name = "SecurityItem",
|
||||
IconSource = FindResource("SecurityIcon") as DrawingImage,
|
||||
Selected = false,
|
||||
});
|
||||
SidebarItems.Add(new SidebarItem()
|
||||
{
|
||||
Type = SidebarItemType.Item,
|
||||
Title = "更新中心",
|
||||
@@ -169,6 +177,7 @@ namespace Ink_Canvas.Windows
|
||||
Selected = false,
|
||||
});
|
||||
SettingsPanes = new Grid[] {
|
||||
SecurityPane,
|
||||
UpdateCenterPane,
|
||||
AboutPane,
|
||||
CanvasAndInkPane,
|
||||
@@ -186,6 +195,7 @@ namespace Ink_Canvas.Windows
|
||||
};
|
||||
|
||||
SettingsPaneScrollViewers = new ScrollViewer[] {
|
||||
SecurityPanel.ScrollViewerEx,
|
||||
UpdateCenterPanel.UpdateCenterScrollViewerEx,
|
||||
SettingsAboutPanel.AboutScrollViewerEx,
|
||||
CanvasAndInkPanel.ScrollViewerEx,
|
||||
@@ -203,6 +213,7 @@ namespace Ink_Canvas.Windows
|
||||
};
|
||||
|
||||
SettingsPaneTitles = new string[] {
|
||||
"安全",
|
||||
"更新中心",
|
||||
"关于",
|
||||
"画板和墨迹",
|
||||
@@ -220,6 +231,7 @@ namespace Ink_Canvas.Windows
|
||||
};
|
||||
|
||||
SettingsPaneNames = new string[] {
|
||||
"SecurityItem",
|
||||
"UpdateCenterItem",
|
||||
"AboutItem",
|
||||
"CanvasAndInkItem",
|
||||
@@ -271,6 +283,8 @@ namespace Ink_Canvas.Windows
|
||||
AdvancedPanel.IsTopBarNeedNoShadowEffect += (o, s) => DropShadowEffectTopBar.Opacity = 0;
|
||||
SnapshotPanel.IsTopBarNeedShadowEffect += (o, s) => DropShadowEffectTopBar.Opacity = 0.25;
|
||||
SnapshotPanel.IsTopBarNeedNoShadowEffect += (o, s) => DropShadowEffectTopBar.Opacity = 0;
|
||||
SecurityPanel.IsTopBarNeedShadowEffect += (o, s) => DropShadowEffectTopBar.Opacity = 0.25;
|
||||
SecurityPanel.IsTopBarNeedNoShadowEffect += (o, s) => DropShadowEffectTopBar.Opacity = 0;
|
||||
UpdateCenterPanel.IsTopBarNeedShadowEffect += (o, s) => DropShadowEffectTopBar.Opacity = 0.25;
|
||||
UpdateCenterPanel.IsTopBarNeedNoShadowEffect += (o, s) => DropShadowEffectTopBar.Opacity = 0;
|
||||
|
||||
@@ -321,7 +335,7 @@ namespace Ink_Canvas.Windows
|
||||
// 使用反射调用所有面板的 ApplyTheme 方法(如果存在)
|
||||
var panels = new UserControl[]
|
||||
{
|
||||
UpdateCenterPanel, StartupPanel, CanvasAndInkPanel, GesturesPanel, InkRecognitionPanel,
|
||||
SecurityPanel, UpdateCenterPanel, StartupPanel, CanvasAndInkPanel, GesturesPanel, InkRecognitionPanel,
|
||||
ThemePanel, ShortcutsPanel, CrashActionPanel, PowerPointPanel,
|
||||
AutomationPanel, LuckyRandomPanel, AdvancedPanel, SnapshotPanel,
|
||||
SettingsAboutPanel, AppearancePanel, SearchPanelControl
|
||||
@@ -766,6 +780,7 @@ namespace Ink_Canvas.Windows
|
||||
LuckyRandomPanel,
|
||||
AdvancedPanel,
|
||||
SnapshotPanel,
|
||||
SecurityPanel,
|
||||
UpdateCenterPanel,
|
||||
SettingsAboutPanel,
|
||||
AppearancePanel
|
||||
@@ -960,6 +975,7 @@ namespace Ink_Canvas.Windows
|
||||
{ "LuckyRandomItem", LuckyRandomPanel },
|
||||
{ "AdvancedItem", AdvancedPanel },
|
||||
{ "SnapshotItem", SnapshotPanel },
|
||||
{ "SecurityItem", SecurityPanel },
|
||||
{ "UpdateCenterItem", UpdateCenterPanel },
|
||||
{ "AppearanceItem", AppearancePanel }
|
||||
};
|
||||
@@ -978,6 +994,7 @@ namespace Ink_Canvas.Windows
|
||||
if (LuckyRandomPane != null) LuckyRandomPane.Visibility = _selectedSidebarItemName == "LuckyRandomItem" ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (AdvancedPane != null) AdvancedPane.Visibility = _selectedSidebarItemName == "AdvancedItem" ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (SnapshotPane != null) SnapshotPane.Visibility = _selectedSidebarItemName == "SnapshotItem" ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (SecurityPane != null) SecurityPane.Visibility = _selectedSidebarItemName == "SecurityItem" ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (UpdateCenterPane != null) UpdateCenterPane.Visibility = _selectedSidebarItemName == "UpdateCenterItem" ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// 为新显示的面板应用主题(延迟执行,确保面板已完全显示)
|
||||
|
||||
Reference in New Issue
Block a user