1pass.lua
author Ryan C. Gordon <icculus@icculus.org>
Sun, 18 Jun 2017 19:40:30 -0400
changeset 56 a573346e6f7b
parent 49 2c57a0ad7c8f
permissions -rw-r--r--
Added One Time Password support.

This is only for time-based OTP for now ("TOPT" algorithm), but that's more
or less what one expects to see in the wild anyhow.

This is sort of a placeholder UI until I replace the entire existing UI with
something better.
icculus@0
     1
JSON = (loadfile "JSON.lua")()
icculus@12
     2
dofile("dumptable.lua")
icculus@12
     3
icculus@17
     4
local basedir = "1Password/1Password.agilekeychain/data/default"  -- !!! FIXME
icculus@17
     5
local password = argv[2]
icculus@17
     6
local items = nil
icculus@35
     7
local faveitems = nil
icculus@17
     8
local keyhookRunning = false
icculus@45
     9
local keyhookGuiMenus = nil
icculus@45
    10
icculus@45
    11
icculus@45
    12
local function runGarbageCollector()
icculus@45
    13
    --local memused = math.floor(collectgarbage("count") * 1024.0)
icculus@45
    14
    --print("Collecting garbage (currently using " .. memused .. " bytes).")
icculus@45
    15
    collectgarbage()
icculus@45
    16
    --local newmemused = math.floor(collectgarbage("count") * 1024.0)
icculus@45
    17
    --print("Now using " .. newmemused .. " bytes (" .. memused - newmemused .. " bytes savings).")
icculus@45
    18
end
icculus@17
    19
icculus@12
    20
local passwordTypeNameMap = {
icculus@12
    21
    ["webforms.WebForm"] = "Logins",
icculus@12
    22
    ["wallet.financial.CreditCard"] = "Credit cards",
icculus@12
    23
    ["passwords.Password"] = "Passwords",
icculus@12
    24
    ["wallet.financial.BankAccountUS"] = "Bank accounts",
icculus@12
    25
    ["wallet.membership.Membership"] = "Memberships",
icculus@12
    26
    ["wallet.government.DriversLicense"] = "Drivers licenses",
icculus@12
    27
    ["system.Tombstone"] = "Dead items",
icculus@25
    28
    ["securenotes.SecureNote"] = "Secure notes",
icculus@49
    29
    ["wallet.government.SsnUS"] = "Social Security Numbers",
icculus@49
    30
    ["wallet.computer.Router"] = "Router passwords",
icculus@12
    31
    -- !!! FIXME: more!
icculus@12
    32
}
icculus@12
    33
icculus@12
    34
local passwordTypeOrdering = {
icculus@12
    35
    "webforms.WebForm",
icculus@12
    36
    "wallet.financial.CreditCard",
icculus@12
    37
    "passwords.Password",
icculus@12
    38
    "wallet.financial.BankAccountUS",
icculus@12
    39
    "wallet.membership.Membership",
icculus@12
    40
    "wallet.government.DriversLicense",
icculus@49
    41
    "wallet.government.SsnUS",
icculus@25
    42
    "securenotes.SecureNote",
icculus@49
    43
    "wallet.computer.Router",
icculus@12
    44
    -- never show "system.Tombstone",
icculus@12
    45
    -- !!! FIXME: more!
icculus@12
    46
}
icculus@0
    47
icculus@6
    48
local function load_json_str(str, desc)
icculus@6
    49
    local retval = JSON:decode(str)
icculus@6
    50
    return retval
icculus@6
    51
end
icculus@6
    52
icculus@0
    53
local function load_json(fname)
icculus@0
    54
    local f = io.open(fname, "rb")
icculus@0
    55
    if (f == nil) then
icculus@0
    56
        return nil
icculus@0
    57
    end
icculus@0
    58
icculus@46
    59
    local str = f:read("*a")
icculus@0
    60
    f:close()
icculus@0
    61
icculus@6
    62
    return load_json_str(str, fname)
icculus@0
    63
end
icculus@0
    64
icculus@0
    65
icculus@5
    66
local keys = {}
icculus@17
    67
local function loadKey(level, password)
icculus@5
    68
    if keys[level] ~= nil then
icculus@5
    69
        return keys[level]
icculus@5
    70
    end
icculus@5
    71
icculus@12
    72
    local keysjson = load_json(basedir .. "/encryptionKeys.js")
icculus@0
    73
    if (keysjson == nil) or (keysjson[level] == nil) then
icculus@0
    74
        return nil
icculus@0
    75
    end
icculus@0
    76
icculus@0
    77
    local identifier = keysjson[level]
icculus@0
    78
    for i,v in ipairs(keysjson.list) do
icculus@0
    79
        if v.identifier == identifier then
icculus@0
    80
			local iterations = v.iterations
icculus@0
    81
            if (iterations == nil) or (iterations < 1000) then
icculus@0
    82
			    iterations = 1000
icculus@0
    83
            end
icculus@0
    84
icculus@0
    85
			local decrypted = decryptUsingPBKDF2(v.data, password, iterations)
icculus@0
    86
			if decrypted == nil then
icculus@0
    87
                return nil
icculus@0
    88
            end
icculus@0
    89
icculus@0
    90
			local validate = decryptBase64UsingKey(v.validation, decrypted)
