1pass.lua
author Ryan C. Gordon <icculus@icculus.org>
Sat, 28 Mar 2015 22:33:10 -0400
changeset 43 85b1cb11d948
parent 42 ca071626bfa0
child 44 2150bce729df
permissions -rw-r--r--
Whoops, forgot to remove original code that I was fixing. :)
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@17
     9
icculus@12
    10
local passwordTypeNameMap = {
icculus@12
    11
    ["webforms.WebForm"] = "Logins",
icculus@12
    12
    ["wallet.financial.CreditCard"] = "Credit cards",
icculus@12
    13
    ["passwords.Password"] = "Passwords",
icculus@12
    14
    ["wallet.financial.BankAccountUS"] = "Bank accounts",
icculus@12
    15
    ["wallet.membership.Membership"] = "Memberships",
icculus@12
    16
    ["wallet.government.DriversLicense"] = "Drivers licenses",
icculus@12
    17
    ["system.Tombstone"] = "Dead items",
icculus@25
    18
    ["securenotes.SecureNote"] = "Secure notes",
icculus@12
    19
    -- !!! FIXME: more!
icculus@12
    20
}
icculus@12
    21
icculus@12
    22
local passwordTypeOrdering = {
icculus@12
    23
    "webforms.WebForm",
icculus@12
    24
    "wallet.financial.CreditCard",
icculus@12
    25
    "passwords.Password",
icculus@12
    26
    "wallet.financial.BankAccountUS",
icculus@12
    27
    "wallet.membership.Membership",
icculus@12
    28
    "wallet.government.DriversLicense",
icculus@25
    29
    "securenotes.SecureNote",
icculus@12
    30
    -- never show "system.Tombstone",
icculus@12
    31
    -- !!! FIXME: more!
icculus@12
    32
}
icculus@0
    33
icculus@6
    34
local function load_json_str(str, desc)
icculus@6
    35
    local retval = JSON:decode(str)
icculus@6
    36
    return retval
icculus@6
    37
end
icculus@6
    38
icculus@0
    39
local function load_json(fname)
icculus@0
    40
    local f = io.open(fname, "rb")
icculus@0
    41
    if (f == nil) then
icculus@0
    42
        return nil
icculus@0
    43
    end
icculus@0
    44
icculus@0
    45
    local str = f:read("*all")
icculus@0
    46
    f:close()
icculus@0
    47
icculus@6
    48
    return load_json_str(str, fname)
icculus@0
    49
end
icculus@0
    50
icculus@0
    51
icculus@5
    52
local keys = {}
icculus@17
    53
local function loadKey(level, password)
icculus@5
    54
    if keys[level] ~= nil then
icculus@5
    55
        return keys[level]
icculus@5
    56
    end
icculus@5
    57
icculus@12
    58
    local keysjson = load_json(basedir .. "/encryptionKeys.js")
icculus@0
    59
    if (keysjson == nil) or (keysjson[level] == nil) then
icculus@0
    60
        return nil
icculus@0
    61
    end
icculus@0
    62
icculus@0
    63
    local identifier = keysjson[level]
icculus@0
    64
    for i,v in ipairs(keysjson.list) do
icculus@0
    65
        if v.identifier == identifier then
icculus@0
    66
			local iterations = v.iterations
icculus@0
    67
            if (iterations == nil) or (iterations < 1000) then
icculus@0
    68
			    iterations = 1000
icculus@0
    69
            end
icculus@0
    70
icculus@0
    71
			local decrypted = decryptUsingPBKDF2(v.data, password, iterations)
icculus@0
    72
			if decrypted == nil then
icculus@0
    73
                return nil
icculus@0
    74
            end
icculus@0
    75
icculus@0
    76
			local validate = decryptBase64UsingKey(v.validation, decrypted)
icculus@0
    77
			if validate ~= decrypted then
icculus@0
    78
                return nil
icculus@0
    79
            end
icculus@0
    80
icculus@5
    81
            keys[level] = decrypted
icculus@0
    82
            return decrypted
icculus@0
    83
        end
