Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

OptionLock, a KeePass 2.x Plugin keeps UI disabled while KeePass is Locked

5.00/5 (1 vote)
17 Apr 2016CPOL7 min read 30.6K   323  
Keeps unnecessary UI elements of KeePass disabled while all documents are locked and also while there are no documents are loaded.

Updates   

4/17/2016 - Updated to OptionLock v1.1.0.0 built against KeePass 2.32 (to handle new "View -> Grouping in Entry List" menu option; and to fix handling of "Help -> Check for Updates" menu option).

Introduction   

KeePass is a "free, open source, light-weight and easy-to-use password manager."

OptionLock is a plugin for KeePass 2.x that keeps unnecessary UI elements of KeePass disabled while all documents are locked and also while there are no documents. OptionLock does not disable any UI elements while at least one database is opened and unlocked.

To Run   

Download and extract the PLGX Plugin to your KeePass directory. I recommend that you wait to do this until after you have a created KeePass database. 

See "Affected UI" near the bottom of this page for details of what UI elements are disabled. 

If you get into a state where you have no database file to open and OptionLock prevents creation of new databases then manually disable OptionLock (e.g. move OptionLock.plgx out of your KeePass directory and re-run KeePass); then create a database; put OptionLock.plgx back; re-run KeePass once more; and now you can open that database and have OptionLock working. 

Background  

KeePass has a short page about plugin development for version 2.x here

KeePass leaves many of its settings accessible while it is locked; whereas many other security minded applications make most all of their GUI inaccessible while their environments are locked. 

KeePass supports having multiple concurrently opened documents; each of which can be in its own independent locked/unlocked state. KeePass can also have no documents. OptionLock considers KeePass as a whole to be "unlocked" when at least one document exists that is unlocked (it does not matter which document is active/selected when multiple document tabs exist). Thus if there are no documents or if all documents are locked, then OptionLock considers KeePass as a whole to be "locked". 

Design Decisions  

If KeePass is locked, then creating a new database or opening a database would put KeePass in an unlocked state. To prevent this, OptionLock disabled UI for creating and opening databases (only while locked). However, it's a bit odd if KeePass is launched without any database, because then OptionLock consider's KeePass locked but there would then be no way to create or open a database leaving KeePass in an unusable state. In this one special case, OptionLock enables the Open and Open Recent menus and buttons. This may or may not be desirable, because it would be very easy to create a database with a different install of KeePass and thus open it. But there are ways to prevent getting in this state, such checking Options --> Advanced --> "Remember and automatically open last used database on startup".  

Other plugins can add menu items and buttons to KeePass. OptionLock currently is not designed to deal with such UI added by other plugins. 

Using the code   

The small VS.NET solution is available and contains the full source (you'll have to place KeePass.exe in the solution directory or fix the reference to KeePass.exe in the project settings). The following are some, but not all, sections from the full source. 

OptionLock is a fairly simple plugin, following KeePass' tutorials it is a class derived from KeePass.Plugins.Plugin and has the same name as the namespace with "Ext" appended to the class name:  

C#
using KeePass.Plugins;
 
namespace OptionLock
{
  public sealed class OptionLockExt : Plugin
  {     

In the project settings assembly info (in VS.NET 10: right click Project --> Properties --> Application tab --> Assembly Information...) follow the tutorial again and be sure to make the title the same as the namespace, and the product must be "KeePass Plugin". FYI, the description shows up in KeePass' plugins dialog. See the tutorial for what to set the rest of the fields to. 

A plugin needs to override two methods, here are OptionLock's: 

C#
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();
}   

The terminate method, is not cancel-able, it's just the plugin's chance to do necessary cleanup. 

OptionLockExt has only four member variables: 

C#
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;     

Most plugins will want to store a reference to the host parameter from the overridden Initialize method because it is the main interface into KeePass and is probably needed after Initialize is called (e.g. in Terminate). 

m_FileLockItem's enabled state is used to determine if KeePass has any documents via these helper properties: 

C#
bool HasDocs { get { return m_FileLockItem.Enabled; } }
bool HasNoDocs { get { return !HasDocs; } }   

The two linked lists store references to KeePass' UI items of interest for tracking and managing their enabled states. 

OptionLock also needs to know whether there exists at least one open and unlocked file. The best way I found for a plugin to check this is like so: 

C#
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;
}   

It's a method instead of a property and named specifically to follow suite with KeePass' MainWindow.IsAtLeastOneFileOpen() method. 

There is no clean direct access to KeePass' UI items of interest, but there is generic access to its controls. KeePass developers gave unique names to many of its controls and of course KeePass is open source so getting these names and then finding controls of interest at run-time is easy. Here's the method, called from Initialize, which does the work:  

C#
// 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;
  }
} 

The last section of that method initializes the items' enabled states and registers handlers to catch when those states get changed. KeePass likes to refresh the state of some of the UI items and does sometimes enable them when OptionLock wants them disabled, so the change is caught and undone when needed: 

C#
// 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);
  }
}  

Up in Initialize KeePass' FileOpened/Closed events are registered for handling. Listening for those events seems to be a reliable way, with decent timing, to catch and check state changes of KeePass being overall locked or unlocked under OptionLock's definitions. These are the handlers: 

C#
// 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);
      }
    }        
  }
}    

The last main piece is for the special case of having no opened documents. For this m_FileLockItem.EnabledChanged is handled: 

C#
// 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);
  }
}    

The ordering of events (described in the code comment above) between these three even handlers matters, but fortunately KeePass appears to call them in a good order for this plugin. 

 

If no docs are open, then m_FileLockItem is disabled. Upon opening a doc/database/file m_FileLockItem becomes enabled and then OptionLock momentarily disables the special case Open and Open Recent menus and buttons. This is correct because at this point in time, there is no open and unlocked database. However, almost immediately after this, the database is opened and then the plugin enables all the tracked and disabled UI items. 

There are more cases and ordering to consider (e.g. 3 opened docs, 2 locked, 1 opened and then it gets closed). But with any combination of 2 or more concurrently opened docs, the state of m_FileLockItem does not change so it's just a matter of database ("files") being closed and opened (which is the same as docs being locked and unlocked respectively). 

Compatibility  

This implementation works with KeePass 2.19, but it is a bit brittle due to relying on string comparisons. KeePass could change these literal string names in different versions. It also means that new UI elements added in new versions won't be accounted for without updates to this plugin. 

Points of Interest   

This plugin could be extended to be more user configurable. It could then drop reliance on brittle string comparisons and be more compatible with newer KeePass versions. It could also open the door to having OptionLock manage the enabled state of UI items added by other plugins (though other plugins' implementations might not like that). Similarly, if users can configure which items OptionLock should manage then it should take care to not allow managing items such that OptionLock enables an item when it really shouldn't be enabled, which could lead to crashes or corrupt state if not avoided.

Although OptionLock's features are convenient, I do no claim that they add any solid security features to KeePass. 

Affected UI 

KeePass does not disable items in Red by default when locked, but OptionLock does: 

Tray Icon's Context Menu 

 Image 1

Toolbar 

Image 2

 Main Menus 

Image 3  Image 4 

Image 5   Image 6 

Related   

Check out my other KeePass 2.X plugin: MinLock, a KeePass 2.x Plugin to keep minimized KeePass Locked

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)