icculus@0
    91
			if validate ~= decrypted then
icculus@0
    92
                return nil
icculus@0
    93
            end
icculus@0
    94
icculus@5
    95
            keys[level] = decrypted
icculus@0
    96
            return decrypted
icculus@0
    97
        end
icculus@0
    98
    end
icculus@0
    99
icculus@0
   100
    return nil
icculus@0
   101
end
icculus@0
   102
icculus@17
   103
local function getHint()
icculus@0
   104
    local f = io.open(basedir .. "/.password.hint", "r")
icculus@0
   105
    if (f == nil) then
icculus@0
   106
        return
icculus@0
   107
    end
icculus@0
   108
icculus@46
   109
    local str = "(hint is '" .. f:read("*a") .. "')."
icculus@0
   110
    f:close()
icculus@12
   111
    --print(str)
icculus@12
   112
    return str
icculus@0
   113
end
icculus@0
   114
icculus@1
   115
icculus@17
   116
local function loadContents()
icculus@12
   117
    return load_json(basedir .. "/contents.js")
icculus@6
   118
end
icculus@6
   119
icculus@45
   120
local function makeMenu()
icculus@45
   121
    return {}
icculus@45
   122
end
icculus@45
   123
icculus@45
   124
local function appendMenuItem(menu, text, callback)
icculus@45
   125
    local item = {}
icculus@45
   126
    item["text"] = text
icculus@45
   127
    if callback ~= nil then
icculus@45
   128
        item["callback"] = callback
icculus@45
   129
    end
