add:安全中心
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user