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:
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:
public override bool Initialize(IPluginHost host)
{
m_Host = host;
InitializeItems();
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:
IPluginHost m_Host;
ToolStripItem m_FileLockItem;
LinkedList<ToolStripItem> m_UnlockedDbItems;
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:
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:
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:
void InitializeItems()
{
m_UnlockedDbItems = new LinkedList<ToolStripItem>();
m_NoDocItems = new LinkedList<ToolStripItem>();
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);
}
}
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)
{
case "m_menuFileLock":
m_FileLockItem = stripItem;
m_FileLockItem.EnabledChanged += FileLockMenuItem_EnabledChanged;
break;
case "m_menuFileOpen":
case "m_menuFileRecent":
m_NoDocItems.AddFirst(stripItem);
break;
case "m_menuFileNew":
case "m_menuFileClose":
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":
case "m_menuToolsPwGenerator":
case "m_menuToolsDb":
case "m_menuToolsTriggers":
case "m_menuToolsPlugins":
case "m_menuToolsOptions":
case "m_menuHelpSelectSource":
case "m_menuHelpCheckForUpdate":
m_UnlockedDbItems.AddFirst(stripItem);
break;
}
}
}
}
}
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;
}
}
}
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:
void NoDbMenuItem_EnabledChanged(object sender, EventArgs e)
{
var item = sender as ToolStripItem;
if (item != null)
{
if (HasNoDocs)
{
EnableNoDbItem(true, item);
}
else
{
EnableNoDbItem(IsAtLeastOneFileOpenAndUnlocked(), item);
}
}
}
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:
void MainWindow_FileOpened(object sender, KeePass.Forms.FileOpenedEventArgs e)
{
if (IsAtLeastOneFileOpenAndUnlocked())
{
foreach (var item in m_UnlockedDbItems)
{
EnableUnlockedDbItem(true, item);
}
foreach (var item in m_NoDocItems)
{
EnableNoDbItem(true, item);
}
}
}
void MainWindow_FileClosed(object sender, KeePass.Forms.FileClosedEventArgs e)
{
if (!IsAtLeastOneFileOpenAndUnlocked())
{
foreach (var item in m_UnlockedDbItems)
{
EnableUnlockedDbItem(false, item);
}
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:
void FileLockMenuItem_EnabledChanged(object sender, EventArgs e)
{
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
Toolbar
Main Menus
Related
Check out my other KeePass 2.X plugin: MinLock, a KeePass 2.x Plugin to keep minimized KeePass Locked