icculus@0
    84
    end
icculus@0
    85
icculus@0
    86
    return nil
icculus@0
    87
end
icculus@0
    88
icculus@17
    89
local function getHint()
icculus@0
    90
    local f = io.open(basedir .. "/.password.hint", "r")
icculus@0
    91
    if (f == nil) then
icculus@0
    92
        return
icculus@0
    93
    end
icculus@0
    94
icculus@12
    95
    local str = "(hint is '" .. f:read("*all") .. "')."
icculus@0
    96
    f:close()
icculus@12
    97
    --print(str)
icculus@12
    98
    return str
icculus@0
    99
end
icculus@0
   100
icculus@1
   101
icculus@17
   102
local function loadContents()
icculus@12
   103
    return load_json(basedir .. "/contents.js")
icculus@6
   104
end
icculus@6
   105
icculus@12
   106
local function build_secret_menuitem(menu, type, str, hidden)
icculus@12
   107
    if str == nil then
icculus@12
   108
        return nil
icculus@8
   109
    end
icculus@12
   110
icculus@12
   111
    local valuestr = str
icculus@12
   112
    if hidden == true then
icculus@12
   113
        valuestr = "*****"
icculus@12
   114
    end
icculus@12
   115
    local text = type .. " " .. valuestr
icculus@12
   116
icculus@12
   117
    local callback = function()
icculus@12
   118
        copyToClipboard(str)
icculus@12
   119
        --print("Copied data [" .. str .. "] to clipboard.")
icculus@17
   120
        keyhookRunning = false
icculus@12
   121
    end
icculus@12
   122
    return appendGuiMenuItem(menu, text, callback)
icculus@12
   123
end
icculus@12
   124
icculus@12
   125
icculus@12
   126
local secret_menuitem_builders = {}
icculus@12
   127
icculus@12
   128
local function build_secret_menuitem_webform(menu, info, secure)
icculus@12
   129
    local addthis = false
icculus@12
   130
    local username = nil
icculus@12
   131
    local password = nil
icculus@32
   132
    local designated_password = nil
icculus@32
   133
    local designated_username = nil
icculus@12
   134
    local email = nil
robbie@21
   135
icculus@24
   136
    if secure.fields == nil then
icculus@24
   137
      print("no secure fields, don't know how to handle this item") 
icculus@24
   138
      return
icculus@24
   139
    end
icculus@24
   140
icculus@12
   141
    for i,v in ipairs(secure.fields) do
icculus@12
   142
        --print(info.name .. ": " .. v.type .. ", " .. v.value)
icculus@12
   143
        local ignored = false
icculus@32
   144
        if (v.value == nil) or (v.value == "") then
icculus@32
   145
            ignored = true
icculus@32
   146
        elseif (v.designation ~= nil) and (v.designation == "password") then
icculus@32
   147
            designated_password = v.value
icculus@32
   148
        elseif (v.designation ~= nil) and (v.designation == "username") then
icculus@32
   149
            designated_username = v.value
icculus@32
   150
        elseif (v.type == "P") then
icculus@12
   151
            password = v.value
icculus@32
   152
        elseif (v.type == "T") then
icculus@12
   153
            username = v.value
icculus@32
   154
        elseif (v.type == "E") then
icculus@12
   155
            email = v.value
icculus@12
   156
        else
icculus@12
   157
            ignored = true
icculus@12
   158
        end
icculus@12
   159
icculus@12
   160
        if not ignored then
icculus@12
   161
            addthis = true
icculus@12
   162
        end
icculus@12
   163
    end
icculus@12
   164
icculus@12
   165
    if addthis then
icculus@32
   166
        -- designated fields always win out.
icculus@32
   167
        if (designated_username ~= nil) then
icculus@32
   168
            username = designated_username
icculus@32
   169
        end
icculus@32
   170
icculus@32
   171
        if (designated_password ~= nil) then
icculus@32
   172
            password = designated_password
icculus@32
   173
        end
icculus@32
   174
icculus@12
   175
        if (username ~= nil) and (email ~= nil) and (email == username) then
