Merge gtkui branch to default.
authorRyan C. Gordon <icculus@icculus.org>
Wed, 14 Jun 2017 00:27:40 -0400
changeset 51 30d2d7598591
parent 44 2150bce729df (current diff)
parent 50 7bc981f5da6c (diff)
child 52 7f21abb9ff17
Merge gtkui branch to default.
--- a/1pass.c	Tue Mar 31 20:18:10 2015 -0400
+++ b/1pass.c	Wed Jun 14 00:27:40 2017 -0400
@@ -8,7 +8,7 @@
 #include <unistd.h>
 #include <dirent.h>
 #include <signal.h>
-
+#include <mntent.h>
 #include "lua.h"
 #include "lauxlib.h"
 #include "lualib.h"
@@ -16,8 +16,15 @@
 #include "aes.h"
 #include "base64.h"
 #include "md5.h"
+#include "sha256.h"
 #include "keyhook.h"
+
 #include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+#include <gdk/gdkkeysyms.h>
+#include <X11/Xlib.h>
+#include <X11/Xlibint.h>
 
 #define STATICARRAYLEN(x) ( (sizeof ((x))) / (sizeof ((x)[0])) )
 
@@ -184,6 +191,13 @@
 static const uint8_t zero16[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
 static const char saltprefix[] = { 'S', 'a', 'l', 't', 'e', 'd', '_', '_' };
 
+static int makeLuaCallback(lua_State *L, const int idx)
+{
+    assert(lua_isfunction(L, idx));
+    lua_pushvalue(L, idx);  // copy the Lua callback (luaL_ref() pops it).
+    return luaL_ref(L, LUA_REGISTRYINDEX);
+} // makeLuaCallback
+
 static inline int retvalStringBytes(lua_State *L, const uint8_t *str, size_t len)
 {
     if (str != NULL)
@@ -350,6 +364,24 @@
 } // decryptBase64UsingKey
 
 
+static void calcSha256(const BYTE *buf, const size_t len, BYTE *hash)
+{
+    SHA256_CTX sha256;
+    sha256_init(&sha256);
+    sha256_update(&sha256, buf, len);
+    sha256_final(&sha256, hash);
+} // calcSha256
+
+static int calcSha256_Lua(lua_State *L)
+{
+    size_t len = 0;
+    const char *str = luaL_checklstring(L, 1, &len);
+    BYTE hash[32];
+    calcSha256(str, len, hash);
+    return retvalStringBytes(L, hash, sizeof (hash));
+} // calcSha256_Lua
+
+
 static int runGuiPasswordPrompt(lua_State *L)
 {
     const char *hintstr = lua_tostring(L, 1);
@@ -375,6 +407,9 @@
     gtk_container_add(GTK_CONTAINER(content_area), entry);
 
     gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_MOUSE);
+    gtk_window_set_keep_above(GTK_WINDOW(dialog), TRUE);
+    gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
+    gtk_window_set_skip_pager_hint(GTK_WINDOW(dialog), TRUE);
     gtk_widget_show_all(dialog);
     gtk_window_set_keep_above(GTK_WINDOW(dialog), TRUE);
     const int ok = (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT);
@@ -392,67 +427,222 @@
     gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), str, -1);
 } // copyToClipboard
 
+static gboolean checkForEscapeKey(GtkWidget *widget, GdkEvent *event, gpointer arg)
+{
+    if ((event->type == GDK_KEY_PRESS) && (event->key.keyval == GDK_KEY_Escape))
+    {
+        // !!! FIXME: this is a little hacky
+        lua_getglobal(luaState, "escapePressed");
+        lua_call(luaState, 0, 0);
+        return TRUE;
+    } // if
 
-static int makeGuiMenu(lua_State *L)
+    return FALSE;  // pass this to other event handlers.
+} // checkForEscapeKey
+
+static gboolean wasSearchDeleteText = FALSE;  // HACK to workaround gtk+ nonsense.
+static void searchChanged(GtkEditable *editable, gpointer arg)
 {
-    return retvalPointer(L, gtk_menu_new());
-} // makeGuiMenu
+    const int callback = (int) ((size_t)arg);
+    GtkWidget *vbox = gtk_widget_get_parent(GTK_WIDGET(editable));
+    lua_rawgeti(luaState, LUA_REGISTRYINDEX, callback);
+    lua_pushlightuserdata(luaState, vbox);
+    lua_pushstring(luaState, gtk_entry_get_text(GTK_ENTRY(editable)));
+    lua_call(luaState, 2, 0);
+    gtk_widget_grab_focus(GTK_WIDGET(editable));
+    if (wasSearchDeleteText)  // HACK to workaround gtk+ nonsense.
+    {
+        gtk_editable_set_position(editable, -1);
+        wasSearchDeleteText = FALSE;
+    } // if
+} // searchChanged
 
+// HACK to workaround gtk+ nonsense.
+static void searchDeleteText(GtkEditable *editable, gint start_pos, gint end_pos, gpointer user_data)
+{
+    wasSearchDeleteText = TRUE;
+} // searchDeleteText
 
