pdb

Simple password manager
Log | Files | Refs

commit 2dd625b815375bc6029fda2bb83e64c3686079f6
parent c3c5f69bb1b7c7cd4d419a8b896b121c5c9d14b7
Author: Michael Savage <mikejsavage@gmail.com>
Date:   Sat, 21 Feb 2015 14:09:11 +0000

Pretty much another rewrite

Diffstat:
README.md | 16+++++++++++-----
src/actions.lua | 62++++++++++++++++++++++++++++++++------------------------------
src/main.lua | 142+++++++++++++++++++++++++++++++++----------------------------------------------
src/paths.lua | 9+++++++++
update-1-openssl-to-libsodium.lua | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
update-2-db-to-flatfiles.lua | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
update-old-db.lua | 99-------------------------------------------------------------------------------
7 files changed, 270 insertions(+), 216 deletions(-)

diff --git a/README.md b/README.md @@ -5,8 +5,9 @@ Security -------- pdb uses lua-symmetric, which uses libsodium's secretbox to secure your -passwords. It also uses lua-arc4random, which uses `arc4random` pulled -from LibreSSL for generating passwords. +passwords. It also uses lua-arc4random, which uses LibreSSL's +`arc4random` for generating passwords. In short, lua-symmetric uses +standard, modern crypto. It prompts you for your password when you are adding it rather than passing it as a command line argument so people can't grab it from `ps`, @@ -20,7 +21,7 @@ Requirements [arc4]: https://github.com/mikejsavage/lua-arc4random [symmetric]: https://github.com/mikejsavage/lua-symmetric -lua, [lua-arc4random][arc4], [lua-symmetric][symmetric], lua-cjson +lua, [lua-arc4random][arc4], [lua-symmetric][symmetric] Optionally: xdotool, dmenu for pdbmenu @@ -29,8 +30,13 @@ Upgrading As of 10th Feb 2015 (commit `22ef6c142d`), pdb uses a new database format. I have included a utility to update an existing password -database, which you can run with `lua update-old-db.lua`. Note that it -also generates a new secret key. +database, which you can run with `lua +update-1-openssl-to-libsodium.lua`. Note that it also generates a new +secret key. + +As of 21st Feb 2015, (commit `XXX`), pdb uses flatfiles instead of a +database. You need to run `lua update-2-db-to-flatfiles.lua` if you wish +to use more recent versions of pdb. Usage diff --git a/src/actions.lua b/src/actions.lua @@ -1,78 +1,80 @@ +local symmetric = require( "symmetric" ) local arc4 = require( "arc4random" ) +local paths = require( "paths" ) + local _M = { } -function _M.get( db, name ) - if not db[ name ] then +function _M.get( key, path ) + local ciphertext = io.readfile( path ) + if not ciphertext then return "No such password." end - io.stdout:write( db[ name ] .. "\n" ) + local password, err = symmetric.decrypt( ciphertext, key ) + if not password then + return err + end + + print( password ) end -function _M.add( db, name ) - if db[ name ] then +function _M.add( key, path ) + if io.readfile( path ) then return "That password is already in the DB." end - io.stdout:write( "Enter a password for " .. name .. ": " ) + io.stdout:write( "Enter a password: " ) io.stdout:flush() local password = assert( io.stdin:read( "*l" ) ) -- TODO + local ciphertext = symmetric.encrypt( password, key ) - db[ name ] = password + local _, err = io.writefile( path, ciphertext ) + return err end -function _M.delete( db, name ) - if not db[ name ] then - return "No such password." - end - - db[ name ] = nil +function _M.delete( _, path ) + local ok, err = os.remove( path ) + return err end -function _M.list( db ) +function _M.list() local names = { } - - for name in pairs( db ) do - table.insert( names, name ) + for file in lfs.dir( paths.db ) do + if file ~= "." and file ~= ".." then + table.insert( names, file ) + end end table.sort( names ) - for _, name in ipairs( names ) do - print( name ) - end -end - -function _M.touch( db ) + print( table.concat( names, "\n" ) ) end -function _M.gen( db, name, length, pattern ) - if db[ name ] then +function _M.gen( key, path, length, pattern ) + if io.readfile( path ) then return "That password is already in the DB." end local allowed_chars = { } - for i = 0, 255 do if string.char( i ):match( pattern ) then table.insert( allowed_chars, string.char( i ) ) end end - if #allowed_chars == 0 then return "Nothing matches given pattern." end local password = "" - for i = 1, length do local c = allowed_chars[ arc4.random( 1, #allowed_chars ) ] - password = password .. c end + local ciphertext = symmetric.encrypt( password, key ) - db[ name ] = password + local _, err = io.writefile( path, ciphertext ) + return err end return _M diff --git a/src/main.lua b/src/main.lua @@ -1,19 +1,54 @@ local lfs = require( "lfs" ) local symmetric = require( "symmetric" ) -local json = require( "cjson.safe" ) local actions = require( "actions" ) +local paths = require( "paths" ) + +function io.readfile( path ) + local file, err = io.open( path, "r" ) + if not file then + return nil, err + end + + local contents, err = file:read( "*all" ) + file:close() + return contents, err +end + +function io.writefile( path, contents ) + local file, err = io.open( path, "w" ) + if not file then + return nil, err + end + + local ok, err = file:write( contents ) + if not ok then + file:close() + return nil, err + end + + local ok, err = file:close() + if not ok then + return nil, err + end + + return true +end table.unpack = table.unpack or unpack local default_length = 32 local default_pattern = "[%w%p ]" +local function eprintf( form, ... ) + io.stderr:write( form:format( ... ) .. "\n" ) +end + local help = "Usage: " .. arg[ 0 ] .. " <command>\n" .. "where <command> is one of the following:\n" .. "\n" - .. "init - create a new key file and empty database\n" + .. "init - create a new key\n" .. "add <name> - prompt you to enter a password for <name>\n" .. "get <name> - print the password stored under <name>\n" .. "delete <name> - delete the password stored under <name>\n" @@ -25,106 +60,47 @@ local help = .. " gen test 10 %d - 10 characters, numbers only\n" .. " gen test \"%l \" - 32 characters, lowercase/spaces" -local dir = os.getenv( "HOME" ) .. "/.pdb/" -local paths = { db = dir .. "db2", key = dir .. "key2" } - local function load_key() - local file, err = io.open( paths.key, "r" ) - if not file then - io.stderr:write( "Unable to open key file: " .. err .. "\n" ) - io.stderr:write( "You might need to create it with `pdb init`.\n" ) + local key, err = io.readfile( paths.key ) + if not key then + eprintf( "Unable to read key file: %s", err ) + eprintf( "You might need to create it with `pdb init`." ) return os.exit( 1 ) end - local key = assert( file:read( "*all" ) ) -- TODO - file:close() - return key end -local function load_db( key ) - local file, err = io.open( paths.db, "r" ) - if not file then - io.stderr:write( "Unable to open DB: " .. err .. "\n" ) - io.stderr:write( "You might need to create it with `pdb init`.\n" ) - return os.exit( 1 ) - end - - local ciphertext = assert( file:read( "*all" ) ) -- TODO - local plaintext = symmetric.decrypt( ciphertext, key ) - if not plaintext then - io.stderr:write( "DB does not decrypt with the given key.\n" ) - return os.exit( 1 ) - end - - local db = json.decode( plaintext ) - if not db then - io.stderr:write( "DB does not appear to be in JSON format.\n" ) - return os.exit( 1 ) - end - - return db -end - --- TODO: write to db2 and os.rename -local function write_db( db, key ) - local plaintext = assert( json.encode( db ) ) - local ciphertext = symmetric.encrypt( plaintext, key ) - - local file, err = io.open( paths.db, "w" ) - if not file then - io.stderr:write( "Could not open DB for writing: " .. err .. "\n" ) - return os.exit( 1 ) - end - - file:write( ciphertext ) - -- TODO - local ok = assert( file:close() ) -end - local function write_new_key() local key = symmetric.key() - local file, err = io.open( paths.key, "w" ) - if not file then - io.stderr:write( "Unable to open key file for writing: " .. err .. "\n" ) + local ok, err = io.writefile( paths.key, key ) + if not ok then + eprintf( "Unable to open key file for writing: %s", err ) return os.exit( 1 ) end - assert( file:write( key ) ) - assert( file:close() ) - return key end --- real code starts here - local commands = { add = { args = 1, syntax = "<name>", - rewrite = true, }, get = { args = 1, - syntax = "<name>", }, delete = { args = 1, syntax = "<name>", - rewrite = true, }, list = { args = 0, }, - touch = { - args = 0, - rewrite = true, - }, gen = { args = 3, syntax = "<name> [length] [pattern]", - rewrite = true, }, } @@ -138,15 +114,13 @@ if not cmd then end if cmd == "init" then - local file_key = io.open( paths.key, "r" ) - local file_db = io.open( paths.db, "r" ) - - if file_key or file_db then - io.stderr:write( "Your key file/DB already exists. Remove them and run init again if you're sure about this.\n" ) + if io.open( paths.key, "r" ) then + eprintf( "Your key file already exists. Remove it and run init again if you're sure about this." ) return os.exit( 1 ) end lfs.mkdir( dir ) + lfs.mkdir( paths.db ) local key = write_new_key() write_db( { }, key ) @@ -155,7 +129,6 @@ if cmd == "init" then end local key = load_key() -local db = load_db( key ) if cmd == "gen" and #arg > 0 then local length @@ -177,22 +150,27 @@ if cmd == "gen" and #arg > 0 then end if not actions[ cmd ] then - io.stderr:write( help .. "\n" ) + eprintf( "%s", help ) return os.exit( 1 ) end +if commands[ cmd ].args > 0 then + if arg[ 1 ]:find( "/" ) then + eprintf( "Password name can't contain slashes." ) + return os.exit( 1 ) + end + + arg[ 1 ] = paths.db .. arg[ 1 ] +end + if commands[ cmd ].args ~= #arg then - io.stderr:write( "Usage: " .. arg[ 0 ] .. " " .. cmd .. " " .. ( commands[ cmd ].syntax or "" ) .. "\n" ) + eprintf( "Usage: %s %s %s", arg[ 0 ], cmd, commands[ cmd ].syntax or "" ) return os.exit( 1 ) end -local err = actions[ cmd ]( db, table.unpack( arg ) ) +local err = actions[ cmd ]( key, table.unpack( arg ) ) if err then - io.stderr:write( err .. "\n" ) + eprintf( "%s", err ) return os.exit( 1 ) end - -if commands[ cmd ] then - write_db( db, key ) -end diff --git a/src/paths.lua b/src/paths.lua @@ -0,0 +1,9 @@ +local _M = { } + +local path = os.getenv( "HOME" ) .. "/.pdb/" + +_M.path = path +_M.db = path .. "passwords/" +_M.key = path .. "key2" + +return _M diff --git a/update-1-openssl-to-libsodium.lua b/update-1-openssl-to-libsodium.lua @@ -0,0 +1,99 @@ +local crypto = require( "crypto" ) +local symmetric = require( "symmetric" ) +local json = require( "cjson" ) + +local Cipher = "aes-256-ctr" +local Hash = "sha256" +local HMAC = "sha256" + +local KeyLength = 32 +local IVLength = 16 +local HMACLength = 32 + +local SplitCipherTextPattern = "^(" .. string.rep( ".", IVLength ) .. ")(.+)(" .. string.rep( ".", HMACLength ) .. ")$" + +local dir = os.getenv( "HOME" ) .. "/.pdb/" +local paths = { + old_db = dir .. "db", + old_key = dir .. "key", + new_db = dir .. "db2", + new_key = dir .. "key2", +} + +function io.readable( path ) + local file = io.open( path, "r" ) + + if file then + file:close() + return true + end + + return false +end + +local function load_old_key() + local file = assert( io.open( paths.old_key, "r" ) ) + + local key = file:read( "*all" ) + assert( key:len() == KeyLength, "bad key file" ) + + assert( file:close() ) + + return key +end + +local function load_old_db( key ) + local file = assert( io.open( paths.old_db, "r" ) ) + + local contents = assert( file:read( "*all" ) ) + assert( file:close() ) + + local iv, c, hmac = contents:match( SplitCipherTextPattern ) + assert( iv, "Corrupt DB" ) + + local key2 = crypto.digest( Hash, key ) + assert( hmac == crypto.hmac.digest( HMAC, c, key2, true ), "Corrupt DB" ) + + local m = crypto.decrypt( Cipher, c, key, iv ) + + return assert( json.decode( m ) ) +end + +local function write_new_key() + local key = symmetric.key() + + local file, err = io.open( paths.new_key, "w" ) + if not file then + io.stderr:write( "Unable to open key file for writing: " .. err .. "\n" ) + return os.exit( 1 ) + end + + assert( file:write( key ) ) + assert( file:close() ) + + return key +end + +local function write_db( db, key ) + local plaintext = assert( json.encode( db ) ) + local ciphertext = symmetric.encrypt( plaintext, key ) + + local file, err = io.open( paths.new_db, "w" ) + if not file then + io.stderr:write( "Could not open DB for writing: " .. err .. "\n" ) + return os.exit( 1 ) + end + + file:write( ciphertext ) + local ok = assert( file:close() ) +end + +if io.readable( paths.new_key ) or io.readable( paths.new_db ) then + io.stderr:write( "You already have a key/DB in the new format!\n" ) + return os.exit( 1 ) +end + +local db = load_old_db( load_old_key() ) + +local key = write_new_key() +write_db( db, key ) diff --git a/update-2-db-to-flatfiles.lua b/update-2-db-to-flatfiles.lua @@ -0,0 +1,59 @@ +local lfs = require( "lfs" ) +local symmetric = require( "symmetric" ) +local json = require( "cjson" ) + +local dir = os.getenv( "HOME" ) .. "/.pdb/" +local paths = { + old_db = dir .. "db2", + key = dir .. "key2", + new_db = dir .. "passwords/", +} + +lfs.mkdir( paths.new_db ) + +local function load_key() + local file, err = io.open( paths.key, "r" ) + if not file then + io.stderr:write( "Unable to open key file: " .. err .. "\n" ) + io.stderr:write( "You might need to create it with `pdb init`.\n" ) + return os.exit( 1 ) + end + + local key = assert( file:read( "*all" ) ) -- TODO + file:close() + + return key +end + +local function load_db( key ) + local file, err = io.open( paths.old_db, "r" ) + if not file then + io.stderr:write( "Unable to open DB: " .. err .. "\n" ) + io.stderr:write( "You might need to create it with `pdb init`.\n" ) + return os.exit( 1 ) + end + + local ciphertext = assert( file:read( "*all" ) ) -- TODO + local plaintext = symmetric.decrypt( ciphertext, key ) + if not plaintext then + io.stderr:write( "DB does not decrypt with the given key.\n" ) + return os.exit( 1 ) + end + + local db = json.decode( plaintext ) + if not db then + io.stderr:write( "DB does not appear to be in JSON format.\n" ) + return os.exit( 1 ) + end + + return db +end + +local key = load_key() +local db = load_db( key ) + +for k, v in pairs( db ) do + local file = io.open( paths.new_db .. k, "w" ) + file:write( symmetric.encrypt( v, key ) ) + file:close() +end diff --git a/update-old-db.lua b/update-old-db.lua @@ -1,99 +0,0 @@ -local crypto = require( "crypto" ) -local symmetric = require( "symmetric" ) -local json = require( "cjson" ) - -local Cipher = "aes-256-ctr" -local Hash = "sha256" -local HMAC = "sha256" - -local KeyLength = 32 -local IVLength = 16 -local HMACLength = 32 - -local SplitCipherTextPattern = "^(" .. string.rep( ".", IVLength ) .. ")(.+)(" .. string.rep( ".", HMACLength ) .. ")$" - -local dir = os.getenv( "HOME" ) .. "/.pdb/" -local paths = { - old_db = dir .. "db", - old_key = dir .. "key", - new_db = dir .. "db2", - new_key = dir .. "key2", -} - -function io.readable( path ) - local file = io.open( path, "r" ) - - if file then - file:close() - return true - end - - return false -end - -local function load_old_key() - local file = assert( io.open( paths.old_key, "r" ) ) - - local key = file:read( "*all" ) - assert( key:len() == KeyLength, "bad key file" ) - - assert( file:close() ) - - return key -end - -local function load_old_db( key ) - local file = assert( io.open( paths.old_db, "r" ) ) - - local contents = assert( file:read( "*all" ) ) - assert( file:close() ) - - local iv, c, hmac = contents:match( SplitCipherTextPattern ) - assert( iv, "Corrupt DB" ) - - local key2 = crypto.digest( Hash, key ) - assert( hmac == crypto.hmac.digest( HMAC, c, key2, true ), "Corrupt DB" ) - - local m = crypto.decrypt( Cipher, c, key, iv ) - - return assert( json.decode( m ) ) -end - -local function write_new_key() - local key = symmetric.key() - - local file, err = io.open( paths.new_key, "w" ) - if not file then - io.stderr:write( "Unable to open key file for writing: " .. err .. "\n" ) - return os.exit( 1 ) - end - - assert( file:write( key ) ) - assert( file:close() ) - - return key -end - -local function write_db( db, key ) - local plaintext = assert( json.encode( db ) ) - local ciphertext = symmetric.encrypt( plaintext, key ) - - local file, err = io.open( paths.new_db, "w" ) - if not file then - io.stderr:write( "Could not open DB for writing: " .. err .. "\n" ) - return os.exit( 1 ) - end - - file:write( ciphertext ) - local ok = assert( file:close() ) -end - -if io.readable( paths.new_key ) or io.readable( paths.new_db ) then - io.stderr:write( "You already have a key/DB in the new format!\n" ) - return os.exit( 1 ) -end - -local db = load_old_db( load_old_key() ) - -local key = write_new_key() -write_db( db, key )