65.9K
CodeProject 正在变化。 阅读更多。
Home

OptionLock,一个KeePass 2.x插件,在KeePass锁定时代替禁用UI

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2012年7月30日

CPOL

7分钟阅读

viewsIcon

32116

downloadIcon

369

当所有文档被锁定时以及没有加载任何文档时,禁用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锁定

© . All rights reserved.