icculus@12
   176
            email = nil
icculus@12
   177
        end
icculus@12
   178
icculus@12
   179
        build_secret_menuitem(menu, "username", username)
icculus@12
   180
        build_secret_menuitem(menu, "email", email)
icculus@12
   181
        build_secret_menuitem(menu, "password", password, true)
icculus@12
   182
    end
icculus@12
   183
end
icculus@12
   184
secret_menuitem_builders["webforms.WebForm"] = build_secret_menuitem_webform
icculus@12
   185
icculus@12
   186
icculus@12
   187
local function build_secret_menuitem_password(menu, info, secure)
icculus@12
   188
    build_secret_menuitem(menu, "password", secure.password, true)
icculus@12
   189
end
icculus@12
   190
secret_menuitem_builders["passwords.Password"] = build_secret_menuitem_password
icculus@12
   191
icculus@12
   192
icculus@12
   193
local function build_secret_menuitem_bankacctus(menu, info, secure)
icculus@12
   194
    -- !!! FIXME: there's more data than this in a generic dictionary.
icculus@12
   195
    build_secret_menuitem(menu, "Account type", secure.accountType)
icculus@12
   196
    build_secret_menuitem(menu, "Routing number", secure.routingNo)
icculus@12
   197
    build_secret_menuitem(menu, "Account number", secure.accountNo)
icculus@12
   198
    build_secret_menuitem(menu, "Bank name", secure.bankName)
icculus@12
   199
    build_secret_menuitem(menu, "Owner", secure.owner)
icculus@41
   200
    build_secret_menuitem(menu, "SWIFT code", secure.swift)
icculus@41
   201
    build_secret_menuitem(menu, "PIN", secure.telephonePin)
icculus@12
   202
end
icculus@12
   203
secret_menuitem_builders["wallet.financial.BankAccountUS"] = build_secret_menuitem_bankacctus
icculus@12
   204
icculus@12
   205
icculus@12
   206
local function build_secret_menuitem_driverslic(menu, info, secure)
icculus@42
   207
    -- !!! FIXME: there's more data for this menuitem than this, in a generic dictionary.
icculus@42
   208
icculus@42
   209
    local birthdate = nil
icculus@42
   210
    if secure.birthdate_yy ~= nil then
icculus@42
   211
        birthdate = secure.birthdate_yy
icculus@42
   212
        if secure.birthdate_mm ~= nil then
icculus@42
   213
            birthdate = birthdate .. "/" .. string.sub("00" .. secure.birthdate_mm, -2)
icculus@42
   214
            if secure.birthdate_dd ~= nil then
icculus@42
   215
                birthdate = birthdate .. "/" .. string.sub("00" .. secure.birthdate_dd, -2)
icculus@42
   216
            end
icculus@42
   217
        end
icculus@42
   218
    end
icculus@42
   219
icculus@43
   220
    local expiredate = nil
icculus@42
   221
    if secure.expiry_date_yy ~= nil then
icculus@42
   222
        expiredate = secure.expiry_date_yy
icculus@42
   223
        if secure.expiry_date_mm ~= nil then
icculus@42
   224
            expiredate = expiredate .. "/" .. string.sub("00" .. secure.expiry_date_mm, -2)
icculus@42
   225
            if secure.expiry_date_dd ~= nil then
icculus@42
   226
                expiredate = expiredate .. "/" .. string.sub("00" .. secure.expiry_date_dd, -2)
icculus@42
   227
            end
icculus@42
   228
        end
icculus@42
   229
    end
icculus@42
   230
icculus@12
   231
    build_secret_menuitem(menu, "License number", secure.number)
icculus@12
   232
    build_secret_menuitem(menu, "Class", secure.class)
icculus@12
   233
    build_secret_menuitem(menu, "Expires", expiredate)
icculus@12
   234
    build_secret_menuitem(menu, "State", secure.state)
icculus@12
   235
    build_secret_menuitem(menu, "Country", secure.country)
icculus@12
   236
    build_secret_menuitem(menu, "Conditions", secure.conditions)
