OptionLock,一个KeePass 2.x插件,在KeePass锁定时代替禁用UI
当所有文档被锁定时以及没有加载任何文档时,禁用KeePass不必要的UI元素。
更新
2016年4月17日 - 更新至OptionLock v1.1.0.0,针对KeePass 2.32构建(以处理新的“视图 -> 条目列表中的分组”菜单选项;并修复“帮助 -> 检查更新”菜单选项的处理)。
引言
KeePass 是一个“免费、开源、轻量级且易于使用的密码管理器。”
OptionLock是KeePass 2.x的一个插件,当所有文档被锁定或没有文档时,它会禁用KeePass不必要的UI元素。当至少有一个数据库被打开并解锁时,OptionLock不会禁用任何UI元素。
运行
下载并将PLGX插件解压到你的KeePass目录。我建议你等到创建了KeePass数据库后再进行此操作。
有关哪些UI元素被禁用的详细信息,请参阅页面底部的“受影响的UI”。
如果你遇到没有可打开的数据库文件,并且OptionLock阻止创建新数据库的情况,那么请手动禁用OptionLock(例如,将OptionLock.plgx移出你的KeePass目录并重新运行KeePass);然后创建一个数据库;将OptionLock.plgx放回;再次重新运行KeePass;现在你可以打开该数据库并让OptionLock工作。
背景
KeePass有一个关于2.x版本插件开发的简短页面,请点击这里。
KeePass在锁定状态下仍可访问其许多设置;而许多其他注重安全性的应用程序在环境锁定时会使其大部分GUI无法访问。
KeePass支持同时打开多个文档;每个文档都可以处于其独立的锁定/解锁状态。KeePass也可以没有文档。当至少一个文档存在且已解锁时(当存在多个文档选项卡时,哪个文档是活动/选定的并不重要),OptionLock将KeePass整体视为“解锁”状态。因此,如果没有文档或所有文档都已锁定,OptionLock将KeePass整体视为“锁定”状态。
设计决策
如果KeePass被锁定,那么创建新数据库或打开数据库会使KeePass处于解锁状态。为了防止这种情况,OptionLock禁用了用于创建和打开数据库的UI(仅在锁定时)。然而,如果KeePass在没有任何数据库的情况下启动,这会有点奇怪,因为OptionLock会认为KeePass已锁定,但这样就无法创建或打开数据库,使KeePass处于不可用状态。在这种特殊情况下,OptionLock启用了打开和最近打开菜单和按钮。这可能合意也可能不合意,因为使用不同KeePass安装创建数据库并打开它会非常容易。但有一些方法可以防止进入这种状态,例如检查选项 -> 高级 -> “启动时记住并自动打开上次使用的数据库”。
其他插件可以向KeePass添加菜单项和按钮。OptionLock目前未设计来处理其他插件添加的此类UI。
使用代码
提供了小的VS.NET解决方案,其中包含完整的源代码(你必须将KeePass.exe放在解决方案目录中,或者修复项目设置中对KeePass.exe的引用)。以下是完整源代码中的部分(但不是全部)片段。
OptionLock是一个相当简单的插件,遵循KeePass的教程,它是一个派生自KeePass.Plugins.Plugin的类,并且与命名空间同名,类名后附加“Ext”:
using KeePass.Plugins;
namespace OptionLock
{
public sealed class OptionLockExt : Plugin
{
在项目设置程序集信息中(在VS.NET 10中:右键点击项目 -> 属性 -> 应用程序选项卡 -> 程序集信息...),再次遵循教程,并确保将标题与命名空间相同,并且产品必须是“KeePass Plugin”。供参考,描述会显示在KeePass的插件对话框中。有关其他字段的设置,请参阅教程。
一个插件需要重写两个方法,以下是OptionLock的:
public override bool Initialize(IPluginHost host)
{
m_Host = host;
InitializeItems();
// Detect lock state changes by the opening/closing of files
m_Host.MainWindow.FileOpened += MainWindow_FileOpened;
m_Host.MainWindow.FileClosed += MainWindow_FileClosed;
return base.Initialize(host);
}
public override void Terminate()
{
CleanupItems();
base.Terminate();
}
terminate 方法是不可取消的,它只是插件进行必要清理的机会。
OptionLockExt
只有四个成员变量:
IPluginHost m_Host;
// KeePass' FileLock menu item
ToolStripItem m_FileLockItem;
// UI made to be accessible only when there is an unlocked database.
LinkedList<ToolStripItem> m_UnlockedDbItems;
// UI made to be accessible only when there is an unlocked database
// or when there are no opened documents.
LinkedList<ToolStripItem> m_NoDocItems;
大多数插件会希望存储对重写的Initialize方法中的host参数的引用,因为它是KeePass的主要接口,并且在Initialize调用后可能需要(例如在Terminate中)。
m_FileLockItem
的启用状态用于通过这些辅助属性判断KeePass是否有任何文档:
bool HasDocs { get { return m_FileLockItem.Enabled; } }
bool HasNoDocs { get { return !HasDocs; } }
这两个链表存储了对KeePass感兴趣的UI项的引用,用于跟踪和管理它们的启用状态。
OptionLock还需要知道是否存在至少一个打开且未锁定的文件。我发现插件检查此项的最佳方法如下:
bool IsAtLeastOneFileOpenAndUnlocked()
{
var mainForm = m_Host.MainWindow;
foreach (var doc in mainForm.DocumentManager.Documents)
{
if (doc.Database.IsOpen && !mainForm.IsFileLocked(doc))
{
return true;
}
}
return false;
}
它是一个方法而不是属性,其命名特意与KeePass的MainWindow.IsAtLeastOneFileOpen()方法保持一致。
无法直接访问KeePass感兴趣的UI项,但可以通用访问其控件。KeePass开发人员为许多控件提供了唯一的名称,当然KeePass是开源的,因此在运行时获取这些名称并找到感兴趣的控件很容易。以下是从Initialize调用的方法,它完成了这项工作:
// Find, initialize and track KeePass' UI items of interest
void InitializeItems()
{
m_UnlockedDbItems = new LinkedList<ToolStripItem>();
m_NoDocItems = new LinkedList<ToolStripItem>();
// TrayContextMenu
foreach (var item in m_Host.MainWindow.TrayContextMenu.Items)
{
var menuItem = item as ToolStripItem;
if (menuItem != null && menuItem.Name == "m_ctxTrayOptions")
{
m_UnlockedDbItems.AddFirst(menuItem);
}
}
// MainMenu
foreach (var mainItem in m_Host.MainWindow.MainMenu.Items)
{
var dropDown = mainItem as ToolStripDropDownItem;
if (dropDown != null)
{
foreach (var item in dropDown.DropDownItems)
{
var stripItem = item as ToolStripItem;
if (stripItem != null)
{
switch (stripItem.Name)
{
// File
case "m_menuFileLock":
m_FileLockItem = stripItem;
m_FileLockItem.EnabledChanged += FileLockMenuItem_EnabledChanged;
break;
// File
case "m_menuFileOpen":
case "m_menuFileRecent":
m_NoDocItems.AddFirst(stripItem);
break;
// File
case "m_menuFileNew":
case "m_menuFileClose":
// View
case "m_menuChangeLanguage":
case "m_menuViewShowToolBar":
case "m_menuViewShowEntryView":
case "m_menuViewWindowLayout":
case "m_menuViewAlwaysOnTop":
case "m_menuViewConfigColumns":
case "m_menuViewSortBy":
case "m_menuViewTanOptions":
case "m_menuViewShowEntriesOfSubGroups":
// Tools
case "m_menuToolsPwGenerator":
case "m_menuToolsDb":
case "m_menuToolsTriggers":
case "m_menuToolsPlugins":
case "m_menuToolsOptions":
// Help
case "m_menuHelpSelectSource":
case "m_menuHelpCheckForUpdate":
m_UnlockedDbItems.AddFirst(stripItem);
break;
}
}
}
}
}
// CustomToolStrip
int toolIndex = m_Host.MainWindow.Controls.IndexOfKey("m_toolMain");
var toolStrip = m_Host.MainWindow.Controls[toolIndex] as KeePass.UI.CustomToolStripEx;
foreach (var item in toolStrip.Items)
{
var stripItem = item as ToolStripItem;
if (stripItem != null)
{
switch (stripItem.Name)
{
case "m_tbOpenDatabase":
m_NoDocItems.AddFirst(stripItem);
break;
case "m_tbNewDatabase":
case "m_tbCloseTab":
m_UnlockedDbItems.AddFirst(stripItem);
break;
}
}
}
// Initialize enabled states of items and track changes
bool isUnlocked = IsAtLeastOneFileOpenAndUnlocked();
foreach (var item in m_UnlockedDbItems)
{
item.Enabled = isUnlocked;
item.EnabledChanged += UnlockedDbMenuItem_EnabledChanged;
}
foreach (var item in m_NoDocItems)
{
item.Enabled = HasNoDocs;
item.EnabledChanged += NoDbMenuItem_EnabledChanged;
}
}
该方法的最后一部分初始化了项目的启用状态,并注册了处理程序以捕获这些状态何时发生变化。KeePass喜欢刷新一些UI项目的状态,并且有时会在OptionLock希望它们禁用时启用它们,因此在需要时会捕获并撤消更改:
// Something external changed the state of a tracked NoDoc item
// so fix its enabled state if needed.
void NoDbMenuItem_EnabledChanged(object sender, EventArgs e)
{
var item = sender as ToolStripItem;
if (item != null)
{
if (HasNoDocs)
{
EnableNoDbItem(true, item);
}
else
{
EnableNoDbItem(IsAtLeastOneFileOpenAndUnlocked(), item);
}
}
}
// Something external changed the state of a tracked UnlockedDb item
// so fix its enabled state if needed.
// KeePass does cause this case to happen as it likes to refresh the
// enabled state of the "Close" and "Options" UI here and there.
void UnlockedDbMenuItem_EnabledChanged(object sender, EventArgs e)
{
var item = sender as ToolStripItem;
if (item != null)
{
EnableUnlockedDbItem(IsAtLeastOneFileOpenAndUnlocked(), item);
}
}
在Initialize中,KeePass的FileOpened/Closed事件被注册进行处理。监听这些事件似乎是一种可靠的方式,具有良好的时机,可以捕获并检查KeePass在OptionLock定义下是整体锁定还是解锁的状态变化。这些是处理程序:
// KeePass opened a file/database
void MainWindow_FileOpened(object sender, KeePass.Forms.FileOpenedEventArgs e)
{
if (IsAtLeastOneFileOpenAndUnlocked())
{
// An unlocked database exists so enable all tracked items.
foreach (var item in m_UnlockedDbItems)
{
EnableUnlockedDbItem(true, item);
}
foreach (var item in m_NoDocItems)
{
EnableNoDbItem(true, item);
}
}
}
// KeePass closed a file/database
void MainWindow_FileClosed(object sender, KeePass.Forms.FileClosedEventArgs e)
{
if (!IsAtLeastOneFileOpenAndUnlocked())
{
// No unlocked databases exist so disable all tracked items.
foreach (var item in m_UnlockedDbItems)
{
EnableUnlockedDbItem(false, item);
}
// Except, only disable NoDoc items when there are docs
if (HasDocs)
{
foreach (var item in m_NoDocItems)
{
EnableNoDbItem(false, item);
}
}
}
}
最后一个主要部分是针对没有打开文档的特殊情况。为此,处理了m_FileLockItem.EnabledChanged
:
// KeePass changed enabled state of its FileLock menu item
void FileLockMenuItem_EnabledChanged(object sender, EventArgs e)
{
// When no docs are open, FileLock menu item is disabled;
// otherwise, it is enabled. KeePass enables it *before*
// firing FileOpened and disables it *after* firing FileClosed.
// Respectively only for first/last of File open/close given
// that KeePass features multiple concurrent opened files.
foreach (var item in m_NoDocItems)
{
EnableNoDbItem(HasNoDocs, item);
}
}
这三个事件处理程序之间的事件顺序(如上面代码注释中所述)很重要,但幸运的是 KeePass 似乎以适合此插件的良好顺序调用它们。
如果没有文档打开,那么m_FileLockItem
会被禁用。打开文档/数据库/文件后,m_FileLockItem
会启用,然后OptionLock会暂时禁用特殊情况下的打开和最近打开菜单和按钮。这是正确的,因为此时此刻,没有打开和解锁的数据库。然而,几乎在此之后,数据库被打开,然后插件会启用所有被跟踪和禁用的UI项目。
还有更多的案例和顺序需要考虑(例如,3个打开的文档,2个锁定,1个打开然后关闭)。但是,对于任何2个或更多同时打开的文档组合,m_FileLockItem
的状态不会改变,因此这只是数据库(“文件”)关闭和打开的问题(这与文档分别锁定和解锁相同)。
兼容性
此实现适用于KeePass 2.19,但由于依赖字符串比较而有点脆弱。KeePass可能会在不同版本中更改这些字面字符串名称。这也意味着新版本中添加的UI元素将无法在新版本中被考虑,除非此插件进行更新。
关注点
此插件可以扩展以更具用户可配置性。这样就可以减少对脆弱字符串比较的依赖,并更好地兼容新版本的KeePass。它还可以让OptionLock管理其他插件添加的UI元素的启用状态(尽管其他插件的实现可能不喜欢这样)。同样,如果用户可以配置OptionLock应该管理哪些项目,那么它应该注意不要允许管理那些OptionLock本不应该启用的项目,否则如果未避免,可能会导致崩溃或状态损坏。
尽管OptionLock的功能很方便,但我并不声称它们为KeePass添加了任何可靠的安全功能。
受影响的UI
KeePass 在锁定时默认不会禁用红色的项目,但 OptionLock 会:
托盘图标的上下文菜单
工具栏
主菜单
相关
查看我的另一个KeePass 2.X插件:MinLock,一个KeePass 2.x插件,用于保持最小化的KeePass锁定