commit 2dd625b815375bc6029fda2bb83e64c3686079f6
parent c3c5f69bb1b7c7cd4d419a8b896b121c5c9d14b7
Author: Michael Savage <mikejsavage@gmail.com>
Date: Sat, 21 Feb 2015 14:09:11 +0000
Pretty much another rewrite
Diffstat:
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 )