icculus@12
   237
    build_secret_menuitem(menu, "Full name", secure.fullname)
icculus@12
   238
    build_secret_menuitem(menu, "Address", secure.address)
icculus@12
   239
    build_secret_menuitem(menu, "Gender", secure.sex)
icculus@12
   240
    build_secret_menuitem(menu, "Birthdate", birthdate)
icculus@12
   241
    build_secret_menuitem(menu, "Height", secure.height)
icculus@12
   242
end
icculus@12
   243
secret_menuitem_builders["wallet.government.DriversLicense"] = build_secret_menuitem_driverslic
icculus@12
   244
icculus@12
   245
icculus@12
   246
local function build_secret_menuitem_membership(menu, info, secure)
icculus@12
   247
    -- !!! FIXME: there's more data than this in a generic dictionary.
icculus@12
   248
    build_secret_menuitem(menu, "Membership number", secure.membership_no)
icculus@12
   249
end
icculus@12
   250
secret_menuitem_builders["wallet.membership.Membership"] = build_secret_menuitem_membership
icculus@12
   251
icculus@12
   252
icculus@12
   253
local function build_secret_menuitem_creditcard(menu, info, secure)
icculus@12
   254
    -- !!! FIXME: there's more data than this in a generic dictionary.
icculus@12
   255
    local expiredate = secure.expiry_yy .. "/" .. string.sub("00" .. secure.expiry_mm, -2)
icculus@12
   256
    build_secret_menuitem(menu, "Type", secure.type)
icculus@12
   257
    build_secret_menuitem(menu, "CC number", secure.ccnum, true)
icculus@12
   258
    build_secret_menuitem(menu, "CVV", secure.cvv, true)
icculus@38
   259
    build_secret_menuitem(menu, "Expires", expiredate)
icculus@12
   260
    build_secret_menuitem(menu, "Card holder", secure.cardholder)
icculus@12
   261
    build_secret_menuitem(menu, "Bank", secure.bank)
icculus@12
   262
end
icculus@12
   263
secret_menuitem_builders["wallet.financial.CreditCard"] = build_secret_menuitem_creditcard
icculus@12
   264
icculus@25
   265
local function build_secret_menuitem_securenote(menu, info, secure)
icculus@25
   266
    build_secret_menuitem(menu, "Notes", secure.notesPlain, true)
icculus@25
   267
end
icculus@25
   268
secret_menuitem_builders["securenotes.SecureNote"] = build_secret_menuitem_securenote
icculus@12
   269
icculus@17
   270
local function build_secret_menuitems(info, menu)
icculus@12
   271
    local metadata = load_json(basedir .. "/" .. info.uuid .. ".1password")
icculus@40
   272
    if (metadata == nil) or (next(metadata) == nil) then  -- the "next" trick tests if table is empty.
icculus@12
   273
        return
icculus@12
   274
    end
icculus@12
   275
robbie@21
   276
    local securityLevel = metadata.securityLevel
robbie@21
   277
    if securityLevel == nil then
icculus@36
   278
        securityLevel = metadata.openContents.securityLevel
robbie@21
   279
    end
icculus@30
   280
    --print("title: " .. metadata.title)
robbie@21
   281
    if securityLevel == nil then
robbie@21
   282
        --print("can't find security level, assuming SL5" .. metadata.title)
robbie@21
   283
        securityLevel = "SL5"
robbie@21
   284
    end
robbie@21
   285
robbie@21
   286
    local plaintext = decryptBase64UsingKey(metadata.encrypted, loadKey(securityLevel, password))
icculus@12
   287
    if plaintext == nil then
icculus@12
   288
        return
icculus@12
   289
    end
icculus@12
   290
icculus@12
   291
    local secure = load_json_str(plaintext, info.uuid)
icculus@12
   292
    if secure == nil then
icculus@12
   293
        return
icculus@12
   294
    end
icculus@12
   295
    --dumptable("secure " .. info.name, secure)
icculus@12
   296
icculus@12
   297
    local menuitem = appendGuiMenuItem(menu, info.name)
icculus@12
   298