-static void clickedMenuItem(void *arg)
+static void destroyLuaCallback(const int callback)
 {
-    // This is the callback from GTK+; now call into our actual Lua callback!
-    const int callback = (int) ((size_t)arg);
-    lua_rawgeti(luaState, LUA_REGISTRYINDEX, callback);
+    //printf("unref callback %d\n", callback);
+    luaL_unref(luaState, LUA_REGISTRYINDEX, callback);
+} // destroyLuaCallback
+
+static void destroyCallback(GtkWidget *widget, gpointer arg)
+{
+    destroyLuaCallback((int) ((size_t)arg));
+} // destroyCallback
+
+static void destroyTopLevelMenu(GtkWidget *widget, gpointer arg)
+{
+    // !!! FIXME: hack
+    int *cbs = (int *) arg;
+    lua_rawgeti(luaState, LUA_REGISTRYINDEX, cbs[1]);
     lua_call(luaState, 0, 0);
+    destroyLuaCallback(cbs[0]);
+    destroyLuaCallback(cbs[1]);
+    free(cbs);
+} // destroyTopLevelMenu
+
+#if 0
+static gboolean
+mappedWindow(GtkWidget *widget, GdkEvent *event, gpointer user_data)
+{
+    GdkEventClient e;
+    memset(&e, '\0', sizeof (e));
+    e.type = GDK_CLIENT_EVENT;
+    e.window = gtk_widget_get_window(widget);
+    e.send_event = 1;
+    e.message_type = gdk_atom_intern_static_string("_NET_ACTIVE_WINDOW");
+    e.data_format = 32;
+    e.data.l[0] = 1;
+    e.data.l[1] = (long) gdk_x11_get_server_time(e.window);
+    e.data.l[2] = 0;
+
+    gdk_window_raise (e.window);
+    gdk_event_send_client_message((GdkEvent *) &e, gdk_x11_drawable_get_xid(gtk_widget_get_root_window(widget)));
+    return TRUE;
+}
+#endif
+
+static int guiCreateTopLevelMenu(lua_State *L)
+{
+    const char *title = luaL_checkstring(L, 1);
+    const int changedCallback = makeLuaCallback(L, 2);
+    const int destroyedCallback = makeLuaCallback(L, 3);
+
+    int *cbs = (int *) malloc(sizeof (int) * 2);  // !!! FIXME: hack
+    cbs[0] = changedCallback;
+    cbs[1] = destroyedCallback;
+
+    GtkWindow *window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
+    gtk_window_set_keep_above(window, TRUE);
+    gtk_window_set_skip_taskbar_hint(window, TRUE);
+    gtk_window_set_skip_pager_hint(window, TRUE);
+    g_signal_connect(window, "destroy", G_CALLBACK(destroyTopLevelMenu), cbs);
+    gtk_window_set_title(window, title);
+    gtk_window_set_position(window, GTK_WIN_POS_MOUSE);
+    gtk_window_set_decorated(window, FALSE);
+    gtk_window_set_resizable(window, FALSE);
+    GtkEntry *search = GTK_ENTRY(gtk_entry_new());
+    g_signal_connect(search, "key-press-event", G_CALLBACK(checkForEscapeKey), window);
+    gtk_entry_set_text(search, "Search...");
+    g_signal_connect(search, "changed", G_CALLBACK(searchChanged), (gpointer) ((size_t)changedCallback));
+    g_signal_connect(search, "delete-text", G_CALLBACK(searchDeleteText), 0); // HACK to workaround gtk+ nonsense.
+
+//    g_signal_connect(window, "map-event", G_CALLBACK(mappedWindow), NULL);
+
+
+    GtkVBox *vbox = GTK_VBOX(gtk_vbox_new(FALSE, 0));
+    gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(vbox));
+
+    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(search), FALSE, FALSE, 0);
+    gtk_widget_show(GTK_WIDGET(search));
+
+    GtkWidget *vsep = GTK_WIDGET(gtk_vseparator_new());
+    gtk_box_pack_start(GTK_BOX(vbox), vsep, FALSE, FALSE, 0);
+    gtk_widget_show(vsep);
+
+    return retvalPointer(L, vbox);
+} // guiCreateTopLevelMenu
+
+static void clickedMenuItem(GtkButton *button, gpointer arg)
+{
+    lua_rawgeti(luaState, LUA_REGISTRYINDEX, (int) ((size_t)arg));
+    lua_pushlightuserdata(luaState, button);
+    lua_call(luaState, 1, 0);
 } // clickedMenuItem
 
-#if 0  // !!! FIXME: figure out how to fire this.
-static void deletedMenuItem(void *arg)
+static int guiAddMenuItem(lua_State *L)
 {
-    // Clean up the Lua function we referenced in the Registry.
-    const int callback = (int) ((size_t)arg);
-printf("unref callback %d\n", callback);
-    luaL_unref(luaState, LUA_REGISTRYINDEX, callback);
-} // deletedMenuItem
-#endif
+    GtkWidget *vbox = (GtkWidget *) lua_touserdata(L, 1);
+    const char *label = luaL_checkstring(L, 2);
+    const int checked = lua_toboolean(L, 3);
+    const int callback = makeLuaCallback(L, 4);
 
