1pass.lua
author Ryan C. Gordon <icculus@icculus.org>
Fri, 23 Jun 2017 17:28:03 -0400
changeset 58 1390348facc7
parent 56 a573346e6f7b
permissions -rw-r--r--
Command line tool that decrypts an OPVault keychain and dumps it to stdout.

To compile: gcc -o opvault opvault.c cJSON.c -lcrypto

Usage: ./opvault </path/to/mykeychain.opvault> <password>

This is just a proof of concept; I'll be recycling this into proper OPVault
support in 1pass later and deleting this tool.

This uses OpenSSL's libcrypto for the math instead of all the homegrown
crypto this project is otherwise using. I'll probably migrate the rest in
this direction, too, since this wasn't as bad as I expected to use and
gets you all the package-manager mojo of automatic bug fixes and security
patches and shared code, etc.

cJSON parses JSON in C. That is from https://github.com/DaveGamble/cJSON

An example OPVault keychain from AgileBits is available here:

https://cache.agilebits.com/security-kb/
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