icculus@12
   299
    if secret_menuitem_builders[info.type] == nil then
icculus@12
   300
        print("WARNING: don't know how to handle items of type " .. info.type)
icculus@12
   301
        dumptable("secure " .. info.type .. " (" .. info.name .. ")", secure)
icculus@12
   302
        return
icculus@12
   303
    end
icculus@12
   304
icculus@35
   305
    if metadata.faveIndex ~= nil then
icculus@35
   306
        --dumptable("fave metadata " .. info.name, metadata)
icculus@35
   307
        faveitems[metadata.faveIndex] = { info=info, secure=secure }
icculus@35
   308
    end
icculus@35
   309
icculus@12
   310
    local submenu = makeGuiMenu()
icculus@12
   311
    secret_menuitem_builders[info.type](submenu, info, secure)
icculus@12
   312
    setGuiMenuItemSubmenu(menuitem, submenu)
icculus@8
   313
end
icculus@8
   314
icculus@17
   315
local function prepItems()
icculus@17
   316
    items = {}
icculus@17
   317
    local contents = loadContents()
icculus@17
   318
    for i,v in ipairs(contents) do
icculus@17
   319
        local t = v[2]
icculus@17
   320
        if items[t] == nil then
icculus@17
   321
            items[t] = {}
icculus@17
   322
        end
icculus@17
   323
        local bucket = items[t]
icculus@17
   324
        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
   325
    end
icculus@17
   326
end
icculus@17
   327
icculus@18
   328
local passwordUnlockTime = nil
icculus@18
   329
icculus@29
   330
local function lockKeychain()
icculus@29
   331
    -- lose the existing password and key, prompt user again.
icculus@29
   332
    password = argv[2]  -- might be nil, don't reset if on command line.
icculus@29
   333
    keys["SL5"] = nil
icculus@29
   334
    passwordUnlockTime = nil
icculus@29
   335
    keyhookRunning = false
icculus@29
   336
    setPowermateLED(false)
icculus@29
   337
    collectgarbage()
icculus@29
   338
end
icculus@29
   339
icculus@29
   340
function pumpLua()  -- not local! Called from C!
icculus@29
   341
    -- !!! FIXME: this should lose the key in RAM and turn off the Powermate
icculus@29
   342
    -- !!! FIXME:  LED when the time expires instead of if the time has
icculus@29
   343
    -- !!! FIXME:  expired when the user is trying to get at the keychain.
icculus@29
   344
    if passwordUnlockTime ~= nil then
icculus@29
   345
        local now = os.time()
icculus@29
   346
        local maxTime = (15 * 60)  -- !!! FIXME: don't hardcode.
icculus@29
   347
        if os.difftime(now, passwordUnlockTime) > maxTime then
icculus@29
   348
            lockKeychain()
icculus@29
   349
        end
icculus@29
   350
    end
icculus@29
   351
end
icculus@29
   352
icculus@29
   353
icculus@17
   354
function keyhookPressed()  -- not local! Called from C!
icculus@22
   355
--print("keyhookPressed: running==" .. tostring(keyhookRunning))
icculus@19
   356
--    if keyhookRunning then
icculus@19
   357
--        return
icculus@19
   358
--    end
icculus@17
   359
icculus@17
   360
    keyhookRunning = true
icculus@17
   361
icculus@17
   362
    while password == nil do
icculus@17
   363
        password = runGuiPasswordPrompt(getHint())
icculus@17
   364
        if password == nil then
icculus@17
   365
            keyhookRunning = false
icculus@17
   366
            return
icculus@17
   367
        end
icculus@17
   368
        if loadKey("SL5", password) == nil then
icculus@17
   369
            password = nil  -- wrong password
icculus@17
   370
            local start = os.time()  -- cook the CPU for three seconds.
icculus@17
   371
            local now = start
icculus@17
   372
            while os.difftime(now, start) < 3 do
icculus@17
   373
                now = os.time()
icculus@17
   374
            end
icculus@18
   375
        else
icculus@18
   376
            passwordUnlockTime = os.time()