icculus@45
   130
    menu[#menu+1] = item
icculus@45
   131
    return item
icculus@45
   132
end
icculus@45
   133
icculus@45
   134
local function setMenuItemSubmenu(menuitem, submenu)
icculus@45
   135
    menuitem["submenu"] = submenu
icculus@45
   136
end
icculus@45
   137
icculus@46
   138
local function setMenuItemChecked(menuitem, ischecked)
icculus@46
   139
    menuitem["checked"] = ischecked
icculus@46
   140
end
icculus@45
   141
icculus@45
   142
icculus@56
   143
local function build_secret_menuitem(menu, type, str, hidden, transform)
icculus@12
   144
    if str == nil then
icculus@12
   145
        return nil
icculus@8
   146
    end
icculus@12
   147
icculus@12
   148
    local valuestr = str
icculus@12
   149
    if hidden == true then
icculus@12
   150
        valuestr = "*****"
icculus@12
   151
    end
icculus@12
   152
    local text = type .. " " .. valuestr
icculus@12
   153
icculus@12
   154
    local callback = function()
icculus@56
   155
        if transform ~= nil then
icculus@56
   156
            str = transform(str)
icculus@56
   157
        end
icculus@12
   158
        copyToClipboard(str)
icculus@12
   159
        --print("Copied data [" .. str .. "] to clipboard.")
icculus@45
   160
        guiDestroyMenu(keyhookGuiMenus[1])
icculus@12
   161
    end
icculus@45
   162
    return appendMenuItem(menu, text, callback)
icculus@12
   163
end
icculus@12
   164
icculus@56
   165
local function transform_otp(str)
icculus@56
   166
    local algorithm, name, argstr = string.match(str, "^otpauth://(.-)/(.-)?(.+)$")
icculus@56
   167
    if algorithm == nil then algorithm = "" end
icculus@56
   168
    if algorithm ~= "totp" then
icculus@56
   169
        print("FIXME: don't know how to handle One Time Passwords using the '" .. algorithm .. "' algorithm!")
icculus@56
   170
        return "000000"
icculus@56
   171
    end
icculus@56
   172
icculus@56
   173
    local args = {}
icculus@56
   174
    while argstr ~= nil and argstr ~= "" do
icculus@56
   175
        local arg
icculus@56
   176
        local idx = string.find(argstr, "&")
icculus@56
   177
        if idx == nil then
icculus@56
   178
            arg = argstr
icculus@56
   179
            argstr = nil
icculus@56
   180
        else
icculus@56
   181
            arg = string.sub(argstr, 0, idx);
icculus@56
   182
            argstr = string.sub(argstr, idx + 1);
icculus@56
   183
        end
icculus@56
   184
icculus@56
   185
        local key, val = string.match(arg, "^(.-)=(.*)$")
icculus@56
   186
        if (key ~= nil) and (val ~= nil) then
icculus@56
   187
            args[key] = val
icculus@56
   188
        end
icculus@56
   189
    end
icculus@56
   190
icculus@56
   191
    --dumptable("otpauth://" .. algorithm .. "/" .. name, args);
icculus@56
   192
icculus@56
   193
    if args["secret"] == nil then
icculus@56
   194
        print("FIXME: this One Time Password doesn't seem to have a secret key!")
icculus@56
   195
        return "000000"
icculus@56
   196
    end
icculus@56
   197
icculus@56
   198
    local retval = decryptTopt(args["secret"]);
icculus@56
   199
    if retval == nil then
icculus@56
   200
        print("FIXME: failed to generate One Time Password; is the secret key bogus?")
icculus@56
   201
        retval = "000000"
icculus@56
   202
    end
icculus@56
   203
    return retval
icculus@56
   204
end
icculus@56
   205
icculus@12
   206
icculus@12
   207
local secret_menuitem_builders = {}
icculus@12
   208
icculus@12
   209
local function build_secret_menuitem_webform(menu, info, secure)
icculus@12
   210
    local addthis = false
icculus@12
   211
    local username = nil
icculus@12
   212
    local password = nil
icculus@56
   213
    local otp = nil
icculus@32
   214
    local designated_password = nil
icculus@32
   215
    local designated_username = nil
icculus@12
   216
    local email = nil
robbie@21
   217
icculus@24
   218
    if secure.fields == nil then
icculus@24
   219
      print("no secure fields, don't know how to handle this item") 
icculus@24
   220
      return
icculus@24
   221
    end
icculus@24
   222
icculus@12
   223
    for i,v in ipairs(secure.fields) do
icculus@12
   224
        --print(info.name .. ": " .. v.type .. ", " .. v.value)
icculus@12
   225
        local ignored = false
icculus@32
   226
        if (v.value == nil) or (v.value == "") then
icculus@32
   227
            ignored = true
icculus@32
   228
        elseif (v.designation ~= nil) and (v.designation == "password") then
icculus@32
   229
            designated_password = v.value
icculus@32
   230
        elseif (v.designation ~= nil) and (v.designation == "username") then
icculus@32
   231
            designated_username = v.value
icculus@32
   232
        elseif (v.type == "P") then
icculus@12
   233
            password = v.value
icculus@32
   234
        elseif (v.type == "T") then
icculus@12
   235
            username = v.value
icculus@32
   236
        elseif (v.type == "E") then
icculus@12
   237
            email = v.value
icculus@12
   238
        else
icculus@12
   239
            ignored = true
icculus@12
   240
        end
icculus@12
   241
icculus@12
   242
        if not ignored then
icculus@12
   243
            addthis = true
icculus@12
   244
        end
icculus@12
   245
    end
icculus@12
   246
icculus@56
   247
    -- this is probably all wrong.
icculus@56
   248
    if secure.sections ~= nil then
icculus@56
   249
        for i,v in ipairs(secure.sections) do
icculus@56
   250
            if v.fields ~= nil then
icculus@56
   251
                for i2,v2 in ipairs(v.fields) do
icculus@56
   252
                    if (type(v2.v) == "string") and (string.sub(v2.v, 0, 10) == "otpauth://") then
icculus@56
   253
                        otp = v2.v
icculus@56
   254
                        addthis = true
icculus@56
   255
                    end
icculus@56
   256
                end
icculus@56
   257
            end
icculus@56
   258
        end
icculus@56
   259
    end
icculus@56
   260
icculus@12
   261
    if addthis then
icculus@32
   262
        -- designated fields always win out.
icculus@32
   263
        if (designated_username ~= nil) then
icculus@32
   264
            username = designated_username
icculus@32
   265
        end
icculus@32
   266
icculus@32
   267
        if (designated_password ~= nil) then
icculus@32
   268
            password = designated_password
icculus@32
   269
        end
icculus@32
   270
icculus@12
   271
        if (username ~= nil) and (email ~= nil) and (email == username) then
icculus@12
   272
            email = nil
icculus@12
   273
        end
icculus@12
   274
icculus@12
   275
        build_secret_menuitem(menu, "username", username)
icculus@12
   276
        build_secret_menuitem(menu, "email", email)
icculus@12
   277
        build_secret_menuitem(menu, "password", password, true)
icculus@56
   278
        build_secret_menuitem(menu, "otp", otp, true, transform_otp)
icculus@12
   279
    end
icculus@12
   280
end
icculus@12
   281
secret_menuitem_builders["webforms.WebForm"] = build_secret_menuitem_webform
icculus@12
   282
icculus@12
   283
icculus@12
   284
local function build_secret_menuitem_password(menu, info, secure)
icculus@12
   285
    build_secret_menuitem(menu, "password", secure.password, true)
icculus@12
   286
end
icculus@12
   287
secret_menuitem_builders["passwords.Password"] = build_secret_menuitem_password
icculus@12
   288
icculus@12
   289
icculus@12
   290
local function build_secret_menuitem_bankacctus(menu, info, secure)
icculus@12
   291
    -- !!! FIXME: there's more data than this in a generic dictionary.
icculus@12
   292
    build_secret_menuitem(menu, "Account type", secure.accountType)
icculus@12
   293
    build_secret_menuitem(menu, "Routing number", secure.routingNo)
icculus@12
   294
    build_secret_menuitem(menu, "Account number", secure.accountNo)
icculus@12
   295
    build_secret_menuitem(menu, "Bank name", secure.bankName)
icculus@12
   296
    build_secret_menuitem(menu, "Owner", secure.owner)
icculus@41
   297
    build_secret_menuitem(menu, "SWIFT code", secure.swift)
icculus@41
   298
    build_secret_menuitem(menu, "PIN", secure.telephonePin)
icculus@12
   299
end
icculus@12
   300
secret_menuitem_builders["wallet.financial.BankAccountUS"] = build_secret_menuitem_bankacctus
icculus@12
   301
icculus@12
   302
icculus@12
   303
local function build_secret_menuitem_driverslic(menu, info, secure)
icculus@42
   304
    -- !!! FIXME: there's more data for this menuitem than this, in a generic dictionary.
icculus@42
   305
icculus@42
   306
    local birthdate = nil
icculus@42
   307
    if secure.birthdate_yy ~= nil then
icculus@42
   308
        birthdate = secure.birthdate_yy
icculus@42
   309
        if secure.birthdate_mm ~= nil then
icculus@42
   310
            birthdate = birthdate .. "/" .. string.sub("00" .. secure.birthdate_mm, -2)
icculus@42
   311
            if secure.birthdate_dd ~= nil then
icculus@42
   312
                birthdate = birthdate .. "/" .. string.sub("00" .. secure.birthdate_dd, -2)
icculus@42
   313
            end
icculus@42
   314
        end
icculus@42
   315
    end
icculus@42
   316
icculus@43
   317
    local expiredate = nil
icculus@42
   318
    if secure.expiry_date_yy ~= nil then
icculus@42
   319
        expiredate = secure.expiry_date_yy
icculus@42
   320
        if secure.expiry_date_mm ~= nil then
icculus@42
   321
            expiredate = expiredate .. "/" .. string.sub("00" .. secure.expiry_date_mm, -2)
icculus@42
   322
            if secure.expiry_date_dd ~= nil then
icculus@42
   323
                expiredate = expiredate .. "/" .. string.sub("00" .. secure.expiry_date_dd, -2)
icculus@42
   324
            end
icculus@42
   325
        end
icculus@42
   326
    end
icculus@42
   327
icculus@12
   328
    build_secret_menuitem(menu, "License number", secure.number)
icculus@12
   329
    build_secret_menuitem(menu, "Class", secure.class)
icculus@12
   330
    build_secret_menuitem(menu, "Expires", expiredate)
icculus@12
   331
    build_secret_menuitem(menu, "State", secure.state)
icculus@12
   332
    build_secret_menuitem(menu, "Country", secure.country)
icculus@12
   333
    build_secret_menuitem(menu, "Conditions", secure.conditions)
icculus@12
   334
    build_secret_menuitem(menu, "Full name", secure.fullname)
icculus@12
   335
    build_secret_menuitem(menu, "Address", secure.address)
icculus@12
   336
    build_secret_menuitem(menu, "Gender", secure.sex)
icculus@12
   337
    build_secret_menuitem(menu, "Birthdate", birthdate)
icculus@12
   338
    build_secret_menuitem(menu, "Height", secure.height)
icculus@12
   339
end
icculus@12
   340
secret_menuitem_builders["wallet.government.DriversLicense"] = build_secret_menuitem_driverslic
icculus@12
   341
icculus@12
   342
icculus@12
   343
local function build_secret_menuitem_membership(menu, info, secure)
icculus@12
   344
    -- !!! FIXME: there's more data than this in a generic dictionary.
icculus@12
   345
    build_secret_menuitem(menu, "Membership number", secure.membership_no)
icculus@12
   346
end
icculus@12
   347
secret_menuitem_builders["wallet.membership.Membership"] = build_secret_menuitem_membership
icculus@12
   348
icculus@12
   349
icculus@12
   350
local function build_secret_menuitem_creditcard(menu, info, secure)
icculus@12
   351
    -- !!! FIXME: there's more data than this in a generic dictionary.
icculus@12
   352
    local expiredate = secure.expiry_yy .. "/" .. string.sub("00" .. secure.expiry_mm, -2)
icculus@12
   353
    build_secret_menuitem(menu, "Type", secure.type)
icculus@12
   354
    build_secret_menuitem(menu, "CC number", secure.ccnum, true)
icculus@12
   355
    build_secret_menuitem(menu, "CVV", secure.cvv, true)
icculus@38
   356
    build_secret_menuitem(menu, "Expires", expiredate)
icculus@12
   357
    build_secret_menuitem(menu, "Card holder", secure.cardholder)
icculus@12
   358
    build_secret_menuitem(menu, "Bank", secure.bank)
icculus@12
   359
end
icculus@12
   360
secret_menuitem_builders["wallet.financial.CreditCard"] = build_secret_menuitem_creditcard
icculus@12
   361
icculus@49
   362
icculus@25
   363
local function build_secret_menuitem_securenote(menu, info, secure)
icculus@25
   364
    build_secret_menuitem(menu, "Notes", secure.notesPlain, true)
icculus@25
   365
end
icculus@25
   366
secret_menuitem_builders["securenotes.SecureNote"] = build_secret_menuitem_securenote
icculus@12
   367
icculus@49
   368
icculus@49
   369
local function build_secret_menuitem_ssnus(menu, info, secure)
icculus@49
   370
    build_secret_menuitem(menu, "Name", secure.name, false)
icculus@49
   371
    build_secret_menuitem(menu, "SSN", secure.number, true)
icculus@49
   372
end
icculus@49
   373
secret_menuitem_builders["wallet.government.SsnUS"] = build_secret_menuitem_ssnus
icculus@49
   374
icculus@49
   375
icculus@49
   376
local function build_secret_menuitem_router(menu, info, secure)
icculus@49
   377
    build_secret_menuitem(menu, "Name", secure.name, false)
icculus@49
   378
    build_secret_menuitem(menu, "Password", secure.password, true)
icculus@49
   379
end
icculus@49
   380
secret_menuitem_builders["wallet.computer.Router"] = build_secret_menuitem_router
icculus@49
   381
icculus@49
   382
icculus@17
   383
local function build_secret_menuitems(info, menu)
icculus@12
   384
    local metadata = load_json(basedir .. "/" .. info.uuid .. ".1password")
icculus@40
   385
    if (metadata == nil) or (next(metadata) == nil) then  -- the "next" trick tests if table is empty.
icculus@12
   386
        return
icculus@12
   387
    end
icculus@12
   388
robbie@21
   389
    local securityLevel = metadata.securityLevel
robbie@21
   390
    if securityLevel == nil then
icculus@36
   391
        securityLevel = metadata.openContents.securityLevel
robbie@21
   392
    end
icculus@30
   393
    --print("title: " .. metadata.title)
robbie@21
   394
    if securityLevel == nil then
robbie@21
   395
        --print("can't find security level, assuming SL5" .. metadata.title)
robbie@21
   396
        securityLevel = "SL5"
robbie@21
   397
    end
robbie@21
   398
robbie@21
   399
    local plaintext = decryptBase64UsingKey(metadata.encrypted, loadKey(securityLevel, password))
icculus@12
   400
    if plaintext == nil then
icculus@12
   401
        return
icculus@12
   402
    end
icculus@12
   403
icculus@12
   404
    local secure = load_json_str(plaintext, info.uuid)
icculus@12
   405
    if secure == nil then
icculus@12
   406
        return
icculus@12
   407
    end
icculus@12
   408
    --dumptable("secure " .. info.name, secure)
icculus@12
   409
icculus@45
   410
    local menuitem = appendMenuItem(menu, info.name)
icculus@12
   411
icculus@12
   412
    if secret_menuitem_builders[info.type] == nil then
icculus@12
   413
        print("WARNING: don't know how to handle items of type " .. info.type)
icculus@12
   414
        dumptable("secure " .. info.type .. " (" .. info.name .. ")", secure)
icculus@12
   415
        return
icculus@12
   416
    end
icculus@12
   417
icculus@35
   418
    if metadata.faveIndex ~= nil then
icculus@35
   419
        --dumptable("fave metadata " .. info.name, metadata)
icculus@35
   420
        faveitems[metadata.faveIndex] = { info=info, secure=secure }
icculus@35
   421
    end
icculus@35
   422
icculus@45
   423
    local submenu = makeMenu()
icculus@12
   424
    secret_menuitem_builders[info.type](submenu, info, secure)
icculus@45
   425
    setMenuItemSubmenu(menuitem, submenu)
icculus@8
   426
end
icculus@8
   427
icculus@17
   428
local function prepItems()
icculus@17
   429
    items = {}
icculus@17
   430
    local contents = loadContents()
icculus@44
   431
    if contents == nil then
icculus@44
   432
        return false
icculus@44
   433
    end
icculus@17
   434
    for i,v in ipairs(contents) do
icculus@17
   435
        local t = v[2]
icculus@17
   436
        if items[t] == nil then
icculus@17
   437
            items[t] = {}
icculus@17
   438
        end
icculus@17
   439
        local bucket = items[t]
icculus@17
   440
        bucket[#bucket+1] = { uuid=v[1], type=t, name=v[3], url=v[4] }  -- !!! FIXME: there are more fields, don't know what they mean yet.
icculus@17
   441
    end
icculus@44
   442
    return true
icculus@17
   443
end
icculus@17
   444
icculus@18
   445
local passwordUnlockTime = nil
icculus@18
   446
icculus@29
   447
local function lockKeychain()
icculus@29
   448
    -- lose the existing password and key, prompt user again.
icculus@29
   449
    password = argv[2]  -- might be nil, don't reset if on command line.
icculus@29
   450
    keys["SL5"] = nil
icculus@29
   451
    passwordUnlockTime = nil
icculus@29
   452
    setPowermateLED(false)
icculus@45
   453
icculus@45
   454
    -- kill the popup if it exists.
icculus@45
   455
    if (keyhookGuiMenus ~= nil) and (keyhookGuiMenus[1] ~= nil) then
icculus@45
   456
        guiDestroyMenu(keyhookGuiMenus[1])
icculus@45
   457
    end
icculus@29
   458
end
icculus@29
   459
icculus@29
   460
function pumpLua()  -- not local! Called from C!
icculus@29
   461
    -- !!! FIXME: this should lose the key in RAM and turn off the Powermate
icculus@29
   462
    -- !!! FIXME:  LED when the time expires instead of if the time has
icculus@29
   463
    -- !!! FIXME:  expired when the user is trying to get at the keychain.
icculus@29
   464
    if passwordUnlockTime ~= nil then
icculus@29
   465
        local now = os.time()
icculus@29
   466
        local maxTime = (15 * 60)  -- !!! FIXME: don't hardcode.
icculus@29
   467
        if os.difftime(now, passwordUnlockTime) > maxTime then
icculus@29
   468
            lockKeychain()
icculus@29
   469
        end
icculus@29
   470
    end
icculus@29
   471
end
icculus@29
   472
icculus@45
   473
function escapePressed()  -- not local! Called from C!
icculus@45
   474
    if keyhookGuiMenus[1] then
icculus@45
   475
        guiDestroyMenu(keyhookGuiMenus[1])
icculus@45
   476
    end
icculus@45
   477
end
icculus@45
   478
icculus@45
   479
icculus@45
   480
local buildGuiMenuList
icculus@45
   481
icculus@45
   482
local function spawnSubMenu(button, submenu, depth)
icculus@45
   483
    local guimenu = guiCreateSubMenu(button)
icculus@45
   484
icculus@45
   485
    for i = #keyhookGuiMenus, depth, -1 do
icculus@45
   486
        if keyhookGuiMenus[i] then
icculus@45
   487
            --print("Destroying conflicting submenu at depth " .. i)
icculus@45
   488
            guiDestroyMenu(keyhookGuiMenus[i])
icculus@45
   489
            keyhookGuiMenus[i] = nil
icculus@45
   490
        end
icculus@45
   491
    end
icculus@45
   492
icculus@45
   493
    --print("New submenu at depth " .. depth)
icculus@45
   494
    keyhookGuiMenus[depth] = guimenu
icculus@45
   495
icculus@45
   496
    buildGuiMenuList(guimenu, submenu)
icculus@45
   497
    guiShowWindow(guimenu)
icculus@45
   498
end
icculus@45
   499
icculus@45
   500
local function buildGuiMenuItem(guimenu, item)
icculus@45
   501
    local cb = item["callback"]
icculus@45
   502
    if cb == nil then
icculus@45
   503
        local submenu = item["submenu"]
icculus@45
   504
        local depth = #keyhookGuiMenus+1
icculus@45
   505
        cb = function (button)
icculus@45
   506
            return spawnSubMenu(button, submenu, depth)
icculus@45
   507
        end
icculus@45
   508
    end
icculus@46
   509
    guiAddMenuItem(guimenu, item["text"], item["checked"], cb)
icculus@45
   510
end
icculus@45
   511
icculus@45
   512
buildGuiMenuList = function(guimenu, list)
icculus@45
   513
    for i,v in ipairs(list) do
icculus@45
   514
        buildGuiMenuItem(guimenu, v)
icculus@45
   515
    end
icculus@45
   516
end
icculus@45
   517
icculus@45
   518
local function buildSearchResultsMenuCategory(guimenu, menu, str)
icculus@45
   519
    local submenu = menu["submenu"]
icculus@45
   520
    if not submenu then return end
icculus@45
   521
icculus@45
   522
    local name = menu["text"]
icculus@45
   523
    -- !!! FIXME: hacky. We should really list favorites first anyhow.
icculus@45
   524
    if name == "Favorites" then return end
icculus@45
   525
icculus@45
   526
    for i,v in ipairs(submenu) do
icculus@45
   527
        if string.find(string.lower(v["text"]), str, 1, true) ~= nil then
icculus@45
   528
            buildGuiMenuItem(guimenu, v)
icculus@45
   529
        end
icculus@45
   530
    end
icculus@45
   531
end
icculus@45
   532
icculus@45
   533
local function buildSearchResultsMenuList(guimenu, topmenu, str)
icculus@45
   534
    for i,v in ipairs(topmenu) do
icculus@45
   535
        buildSearchResultsMenuCategory(guimenu, v, str)
icculus@45
   536
    end
icculus@45
   537
end
icculus@45
   538
icculus@45
   539
local function searchEntryChanged(guimenu, str, topmenu)
icculus@45
   540
    --print("search changed to '" .. str .. "' ...")
icculus@45
   541
    guiRemoveAllMenuItems(guimenu)
icculus@45
   542
    if str == "" then
icculus@45
   543
        buildGuiMenuList(guimenu, topmenu)
icculus@45
   544
    else
icculus@45
   545
        buildSearchResultsMenuList(guimenu, topmenu, string.lower(str))
icculus@45
   546
    end
icculus@45
   547
    guiShowWindow(guimenu)
icculus@45
   548
end
icculus@45
   549
icculus@45
   550
local function handleMenuDestroyed()
icculus@45
   551
    --print("Destroying main menu...")
icculus@45
   552
    for i,v in ipairs(keyhookGuiMenus) do
icculus@45
   553
        if i > 1 then
icculus@45
   554
            guiDestroyMenu(v)
icculus@45
   555
        end
icculus@45
   556
    end
icculus@45
   557
    keyhookGuiMenus = nil
icculus@45
   558
    keyhookRunning = false
icculus@45
   559
icculus@45
   560
    runGarbageCollector()
icculus@45
   561
end
icculus@45
   562
icculus@45
   563
local function launchGuiMenu(topmenu)
icculus@45
   564
    local guimenu = guiCreateTopLevelMenu("1pass",
icculus@45
   565
icculus@45
   566
        function(guimenu, str) -- search text changed callback
icculus@45
   567
            return searchEntryChanged(guimenu, str, topmenu)
icculus@45
   568
        end,
icculus@45
   569
icculus@45
   570
        function()  -- window destroyed callback
icculus@45
   571
            handleMenuDestroyed()
icculus@45
   572
        end
icculus@45
   573
    )
icculus@45
   574
    keyhookGuiMenus = {}
icculus@45
   575
    keyhookGuiMenus[#keyhookGuiMenus+1] = guimenu
icculus@45
   576
    buildGuiMenuList(guimenu, topmenu)
icculus@45
   577
    guiShowWindow(guimenu)
icculus@45
   578
end
icculus@29
   579
icculus@46
   580
local trustedDisks = {}
icculus@46
   581
icculus@46
   582
local function getTrustedDiskChecksumPath(mntpoint)
icculus@46
   583
    return mntpoint .. "/1pass.dat"
icculus@46
   584
end
icculus@46
   585
icculus@46
   586
local function getTrustedDiskChecksum(mntpoint)
icculus@46
   587
    local f = io.open(getTrustedDiskChecksumPath(mntpoint), "rb")
icculus@46
   588
    if f == nil then
icculus@46
   589
        return nil
icculus@46
   590
    end
icculus@46
   591
icculus@46
   592
    local str = f:read("*a")
icculus@46
   593
    f:close()
icculus@46
   594
    return calcSha256(str)
icculus@46
   595
end
icculus@46
   596
icculus@46
   597
local function choseTrustedDisk(mntpoint)
icculus@46
   598
    if trustedDisks[mntpoint] ~= nil then
icculus@46
   599
        trustedDisks[mntpoint] = nil  -- no longer check existing trusted disk.
icculus@46
   600
    else
icculus@46
   601
        -- !!! FIXME: probably needs a message box if this fails.
icculus@46
   602
        local checksum = getTrustedDiskChecksum(mntpoint)
icculus@46
   603
        -- No checksum file yet? Generate and write out a random string.
icculus@46
   604
        if checksum == nil then
icculus@46
   605
            local f = io.open("/dev/urandom", "rb")
icculus@46
   606
            if f ~= nil then
icculus@46
   607
                local str = f:read(4096)
icculus@46
   608
                f:close()
icculus@46
   609
                if (str ~= nil) and (#str == 4096) then
icculus@46
   610
                    f = io.open(getTrustedDiskChecksumPath(mntpoint), "wb")
icculus@46
   611
                    if f ~= nil then
icculus@46
   612
                        if f:write(str) and f:flush() then
icculus@46
   613
                            checksum = calcSha256(str)
icculus@46
   614
                        end
icculus@46
   615
                        f:close()
icculus@46
   616
                    end
icculus@46
   617
                end
icculus@46
   618
            end
icculus@46
   619
        end
icculus@46
   620
        trustedDisks[mntpoint] = checksum
icculus@46
   621
    end
icculus@46
   622
icculus@46
   623
    -- kill the popup if it exists.
icculus@46
   624
    -- !!! FIXME: put this in its own function, this is a copy/paste from elsewhere.
icculus@46
   625
    if (keyhookGuiMenus ~= nil) and (keyhookGuiMenus[1] ~= nil) then
icculus@46
   626
        guiDestroyMenu(keyhookGuiMenus[1])
icculus@46
   627
    end
icculus@46
   628
end
icculus@46
   629
icculus@46
   630
local function buildTrustedDeviceMenu()
icculus@46
   631
    local menu = makeMenu()
icculus@46
   632
    local disks = getMountedDisks()  -- this is a C function.
icculus@46
   633
icculus@46
   634
    table.sort(disks, function(a, b) return a < b end)
icculus@46
   635
    for i,v in ipairs(disks) do
icculus@46
   636
        local item = appendMenuItem(menu, v, function() choseTrustedDisk(v) end)
icculus@46
   637
        if trustedDisks[v] ~= nil then
icculus@46
   638
            setMenuItemChecked(item, true)
icculus@46
   639
        end
icculus@46
   640
    end
icculus@46
   641
icculus@46
   642
    return menu
icculus@46
   643
end
icculus@46
   644
icculus@17
   645
function keyhookPressed()  -- not local! Called from C!
icculus@45
   646
    --print("keyhookPressed: running==" .. tostring(keyhookRunning))
icculus@45
   647
    if keyhookRunning then
icculus@45
   648
        return
icculus@45
   649
    end
icculus@17
   650
icculus@17
   651
    keyhookRunning = true
icculus@17
   652
icculus@46
   653
    local allowaccess = true;
icculus@46
   654
    for mntpoint,checksum in pairs(trustedDisks) do
icculus@46
   655
        if getTrustedDiskChecksum(mntpoint) ~= checksum then
icculus@46
   656
            allowaccess = false
icculus@46
   657
            break
icculus@46
   658
        end
icculus@46
   659
    end
icculus@46
   660
icculus@46
   661
    if not allowaccess then
icculus@46
   662
        -- !!! FIXME: probably needs a message box if this happens.
icculus@46
   663
        keyhookRunning = false
icculus@46
   664
        return
icculus@46
   665
    end
icculus@46
   666
icculus@17
   667
    while password == nil do
icculus@17
   668
        password = runGuiPasswordPrompt(getHint())
icculus@17
   669
        if password == nil then
icculus@17
   670
            keyhookRunning = false
icculus@17
   671
            return
icculus@17
   672
        end
icculus@17
   673
        if loadKey("SL5", password) == nil then
icculus@17
   674
            password = nil  -- wrong password
icculus@17
   675
            local start = os.time()  -- cook the CPU for three seconds.
icculus@17
   676
            local now = start
icculus@17
   677
            while os.difftime(now, start) < 3 do
icculus@17
   678
                now = os.time()
icculus@17
   679
            end
icculus@18
   680
        else
icculus@18
   681
            passwordUnlockTime = os.time()
icculus@28
   682
            setPowermateLED(true)
icculus@17
   683
        end
icculus@17
   684
    end
icculus@17
   685
icculus@44
   686
    if not prepItems() then
icculus@44
   687
        keyhookRunning = false
icculus@44
   688
        return
icculus@44
   689
    end
icculus@17
   690
icculus@45
   691
    local topmenu = makeMenu()
icculus@45
   692
    local favesmenu = makeMenu()
icculus@46
   693
    local securitymenu = makeMenu()
icculus@35
   694
    faveitems = {}
icculus@35
   695
icculus@45
   696
    setMenuItemSubmenu(appendMenuItem(topmenu, "Favorites"), favesmenu)
icculus@46
   697
    setMenuItemSubmenu(appendMenuItem(topmenu, "Security"), securitymenu)
icculus@18
   698
icculus@46
   699
    appendMenuItem(securitymenu, "Lock keychain now", function() lockKeychain() end)
icculus@46
   700
    setMenuItemSubmenu(appendMenuItem(securitymenu, "Require trusted device"), buildTrustedDeviceMenu())
icculus@18
   701
icculus@17
   702
    for orderi,type in ipairs(passwordTypeOrdering) do
icculus@17
   703
        local bucket = items[type]
robbie@21
   704
        if bucket ~= nil then
robbie@21
   705
            local realname = passwordTypeNameMap[type]
robbie@21
   706
            if realname == nil then
robbie@21
   707
                realname = type
robbie@21
   708
            end
icculus@45
   709
            local menuitem = appendMenuItem(topmenu, realname)
icculus@45
   710
            local submenu = makeMenu()
robbie@21
   711
            table.sort(bucket, function(a, b) return a.name < b.name end)
robbie@21
   712
            for i,v in pairs(bucket) do
robbie@21
   713
                build_secret_menuitems(v, submenu)
robbie@21
   714
            end
icculus@45
   715
            setMenuItemSubmenu(menuitem, submenu)
robbie@21
   716
        else
icculus@37
   717
            --print("no bucket found for item type '" .. type .. "'")
icculus@17
   718
        end
icculus@17
   719
    end
icculus@35
   720
    
icculus@35
   721
    -- This favepairs stuff is obnoxious.
icculus@35
   722
    local function favepairs(t)
icculus@35
   723
        local a = {}
icculus@35
   724
        for n in pairs(t) do table.insert(a, n) end
icculus@35
   725
        table.sort(a)
icculus@35
   726
        local i = 0
icculus@35
   727
        local iter = function()
icculus@35
   728
            i = i + 1
icculus@35
   729
            if a[i] == nil then
icculus@35
   730
                return nil
icculus@35
   731
            else
icculus@35
   732
                return a[i], t[a[i]]
icculus@35
   733
            end
icculus@35
   734
        end
icculus@35
   735
        return iter
icculus@35
   736
    end
icculus@35
   737
icculus@35
   738
    for i,v in favepairs(faveitems) do
icculus@35
   739
        --dumptable("fave " .. i, v)
icculus@45
   740
        local menuitem = appendMenuItem(favesmenu, v.info.name)
icculus@45
   741
        local submenu = makeMenu()
icculus@35
   742
        secret_menuitem_builders[v.info.type](submenu, v.info, v.secure)
icculus@45
   743
        setMenuItemSubmenu(menuitem, submenu)
icculus@35
   744
    end
icculus@35
   745
icculus@35
   746
    favepairs = nil
icculus@35
   747
    faveitems = nil
icculus@17
   748
icculus@45
   749
    launchGuiMenu(topmenu)
icculus@17
   750
end
icculus@17
   751
icculus@6
   752
icculus@1
   753
-- Mainline!
icculus@1
   754
icculus@7
   755
--for i,v in ipairs(argv) do
icculus@7
   756
--    print("argv[" .. i .. "] = " .. v)
icculus@7
   757
--end
icculus@7
   758
icculus@17
   759
-- !!! FIXME: message box, exit if basedir is wack.
icculus@44
   760
local f = io.open(basedir .. "/contents.js", "rb")
icculus@44
   761
if f == nil then
icculus@44
   762
    print("ERROR: Couldn't read your 1Password keychain in '" .. basedir .. "'.")
icculus@44
   763
    print("ERROR: Please make sure it exists and you have permission to access it.")
icculus@44
   764
    print("ERROR: (maybe you need to run 'ln -s ~/Dropbox/1Password' here?")
icculus@44
   765
    print("ERROR: Giving up for now.")
icculus@44
   766
    os.exit(1)
icculus@44
   767
end
icculus@44
   768
f:close()
icculus@44
   769
icculus@17
   770
-- !!! FIXME: this can probably happen in C now (the Lua mainline is basically gone now).
icculus@28
   771
setPowermateLED(false)  -- off by default
icculus@27
   772
print("Now waiting for the magic key combo (probably Alt-Meta-\\) ...")
icculus@11
   773
giveControlToGui()
icculus@11
   774
icculus@0
   775
-- end of 1pass.lua ...
icculus@0
   776