-static int appendGuiMenuItem(lua_State *L)
-{
-    const int argc = lua_gettop(L);
-    GtkWidget *menu = (GtkWidget *) lua_touserdata(L, 1);
-    const char *label = luaL_checkstring(L, 2);
-    GtkWidget *item = gtk_menu_item_new_with_label(label);
-
-    if ((argc >= 3) && (!lua_isnil(L, 3)))
+    if (checked)
     {
-        assert(lua_isfunction(L, 3));
-        lua_pushvalue(L, 3);  // copy the Lua callback (luaL_ref() pops it).
-        const int callback = luaL_ref(L, LUA_REGISTRYINDEX);
-        gtk_signal_connect_object(GTK_OBJECT(item), "activate", GTK_SIGNAL_FUNC(clickedMenuItem), (gpointer) ((size_t)callback));
+        // !!! FIXME: this is pretty lousy.
+        const size_t len = strlen(label) + 5;
+        char *buf = (char *) alloca(len);
+        snprintf(buf, len, "[X] %s", label);
+        label = buf;
     } // if
 
+    GtkWidget *item = GTK_WIDGET(gtk_button_new_with_label(label));
+    g_signal_connect(item, "key-press-event", G_CALLBACK(checkForEscapeKey), NULL);
+    g_signal_connect(item, "clicked", G_CALLBACK(clickedMenuItem), (gpointer) ((size_t)callback));
+    g_signal_connect(item, "destroy", G_CALLBACK(destroyCallback), (gpointer) ((size_t)callback));
+
+    //gtk_button_set_image(button, gtk_image_new_from_stock(GTK_STOCK_OPEN, GTK_ICON_SIZE_MENU));
+    gtk_button_set_alignment (GTK_BUTTON(item), 0.0f, 0.5f);
+    gtk_button_set_relief(GTK_BUTTON(item), GTK_RELIEF_NONE);
+    gtk_box_pack_start(GTK_BOX(vbox), item, FALSE, FALSE, 0);
     gtk_widget_show(item);
-    gtk_menu_append(menu, item);
     return retvalPointer(L, item);
-} // appendGuiMenuItem
+} // guiAddMenuItem
 
+static int guiRemoveAllMenuItems(lua_State *L)
+{
+    GtkWidget *vbox = (GtkWidget *) lua_touserdata(L, 1);
+    GList *children = gtk_container_get_children(GTK_CONTAINER(vbox));
+    GList *iter;
 
-static int setGuiMenuItemSubmenu(lua_State *L)
+    gtk_widget_hide(vbox);
+    for (iter = children; iter != NULL; iter = g_list_next(iter))
+    {
+        if (G_OBJECT_TYPE(iter->data) == GTK_TYPE_BUTTON)
+             gtk_widget_destroy(GTK_WIDGET(iter->data));
+    } // for
+    g_list_free(children);
+
+    return 0;
+} // guiRemoveAllMenuItems
+
+static int guiDestroyMenu(lua_State *L)
 {
-    GtkMenuItem *item = (GtkMenuItem *) lua_touserdata(L, 1);
-    GtkWidget *submenu = (GtkWidget *) lua_touserdata(L, 2);
-    gtk_menu_item_set_submenu(item, submenu);
+    GtkWidget *widget = (GtkWidget *) lua_touserdata(L, 1);
+    gtk_widget_destroy(gtk_widget_get_toplevel(widget));
     return 0;
-} // setGuiMenuItemSubmenu
+} // guiDestroyMenu
 
+static int guiShowWindow(lua_State *L)
+{
+    GtkWidget *widget = (GtkWidget *) lua_touserdata(L, 1);
+    //gtk_container_resize_children(GTK_CONTAINER(vbox));
+    gtk_widget_show(widget);
+    GtkWidget *toplevel = gtk_widget_get_toplevel(widget);
+    gtk_window_present(GTK_WINDOW(toplevel));
+    return 0;
+} // guiShowWindow
 
-static int popupGuiMenu(lua_State *L)
+static int guiCreateSubMenu(lua_State *L)
 {
-    GtkMenu *menu = (GtkMenu *) lua_touserdata(L, 1);
-    gtk_menu_popup(menu, NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
-    return 0;
-} // popupGuiMenu
+    GtkWidget *widget = (GtkWidget *) lua_touserdata(L, 1);
+    GtkWidget *topwindow = gtk_widget_get_toplevel(widget);
+    GtkWindow *window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
+    gtk_window_set_keep_above(window, TRUE);
+    gtk_window_set_skip_taskbar_hint(window, TRUE);
+    gtk_window_set_skip_pager_hint(window, TRUE);
+    //g_signal_connect(window, "destroy", G_CALLBACK(destroySubMenu), topwindow);
+    gtk_window_set_decorated(window, FALSE);
+
+    GtkVBox *vbox = GTK_VBOX(gtk_vbox_new(FALSE, 0));
+    gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(vbox));
+
+    // line the new submenu up...
+    //  !!! FIXME: if overflow off right end of screen, go to the left of (widget) instead.
+    gint basex, basey, x, y;
+    gtk_window_get_position(GTK_WINDOW(topwindow), &basex, &basey);
+    gtk_widget_translate_coordinates(widget, topwindow, 0, 0, &x, &y);
+    x += basex;
+    y += basey;
+    x += widget->allocation.width;
+    gtk_window_move(window, x, y);
+
+    return retvalPointer(L, vbox);
+} // guiCreateSubMenu
 
 
 static int setPowermateLED_Lua(lua_State *L)
@@ -502,6 +692,29 @@
 } // giveControlToGui
 
 