icculus@28
   377
            setPowermateLED(true)
icculus@17
   378
        end
icculus@17
   379
    end
icculus@17
   380
icculus@17
   381
    prepItems()
icculus@17
   382
icculus@17
   383
    local topmenu = makeGuiMenu()
icculus@35
   384
    local favesmenu = makeGuiMenu()
icculus@35
   385
    faveitems = {}
icculus@35
   386
icculus@35
   387
    setGuiMenuItemSubmenu(appendGuiMenuItem(topmenu, "Favorites"), favesmenu)
icculus@18
   388
icculus@29
   389
    appendGuiMenuItem(topmenu, "Lock keychain", function() lockKeychain() end)
icculus@18
   390
icculus@17
   391
    for orderi,type in ipairs(passwordTypeOrdering) do
icculus@17
   392
        local bucket = items[type]
robbie@21
   393
        if bucket ~= nil then
robbie@21
   394
            local realname = passwordTypeNameMap[type]
robbie@21
   395
            if realname == nil then
robbie@21
   396
                realname = type
robbie@21
   397
            end
robbie@21
   398
            local menuitem = appendGuiMenuItem(topmenu, realname)
robbie@21
   399
            local submenu = makeGuiMenu()
robbie@21
   400
            table.sort(bucket, function(a, b) return a.name < b.name end)
robbie@21
   401
            for i,v in pairs(bucket) do
robbie@21
   402
                build_secret_menuitems(v, submenu)
robbie@21
   403
            end
robbie@21
   404
            setGuiMenuItemSubmenu(menuitem, submenu)
robbie@21
   405
        else
icculus@37
   406
            --print("no bucket found for item type '" .. type .. "'")
icculus@17
   407
        end
icculus@17
   408
    end
icculus@35
   409
    
icculus@35
   410
    -- This favepairs stuff is obnoxious.
icculus@35
   411
    local function favepairs(t)
icculus@35
   412
        local a = {}
icculus@35
   413
        for n in pairs(t) do table.insert(a, n) end
icculus@35
   414
        table.sort(a)
icculus@35
   415
        local i = 0
icculus@35
   416
        local iter = function()
icculus@35
   417
            i = i + 1
icculus@35
   418
            if a[i] == nil then
icculus@35
   419
                return nil
icculus@35
   420
            else
icculus@35
   421
                return a[i], t[a[i]]
icculus@35
   422
            end
icculus@35
   423
        end
icculus@35
   424
        return iter
icculus@35
   425
    end
icculus@35
   426
icculus@35
   427
    for i,v in favepairs(faveitems) do
icculus@35
   428
        --dumptable("fave " .. i, v)
icculus@35
   429
        local menuitem = appendGuiMenuItem(favesmenu, v.info.name)
icculus@35
   430
        local submenu = makeGuiMenu()
icculus@35
   431
        secret_menuitem_builders[v.info.type](submenu, v.info, v.secure)
icculus@35
   432
        setGuiMenuItemSubmenu(menuitem, submenu)
icculus@35
   433
    end
icculus@35
   434
icculus@35
   435
    favepairs = nil
icculus@35
   436
    faveitems = nil
icculus@17
   437
icculus@17
   438
    popupGuiMenu(topmenu)
icculus@17
   439
end
icculus@17
   440
icculus@6
   441
icculus@1
   442
-- Mainline!
icculus@1
   443
icculus@7
   444
--for i,v in ipairs(argv) do
icculus@7
   445
--    print("argv[" .. i .. "] = " .. v)
icculus@7
   446
--end
icculus@7
   447
icculus@17
   448
-- !!! FIXME: message box, exit if basedir is wack.
icculus@17
   449
-- !!! FIXME: this can probably happen in C now (the Lua mainline is basically gone now).
icculus@28
   450
setPowermateLED(false)  -- off by default
icculus@27
   451
print("Now waiting for the magic key combo (probably Alt-Meta-\\) ...")
icculus@11
   452
giveControlToGui()
icculus@11
   453
icculus@0
   454
-- end of 1pass.lua ...
icculus@0
   455