add:安全中心

This commit is contained in:
2026-02-16 20:22:41 +08:00
parent 2f8c368eef
commit 45dc3cb537
18 changed files with 1167 additions and 49 deletions
+5 -5
View File
@@ -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++;
}
}
+3
View File
@@ -1346,6 +1346,9 @@ namespace Ink_Canvas.Helpers
{
try
{
App.IsUpdateInstalling = true;
try { ProcessProtectionManager.SetEnabled(false); } catch { }
// 在更新前备份设置文件
try
{
+2 -2
View File
@@ -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}");
}
+15 -8
View File
@@ -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 -1
View File
@@ -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);
}
}
+254
View File
@@ -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;
}
}
}