+static int getMountedDisks(lua_State *L)
+{
+    lua_newtable(L);
+    int luai = 1;
+
+    FILE *mounts = setmntent("/etc/mtab", "r");
+    if (mounts != NULL)
+    {
+        struct mntent *ent = NULL;
+        while ((ent = getmntent(mounts)) != NULL)
+        {
+            lua_pushinteger(luaState, luai);
+            lua_pushstring(luaState, ent->mnt_dir);
+            lua_settable(luaState, -3);
+            luai++;
+        } // while
+        endmntent(mounts);
+    } // if
+
+    return 1;  // return the table.
+} // getMountedDisks
+
+
 static void *luaAlloc(void *ud, void *ptr, size_t osize, size_t nsize)
 {
     if (nsize == 0)
@@ -553,14 +766,19 @@
     // Set up initial C functions, etc we want to expose to Lua code...
     luaSetCFunc(luaState, decryptUsingPBKDF2, "decryptUsingPBKDF2");
     luaSetCFunc(luaState, decryptBase64UsingKey, "decryptBase64UsingKey");
-    luaSetCFunc(luaState, makeGuiMenu, "makeGuiMenu");
-    luaSetCFunc(luaState, appendGuiMenuItem, "appendGuiMenuItem");
-    luaSetCFunc(luaState, setGuiMenuItemSubmenu, "setGuiMenuItemSubmenu");
-    luaSetCFunc(luaState, popupGuiMenu, "popupGuiMenu");
     luaSetCFunc(luaState, giveControlToGui, "giveControlToGui");
     luaSetCFunc(luaState, runGuiPasswordPrompt, "runGuiPasswordPrompt");
     luaSetCFunc(luaState, copyToClipboard, "copyToClipboard");
     luaSetCFunc(luaState, setPowermateLED_Lua, "setPowermateLED");
+    luaSetCFunc(luaState, calcSha256_Lua, "calcSha256");
+    luaSetCFunc(luaState, getMountedDisks, "getMountedDisks");
+
+    luaSetCFunc(luaState, guiCreateTopLevelMenu, "guiCreateTopLevelMenu");
+    luaSetCFunc(luaState, guiCreateSubMenu, "guiCreateSubMenu");
+    luaSetCFunc(luaState, guiAddMenuItem, "guiAddMenuItem");
+    luaSetCFunc(luaState, guiRemoveAllMenuItems, "guiRemoveAllMenuItems");
+    luaSetCFunc(luaState, guiDestroyMenu, "guiDestroyMenu");
+    luaSetCFunc(luaState, guiShowWindow, "guiShowWindow");
 
     // Set up argv table...
     lua_newtable(luaState);
--- a/1pass.lua	Tue Mar 31 20:18:10 2015 -0400
+++ b/1pass.lua	Wed Jun 14 00:27:40 2017 -0400
@@ -6,6 +6,16 @@
 local items = nil
 local faveitems = nil
 local keyhookRunning = false
+local keyhookGuiMenus = nil
+
+
+local function runGarbageCollector()
+    --local memused = math.floor(collectgarbage("count") * 1024.0)
+    --print("Collecting garbage (currently using " .. memused .. " bytes).")
+    collectgarbage()
+    --local newmemused = math.floor(collectgarbage("count") * 1024.0)
+    --print("Now using " .. newmemused .. " bytes (" .. memused - newmemused .. " bytes savings).")
+end
 
 local passwordTypeNameMap = {
     ["webforms.WebForm"] = "Logins",
@@ -16,6 +26,8 @@
     ["wallet.government.DriversLicense"] = "Drivers licenses",
     ["system.Tombstone"] = "Dead items",
     ["securenotes.SecureNote"] = "Secure notes",
+    ["wallet.government.SsnUS"] = "Social Security Numbers",
+    ["wallet.computer.Router"] = "Router passwords",
     -- !!! FIXME: more!
 }
 
@@ -26,7 +38,9 @@
     "wallet.financial.BankAccountUS",
     "wallet.membership.Membership",
     "wallet.government.DriversLicense",
+    "wallet.government.SsnUS",
     "securenotes.SecureNote",
+    "wallet.computer.Router",
     -- never show "system.Tombstone",
     -- !!! FIXME: more!
 }
@@ -42,7 +56,7 @@
         return nil
     end
 
-    local str = f:read("*all")
+    local str = f:read("*a")
     f:close()
 
     return load_json_str(str, fname)
@@ -92,7 +106,7 @@
         return
     end
 
-    local str = "(hint is '" .. f:read("*all") .. "')."
+    local str = "(hint is '" .. f:read("*a") .. "')."
     f:close()
     --print(str)
     return str
@@ -103,6 +117,29 @@
     return load_json(basedir .. "/contents.js")
 end
 
+local function makeMenu()
+    return {}
+end
+
+local function appendMenuItem(menu, text, callback)
+    local item = {}
+    item["text"] = text
+    if callback ~= nil then
+        item["callback"] = callback
+    end
+    menu[#menu+1] = item
+    return item
+end
+
+local function setMenuItemSubmenu(menuitem, submenu)
+    menuitem["submenu"] = submenu
+end
+
+local function setMenuItemChecked(menuitem, ischecked)
+    menuitem["checked"] = ischecked
+end
+
+
 local function build_secret_menuitem(menu, type, str, hidden)
     if str == nil then
         return nil
@@ -117,9 +154,9 @@
     local callback = function()
         copyToClipboard(str)
         --print("Copied data [" .. str .. "] to clipboard.")
-        keyhookRunning = false
+        guiDestroyMenu(keyhookGuiMenus[1])
     end
-    return appendGuiMenuItem(menu, text, callback)
+    return appendMenuItem(menu, text, callback)
 end
 
 
@@ -262,11 +299,27 @@
 end
 secret_menuitem_builders["wallet.financial.CreditCard"] = build_secret_menuitem_creditcard
 
+
 local function build_secret_menuitem_securenote(menu, info, secure)
     build_secret_menuitem(menu, "Notes", secure.notesPlain, true)
 end
 secret_menuitem_builders["securenotes.SecureNote"] = build_secret_menuitem_securenote
 
+
+local function build_secret_menuitem_ssnus(menu, info, secure)
+    build_secret_menuitem(menu, "Name", secure.name, false)
+    build_secret_menuitem(menu, "SSN", secure.number, true)
+end
+secret_menuitem_builders["wallet.government.SsnUS"] = build_secret_menuitem_ssnus
+
+
+local function build_secret_menuitem_router(menu, info, secure)
+    build_secret_menuitem(menu, "Name", secure.name, false)
+    build_secret_menuitem(menu, "Password", secure.password, true)
+end
+secret_menuitem_builders["wallet.computer.Router"] = build_secret_menuitem_router
+
+
 local function build_secret_menuitems(info, menu)
     local metadata = load_json(basedir .. "/" .. info.uuid .. ".1password")
     if (metadata == nil) or (next(metadata) == nil) then  -- the "next" trick tests if table is empty.
@@ -294,7 +347,7 @@
     end
     --dumptable("secure " .. info.name, secure)
 
-    local menuitem = appendGuiMenuItem(menu, info.name)
+    local menuitem = appendMenuItem(menu, info.name)
 
     if secret_menuitem_builders[info.type] == nil then
         print("WARNING: don't know how to handle items of type " .. info.type)
@@ -307,9 +360,9 @@
         faveitems[metadata.faveIndex] = { info=info, secure=secure }
     end
 
-    local submenu = makeGuiMenu()
+    local submenu = makeMenu()
     secret_menuitem_builders[info.type](submenu, info, secure)
-    setGuiMenuItemSubmenu(menuitem, submenu)
+    setMenuItemSubmenu(menuitem, submenu)
 end
 
 local function prepItems()
@@ -336,9 +389,12 @@
     password = argv[2]  -- might be nil, don't reset if on command line.
     keys["SL5"] = nil
     passwordUnlockTime = nil
-    keyhookRunning = false
     setPowermateLED(false)
-    collectgarbage()
+
+    -- kill the popup if it exists.
+    if (keyhookGuiMenus ~= nil) and (keyhookGuiMenus[1] ~= nil) then
+        guiDestroyMenu(keyhookGuiMenus[1])
+    end
 end
 
 function pumpLua()  -- not local! Called from C!
@@ -354,15 +410,200 @@
     end
 end
 
+function escapePressed()  -- not local! Called from C!
+    if keyhookGuiMenus[1] then
+        guiDestroyMenu(keyhookGuiMenus[1])
+    end
+end
+
+
+local buildGuiMenuList
+
+local function spawnSubMenu(button, submenu, depth)
+    local guimenu = guiCreateSubMenu(button)
+
+    for i = #keyhookGuiMenus, depth, -1 do
+        if keyhookGuiMenus[i] then
+            --print("Destroying conflicting submenu at depth " .. i)
+            guiDestroyMenu(keyhookGuiMenus[i])
+            keyhookGuiMenus[i] = nil
+        end
+    end
+
+    --print("New submenu at depth " .. depth)
+    keyhookGuiMenus[depth] = guimenu
+
+    buildGuiMenuList(guimenu, submenu)
+    guiShowWindow(guimenu)
+end
+
+local function buildGuiMenuItem(guimenu, item)
+    local cb = item["callback"]
+    if cb == nil then
+        local submenu = item["submenu"]
+        local depth = #keyhookGuiMenus+1
+        cb = function (button)
+            return spawnSubMenu(button, submenu, depth)
+        end
+    end
+    guiAddMenuItem(guimenu, item["text"], item["checked"], cb)
+end
+
+buildGuiMenuList = function(guimenu, list)
+    for i,v in ipairs(list) do
+        buildGuiMenuItem(guimenu, v)
+    end
+end
+
+local function buildSearchResultsMenuCategory(guimenu, menu, str)
+    local submenu = menu["submenu"]
+    if not submenu then return end
+
+    local name = menu["text"]
+    -- !!! FIXME: hacky. We should really list favorites first anyhow.
+    if name == "Favorites" then return end
+
+    for i,v in ipairs(submenu) do
+        if string.find(string.lower(v["text"]), str, 1, true) ~= nil then
+            buildGuiMenuItem(guimenu, v)
+        end
+    end
+end
+
+local function buildSearchResultsMenuList(guimenu, topmenu, str)
+    for i,v in ipairs(topmenu) do
+        buildSearchResultsMenuCategory(guimenu, v, str)
+    end
+end
+
+local function searchEntryChanged(guimenu, str, topmenu)
+    --print("search changed to '" .. str .. "' ...")
+    guiRemoveAllMenuItems(guimenu)
+    if str == "" then
+        buildGuiMenuList(guimenu, topmenu)
+    else
+        buildSearchResultsMenuList(guimenu, topmenu, string.lower(str))
+    end
+    guiShowWindow(guimenu)
+end
+
+local function handleMenuDestroyed()
+    --print("Destroying main menu...")
+    for i,v in ipairs(keyhookGuiMenus) do
+        if i > 1 then
+            guiDestroyMenu(v)
+        end
+    end
+    keyhookGuiMenus = nil
+    keyhookRunning = false
+
+    runGarbageCollector()
+end
+
+local function launchGuiMenu(topmenu)
+    local guimenu = guiCreateTopLevelMenu("1pass",
+
+        function(guimenu, str) -- search text changed callback
+            return searchEntryChanged(guimenu, str, topmenu)
+        end,
+
+        function()  -- window destroyed callback
+            handleMenuDestroyed()
+        end
+    )
+    keyhookGuiMenus = {}
+    keyhookGuiMenus[#keyhookGuiMenus+1] = guimenu
+    buildGuiMenuList(guimenu, topmenu)
+    guiShowWindow(guimenu)
+end
+
+local trustedDisks = {}
+
+local function getTrustedDiskChecksumPath(mntpoint)
+    return mntpoint .. "/1pass.dat"
+end
+
+local function getTrustedDiskChecksum(mntpoint)
+    local f = io.open(getTrustedDiskChecksumPath(mntpoint), "rb")
+    if f == nil then
+        return nil
+    end
+
+    local str = f:read("*a")
+    f:close()
+    return calcSha256(str)
+end
+
+local function choseTrustedDisk(mntpoint)
+    if trustedDisks[mntpoint] ~= nil then
+        trustedDisks[mntpoint] = nil  -- no longer check existing trusted disk.
+    else
+        -- !!! FIXME: probably needs a message box if this fails.
+        local checksum = getTrustedDiskChecksum(mntpoint)
+        -- No checksum file yet? Generate and write out a random string.
+        if checksum == nil then
+            local f = io.open("/dev/urandom", "rb")
+            if f ~= nil then
+                local str = f:read(4096)
+                f:close()
+                if (str ~= nil) and (#str == 4096) then
+                    f = io.open(getTrustedDiskChecksumPath(mntpoint), "wb")
+                    if f ~= nil then
+                        if f:write(str) and f:flush() then
+                            checksum = calcSha256(str)
+                        end
+                        f:close()
+                    end
+                end
+            end
+        end
+        trustedDisks[mntpoint] = checksum
+    end
+
+    -- kill the popup if it exists.
+    -- !!! FIXME: put this in its own function, this is a copy/paste from elsewhere.
+    if (keyhookGuiMenus ~= nil) and (keyhookGuiMenus[1] ~= nil) then
+        guiDestroyMenu(keyhookGuiMenus[1])
+    end
+end
+
+local function buildTrustedDeviceMenu()
+    local menu = makeMenu()
+    local disks = getMountedDisks()  -- this is a C function.
+
+    table.sort(disks, function(a, b) return a < b end)
+    for i,v in ipairs(disks) do
+        local item = appendMenuItem(menu, v, function() choseTrustedDisk(v) end)
+        if trustedDisks[v] ~= nil then
+            setMenuItemChecked(item, true)
+        end
+    end
+
+    return menu
+end
 
 function keyhookPressed()  -- not local! Called from C!
---print("keyhookPressed: running==" .. tostring(keyhookRunning))
---    if keyhookRunning then
---        return
---    end
+    --print("keyhookPressed: running==" .. tostring(keyhookRunning))
+    if keyhookRunning then
+        return
+    end
 
     keyhookRunning = true
 
+    local allowaccess = true;
+    for mntpoint,checksum in pairs(trustedDisks) do
+        if getTrustedDiskChecksum(mntpoint) ~= checksum then
+            allowaccess = false
+            break
+        end
+    end
+
+    if not allowaccess then
+        -- !!! FIXME: probably needs a message box if this happens.
+        keyhookRunning = false
+        return
+    end
+
     while password == nil do
         password = runGuiPasswordPrompt(getHint())
         if password == nil then
@@ -387,13 +628,16 @@
         return
     end
 
-    local topmenu = makeGuiMenu()
-    local favesmenu = makeGuiMenu()
+    local topmenu = makeMenu()
+    local favesmenu = makeMenu()
+    local securitymenu = makeMenu()
     faveitems = {}
 
-    setGuiMenuItemSubmenu(appendGuiMenuItem(topmenu, "Favorites"), favesmenu)
+    setMenuItemSubmenu(appendMenuItem(topmenu, "Favorites"), favesmenu)
+    setMenuItemSubmenu(appendMenuItem(topmenu, "Security"), securitymenu)
 
-    appendGuiMenuItem(topmenu, "Lock keychain", function() lockKeychain() end)
+    appendMenuItem(securitymenu, "Lock keychain now", function() lockKeychain() end)
+    setMenuItemSubmenu(appendMenuItem(securitymenu, "Require trusted device"), buildTrustedDeviceMenu())
 
     for orderi,type in ipairs(passwordTypeOrdering) do
         local bucket = items[type]
@@ -402,13 +646,13 @@
             if realname == nil then
                 realname = type
             end
-            local menuitem = appendGuiMenuItem(topmenu, realname)
-            local submenu = makeGuiMenu()
+            local menuitem = appendMenuItem(topmenu, realname)
+            local submenu = makeMenu()
             table.sort(bucket, function(a, b) return a.name < b.name end)
             for i,v in pairs(bucket) do
                 build_secret_menuitems(v, submenu)
             end
-            setGuiMenuItemSubmenu(menuitem, submenu)
+            setMenuItemSubmenu(menuitem, submenu)
         else
             --print("no bucket found for item type '" .. type .. "'")
         end
@@ -433,16 +677,16 @@
 
     for i,v in favepairs(faveitems) do
         --dumptable("fave " .. i, v)
-        local menuitem = appendGuiMenuItem(favesmenu, v.info.name)
-        local submenu = makeGuiMenu()
+        local menuitem = appendMenuItem(favesmenu, v.info.name)
+        local submenu = makeMenu()
         secret_menuitem_builders[v.info.type](submenu, v.info, v.secure)
-        setGuiMenuItemSubmenu(menuitem, submenu)
+        setMenuItemSubmenu(menuitem, submenu)
     end
 
     favepairs = nil
     faveitems = nil
 
-    popupGuiMenu(topmenu)
+    launchGuiMenu(topmenu)
 end
 
 
--- a/CMakeLists.txt	Tue Mar 31 20:18:10 2015 -0400
+++ b/CMakeLists.txt	Wed Jun 14 00:27:40 2017 -0400
@@ -80,6 +80,7 @@
     aes.c
     md5.c
     sha1.c
+    sha256.c
     base64.c
     lua/lapi.c
     lua/ldebug.c
--- a/LICENSE.txt	Tue Mar 31 20:18:10 2015 -0400
+++ b/LICENSE.txt	Wed Jun 14 00:27:40 2017 -0400
@@ -1,5 +1,5 @@
 
-   Copyright (c) 2013 Ryan C. Gordon.
+   Copyright (c) 2013-2016 Ryan C. Gordon.
 
    This software is provided 'as-is', without any express or implied warranty.
    In no event will the authors be held liable for any damages arising from
--- a/keyhook.c	Tue Mar 31 20:18:10 2015 -0400
+++ b/keyhook.c	Wed Jun 14 00:27:40 2017 -0400
@@ -1,6 +1,8 @@
 // !!! FIXME: this is X11 specific.  :(
 
 #include <stdio.h>
+
+#if 1
 #include <stdlib.h>
 #include <string.h>
 #include <pthread.h>
@@ -19,9 +21,12 @@
     const xEvent *xev = (const xEvent *) data->data;
     if (data->category == XRecordFromServer)
     {
-        const BYTE keycode = xev->u.u.detail;
         if (xev->u.u.type == KeyPress)
         {
+            const BYTE keycode = xev->u.u.detail;
+
+            //printf("Pressed X11 keycode %u\n", (unsigned int) keycode);
+
             // !!! FIXME: don't hardcode these keycodes.
             if ((keycode == 64) && (keyPressFlags == 0))
                 keyPressFlags++;
@@ -130,5 +135,21 @@
     return sawKeyCombo;
 } // pumpKeyHook
 
+#else
+
+int initKeyHook(void) { return 1; }
+void deinitKeyHook(void) {}
+
+int pumpKeyHook(void)
+{
+static int x = 50;
+const int retval = (x == 50);
+if (++x > 50) x = 0;
+if (retval) printf("fire it!\n");
+return retval;
+}
+
+#endif
+
 // end of keyhook.c ...
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sha256.c	Wed Jun 14 00:27:40 2017 -0400
@@ -0,0 +1,158 @@
+/*********************************************************************
+* Filename:   sha256.c
+* Author:     Brad Conte (brad AT bradconte.com)
+* Copyright:
+* Disclaimer: This code is presented "as is" without any guarantees.
+* Details:    Implementation of the SHA-256 hashing algorithm.
+              SHA-256 is one of the three algorithms in the SHA2
+              specification. The others, SHA-384 and SHA-512, are not
+              offered in this implementation.
+              Algorithm specification can be found here:
+               * http://csrc.nist.gov/publications/fips/fips180-2/fips180-2withchangenotice.pdf
+              This implementation uses little endian byte order.
+*********************************************************************/
+
+/*************************** HEADER FILES ***************************/
+#include <stdlib.h>
+#include <memory.h>
+#include "sha256.h"
+
+/****************************** MACROS ******************************/
+#define ROTLEFT(a,b) (((a) << (b)) | ((a) >> (32-(b))))
+#define ROTRIGHT(a,b) (((a) >> (b)) | ((a) << (32-(b))))
+
+#define CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
+#define MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
+#define EP0(x) (ROTRIGHT(x,2) ^ ROTRIGHT(x,13) ^ ROTRIGHT(x,22))
+#define EP1(x) (ROTRIGHT(x,6) ^ ROTRIGHT(x,11) ^ ROTRIGHT(x,25))
+#define SIG0(x) (ROTRIGHT(x,7) ^ ROTRIGHT(x,18) ^ ((x) >> 3))
+#define SIG1(x) (ROTRIGHT(x,17) ^ ROTRIGHT(x,19) ^ ((x) >> 10))
+
+/**************************** VARIABLES *****************************/
+static const WORD k[64] = {
+	0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
+	0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
+	0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
+	0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
+	0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
+	0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
+	0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
+	0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
+};
+
+/*********************** FUNCTION DEFINITIONS ***********************/
+void sha256_transform(SHA256_CTX *ctx, const BYTE data[])
+{
+	WORD a, b, c, d, e, f, g, h, i, j, t1, t2, m[64];
+
+	for (i = 0, j = 0; i < 16; ++i, j += 4)
+		m[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | (data[j + 3]);
+	for ( ; i < 64; ++i)
+		m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16];
+
+	a = ctx->state[0];
+	b = ctx->state[1];
+	c = ctx->state[2];
+	d = ctx->state[3];
+	e = ctx->state[4];
+	f = ctx->state[5];
+	g = ctx->state[6];
+	h = ctx->state[7];
+
+	for (i = 0; i < 64; ++i) {
+		t1 = h + EP1(e) + CH(e,f,g) + k[i] + m[i];
+		t2 = EP0(a) + MAJ(a,b,c);
+		h = g;
+		g = f;
+		f = e;
+		e = d + t1;
+		d = c;
+		c = b;
+		b = a;
+		a = t1 + t2;
+	}
+
+	ctx->state[0] += a;
+	ctx->state[1] += b;
+	ctx->state[2] += c;
+	ctx->state[3] += d;
+	ctx->state[4] += e;
+	ctx->state[5] += f;
+	ctx->state[6] += g;
+	ctx->state[7] += h;
+}
+
+void sha256_init(SHA256_CTX *ctx)
+{
+	ctx->datalen = 0;
+	ctx->bitlen = 0;
+	ctx->state[0] = 0x6a09e667;
+	ctx->state[1] = 0xbb67ae85;
+	ctx->state[2] = 0x3c6ef372;
+	ctx->state[3] = 0xa54ff53a;
+	ctx->state[4] = 0x510e527f;
+	ctx->state[5] = 0x9b05688c;
+	ctx->state[6] = 0x1f83d9ab;
+	ctx->state[7] = 0x5be0cd19;
+}
+
+void sha256_update(SHA256_CTX *ctx, const BYTE data[], size_t len)
+{
+	WORD i;
+
+	for (i = 0; i < len; ++i) {
+		ctx->data[ctx->datalen] = data[i];
+		ctx->datalen++;
+		if (ctx->datalen == 64) {
+			sha256_transform(ctx, ctx->data);
+			ctx->bitlen += 512;
+			ctx->datalen = 0;
+		}
+	}
+}
+
+void sha256_final(SHA256_CTX *ctx, BYTE hash[])
+{
+	WORD i;
+
+	i = ctx->datalen;
+
+	// Pad whatever data is left in the buffer.
+	if (ctx->datalen < 56) {
+		ctx->data[i++] = 0x80;
+		while (i < 56)
+			ctx->data[i++] = 0x00;
+	}
+	else {
+		ctx->data[i++] = 0x80;
+		while (i < 64)
+			ctx->data[i++] = 0x00;
+		sha256_transform(ctx, ctx->data);
+		memset(ctx->data, 0, 56);
+	}
+
+	// Append to the padding the total message's length in bits and transform.
+	ctx->bitlen += ctx->datalen * 8;
+	ctx->data[63] = ctx->bitlen;
+	ctx->data[62] = ctx->bitlen >> 8;
+	ctx->data[61] = ctx->bitlen >> 16;
+	ctx->data[60] = ctx->bitlen >> 24;
+	ctx->data[59] = ctx->bitlen >> 32;
+	ctx->data[58] = ctx->bitlen >> 40;
+	ctx->data[57] = ctx->bitlen >> 48;
+	ctx->data[56] = ctx->bitlen >> 56;
+	sha256_transform(ctx, ctx->data);
+
+	// Since this implementation uses little endian byte ordering and SHA uses big endian,
+	// reverse all the bytes when copying the final state to the output hash.
+	for (i = 0; i < 4; ++i) {
+		hash[i]      = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff;
+		hash[i + 4]  = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff;
+		hash[i + 8]  = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff;
+		hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff;
+		hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff;
+		hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff;
+		hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff;
+		hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sha256.h	Wed Jun 14 00:27:40 2017 -0400
@@ -0,0 +1,34 @@
+/*********************************************************************
+* Filename:   sha256.h
+* Author:     Brad Conte (brad AT bradconte.com)
+* Copyright:
+* Disclaimer: This code is presented "as is" without any guarantees.
+* Details:    Defines the API for the corresponding SHA1 implementation.
+*********************************************************************/
+
+#ifndef SHA256_H
+#define SHA256_H
+
+/*************************** HEADER FILES ***************************/
+#include <stddef.h>
+
+/****************************** MACROS ******************************/
+#define SHA256_BLOCK_SIZE 32            // SHA256 outputs a 32 byte digest
+
+/**************************** DATA TYPES ****************************/
+typedef unsigned char BYTE;             // 8-bit byte
+typedef unsigned int  WORD;             // 32-bit word, change to "long" for 16-bit machines
+
+typedef struct {
+	BYTE data[64];
+	WORD datalen;
+	unsigned long long bitlen;
+	WORD state[8];
+} SHA256_CTX;
+
+/*********************** FUNCTION DECLARATIONS **********************/
+void sha256_init(SHA256_CTX *ctx);
+void sha256_update(SHA256_CTX *ctx, const BYTE data[], size_t len);
+void sha256_final(SHA256_CTX *ctx, BYTE hash[]);
+
+#endif   // SHA256_H