commit 22ef6c142d20e2466116e2f74d94f12c12caf986
parent 9418d82ce62211cc71609747e49ff013c92e5371
Author: Michael Savage <mikejsavage@gmail.com>
Date: Tue, 10 Feb 2015 21:46:25 +0000
Rewrite!
Diffstat:
.gitignore | | | 1 | + |
Makefile | | | 6 | ++++++ |
README.md | | | 37 | ++++++++++++++++++++----------------- |
merge.lua | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
pdb | | | 253 | ------------------------------------------------------------------------------- |
src/actions.lua | | | 78 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
src/main.lua | | | 198 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
update-old-db.lua | | | 99 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
8 files changed, 448 insertions(+), 270 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1 +1,2 @@
.tags
+pdb
diff --git a/Makefile b/Makefile
@@ -0,0 +1,6 @@
+pdb: src/*.lua
+ lua merge.lua src main.lua > pdb
+ chmod +x pdb
+
+clean:
+ rm -f pdb
diff --git a/README.md b/README.md
@@ -4,9 +4,9 @@ An encrypted password store with dmenu integration.
Security
--------
-It uses AES-256 in CTR mode to encrypt your database, which is fine and
-generates your 256 bit secret key from `/dev/random` and IVs from
-urandom, which is also fine.
+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.
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`,
@@ -17,8 +17,20 @@ shoulder can obviously see what you type.
Requirements
------------
-OpenSSL, lua, luafilesystem, luacrypto, lua-cjson for pdb
-xdotool, dmenu for pdbmenu
+[arc4]: https://github.com/mikejsavage/lua-arc4random
+[symmetric]: https://github.com/mikejsavage/lua-symmetric
+
+lua, [lua-arc4random][arc4], [lua-symmetric][symmetric], lua-cjson
+Additionally: xdotool, dmenu for pdbmenu
+
+
+Upgrading
+---------
+
+As of 10th Feb 2015 (commit `xxx`), 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.
Usage
@@ -28,27 +40,18 @@ pdb requires you to put a shared secret on each computer you want to
use the database on, but the database itself can be given to entities
you don't trust (Dropbox, etc) without revealing your passwords.
-You need to generate a private key for encrypting your password database
-with `pdb genkey`. This should be copied manually between computers you
-want to keep the database on and not given to anyone else.
-
Initialise the database on one of your machines with `pdb init`. You can
then start playing with it (`pdb add`, `pdb list`, etc. run `pdb` by
itself for a full list).
An example session:
- $ pdb genkey
- Generating your private key. Go do something else while we gather entropy.
- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- Done! You should chmod 700 ~/.pdb/key
- $ pdb init
- Database initialised.
+ $ pdb init
+ Initialized empty password db in /home/mike/.pdb/
+ You should chmod 600 /home/mike/.pdb/key2
$ pdb add test
Enter a password for test: fdsa
$ pdb gen test2
- Generating a password for test2. Go do something else while we gather entropy.
- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
$ pdb list
test
test2
diff --git a/merge.lua b/merge.lua
@@ -0,0 +1,46 @@
+if not arg[ 1 ] or not arg[ 2 ] then
+ print( arg[ 0 ] .. " <source directory> <path to main>" )
+ os.exit( 1 )
+end
+
+local lfs = require( "lfs" )
+
+local merged = { "#! /usr/bin/lua" }
+
+local root = arg[ 1 ]
+local main = arg[ 2 ]
+
+local function addDir( rel )
+ for file in lfs.dir( root .. "/" .. rel ) do
+ if file ~= "." and file ~= ".." then
+ local full = root .. "/" .. rel .. file
+ local attr = lfs.attributes( full )
+
+ if attr.mode == "directory" then
+ addDir( rel .. file .. "/" )
+ elseif file:match( "%.lua$" ) and ( rel ~= "" or file ~= main ) then
+ local f = io.open( full, "r" )
+ local contents = f:read( "*all" )
+ f:close()
+
+ table.insert( merged, ( [[
+ package.preload[ "%s" ] = function( ... )
+ %s
+ end]] ):format(
+ ( rel .. file ):gsub( "%.lua$", "" ):gsub( "/", "." ),
+ contents
+ )
+ )
+ end
+ end
+ end
+end
+
+addDir( "" )
+
+local f = io.open( root .. "/" .. main, "r" )
+local contents = f:read( "*all" )
+f:close()
+
+print( table.concat( merged, "\n" ) )
+print( contents )
diff --git a/pdb b/pdb
@@ -1,253 +0,0 @@
-#! /usr/bin/lua
-
--- libs
-local crypto = require( "crypto" )
-local lfs = require( "lfs" )
-local json = require( "cjson.safe" )
-
--- config
-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 ) .. ")$"
-
--- consts
-local Help =
- "Usage: " .. arg[ 0 ] .. " <command>\n"
- .. "where <command> is one of the following:\n"
- .. "\n"
- .. "genkey - generate a private key for encrypting your passwords\n"
- .. "init - creates an initial empty database\n"
- .. "add <name> - prompts you to enter a password for <name>\n"
- .. "get <name> - prints the password stored under <name>\n"
- .. "delete <name> - deleted the password stored under <name>\n"
- .. "list - lists the names of all stored passwords\n"
- .. "touch - decrypts, sanity checks and reencrypts the database\n"
- .. "gen <name> [length] [pattern] - generates a password for <name>. [pattern] is a Lua pattern\n"
- .. " examples:\n"
- .. " gen test - 32 characters, alphanumeric/puntuation/spaces\n"
- .. " gen test 16 - 16 characters, alphanumeric/puntuation/spaces\n"
- .. " gen test 10 %%d - 10 characters, numbers only\n"
- .. " gen test \"%%l \" - 32 characters, lowercase/spaces\n"
-
-local commands = { "genkey", "init", "add", "get", "delete", "list", "touch", "gen" }
-
-for _, command in ipairs( commands ) do
- commands[ command ] = true
-end
-
-local dir = os.getenv( "HOME" ) .. "/.pdb/"
-local paths = { db = dir .. "db", key = dir .. "key" }
-
-local random = assert( io.open( "/dev/random", "r" ) )
-local urandom = assert( io.open( "/dev/urandom", "r" ) )
-
--- we might need this later
-local passwordToAdd
-
--- some helpers
-local function check( condition, form, ... )
- if not condition then
- io.stdout:write( form:format( ... ) .. "\n" )
-
- os.exit( 1 )
- end
-end
-
-local function loadKey()
- local file = assert( io.open( paths.key, "r" ) )
-
- local key = file:read( "*all" )
- assert( key:len() == KeyLength, "bad key file" )
-
- assert( file:close() )
-
- return key
-end
-
-local function loadDB( key )
- local file = assert( io.open( paths.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 writeDB( db, key )
- local iv = assert( urandom:read( IVLength ) )
-
- local m = assert( json.encode( db ) )
- local c = crypto.encrypt( Cipher, m, key, iv )
-
- local key2 = crypto.digest( Hash, key )
- local hmac = crypto.hmac.digest( HMAC, c, key2, true )
-
- local file = assert( io.open( paths.db, "w" ) )
- assert( file:write( iv .. c .. hmac ) )
- assert( file:close() )
-end
-
-local function genkey()
- local rfile = io.open( paths.key, "r" )
- check( not rfile, "You already have a private key." )
-
- print( "Generating your private key. Go do something else while we gather entropy." )
-
- local key = ""
-
- while key:len() < KeyLength do
- key = key .. assert( random:read( 1 ) )
-
- io.write( "\r" )
- for i = 1, 32 do
- io.write( i <= key:len() and ">" or "-" )
- end
- io.flush()
- end
-
- print( "\a" )
-
- local wfile = assert( io.open( dir .. "key", "w" ) )
- assert( wfile:write( key ) )
- assert( wfile:close() )
-
- print( "Done! You should chmod 700 ~/.pdb/key" )
-
- os.exit( 0 )
-end
-
--- real code starts here
-
-check( arg[ 1 ] and commands[ arg[ 1 ] ], Help )
-
-lfs.mkdir( dir )
-
-if arg[ 1 ] == "genkey" then
- local ok, err = pcall( genkey )
-
- check( ok, "genkey failed: %s", err )
-
- os.exit( 0 );
-end
-
-local ok_key, key = pcall( loadKey )
-check( ok_key, "Couldn't private key: %s", key )
-
-if arg[ 1 ] == "init" then
- local file = io.open( paths.db, "r" )
- check( not file, "You already have a password database." )
-
- local ok, err_write = pcall( writeDB, { }, key )
- check( ok, "Couldn't write database: %s", err_write )
-
- print( "Database initialised." )
-
- os.exit( 0 )
-end
-
-if arg[ 1 ] == "add" then
- check( arg[ 2 ], "add needs a password name." )
-
- io.stdout:write( "Enter a password for " .. arg[ 2 ] .. ": " )
- io.stdout:flush()
-
- passwordToAdd = assert( io.stdin:read( "*l" ) )
-
- check( passwordToAdd and passwordToAdd:len() > 0, "Nevermind." )
-end
-
-local ok_load, db = pcall( loadDB, key )
-check( ok_load, "Couldn't load database: %s", db )
-
-if arg[ 1 ] == "get" then
- check( arg[ 2 ], "get needs a password name." )
-
- if db[ arg[ 2 ] ] then
- print( db[ arg[ 2 ] ] )
- else
- os.exit( 1 )
- end
-elseif arg[ 1 ] == "add" then
- check( not db[ arg[ 2 ] ], "%s is already in the database", arg[ 2 ] )
-
- db[ arg[ 2 ] ] = passwordToAdd
-elseif arg[ 1 ] == "delete" then
- check( arg[ 2 ], "delete needs a password name." )
-
- db[ arg[ 2 ] ] = nil
-elseif arg[ 1 ] == "list" then
- local passwords = { }
-
- for k in pairs( db ) do
- table.insert( passwords, k )
- end
- table.sort( passwords )
-
- for _, k in ipairs( passwords ) do
- print( k )
- end
-elseif arg[ 1 ] == "gen" then
- check( not db[ arg[ 2 ] ], "%s is already in the database", arg[ 2 ] )
-
- local genLength = tonumber( arg[ 3 ] ) or 32
- local genPattern = "[" .. ( ( ( arg[ 3 ] and not tonumber( arg[ 3 ] ) ) and arg[ 3 ] ) or arg[ 4 ] or "%w%p " ) .. "]"
-
- -- matches is a list of characters that match genPattern
- -- chars is matches repeated to best fill 256 chars to
- -- accelerate generation of passwords from small charsets
-
- local matches = { }
- local chars = { }
-
- for i = 0, 255 do
- if string.char( i ):match( genPattern ) then
- table.insert( matches, string.char( i ) )
- end
- end
-
- for i = 1, math.floor( 256 / #matches ) * #matches do
- chars[ string.char( i - 1 ) ] = matches[ ( ( i - 1 ) % #matches ) + 1 ]
- end
-
- print( "Generating a password for " .. arg[ 2 ] .. ". Go do something else while we gather entropy." )
-
- local password = ""
-
- while password:len() < genLength do
- local c = random:read( 1 )
-
- if chars[ c ] then
- password = password .. chars[ c ]
- end
-
- io.write( "\r" )
- for i = 1, genLength do
- io.write( i <= password:len() and ">" or "-" )
- end
- io.flush()
- end
-
- print( "\a" )
-
- db[ arg[ 2 ] ] = password
-end
-
-local ok_write, err_write = pcall( writeDB, db, key )
-check( ok_write, "Couldn't write database: %s", err_write )
-
-assert( random:close() )
-assert( urandom:close() )
diff --git a/src/actions.lua b/src/actions.lua
@@ -0,0 +1,78 @@
+local arc4 = require( "arc4random" )
+
+local _M = { }
+
+function _M.get( db, name )
+ if not db[ name ] then
+ return "No such password."
+ end
+
+ io.stdout:write( db[ name ] )
+end
+
+function _M.add( db, name )
+ if db[ name ] then
+ return "That password is already in the DB."
+ end
+
+ io.stdout:write( "Enter a password for " .. name .. ": " )
+ io.stdout:flush()
+
+ local password = assert( io.stdin:read( "*l" ) ) -- TODO
+
+ db[ name ] = password
+end
+
+function _M.delete( db, name )
+ if not db[ name ] then
+ return "No such password."
+ end
+
+ db[ name ] = nil
+end
+
+function _M.list( db )
+ local names = { }
+
+ for name in pairs( db ) do
+ table.insert( names, name )
+ end
+ table.sort( names )
+
+ for _, name in ipairs( names ) do
+ print( name )
+ end
+end
+
+function _M.touch( db )
+end
+
+function _M.gen( db, name, length, pattern )
+ if db[ name ] 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
+
+ db[ name ] = password
+end
+
+return _M
diff --git a/src/main.lua b/src/main.lua
@@ -0,0 +1,198 @@
+local lfs = require( "lfs" )
+local symmetric = require( "symmetric" )
+local json = require( "cjson.safe" )
+
+local actions = require( "actions" )
+
+table.unpack = table.unpack or unpack
+
+local default_length = 32
+local default_pattern = "[%w%p ]"
+
+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"
+ .. "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"
+ .. "list - list stored passwords\n"
+ .. "gen <name> [length] [pattern] - generate a password for <name>\n"
+ .. "[pattern] is a Lua pattern. Some examples:\n"
+ .. " gen test - 32 characters, alphanumeric/puntuation/spaces\n"
+ .. " gen test 16 - 16 characters, alphanumeric/puntuation/spaces\n"
+ .. " 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" )
+ 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" )
+ 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,
+ },
+}
+
+local cmd = arg[ 1 ]
+table.remove( arg, 1 )
+
+if not cmd then
+ print( help )
+
+ return os.exit( 0 )
+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" )
+ return os.exit( 1 )
+ end
+
+ lfs.mkdir( dir )
+
+ local key = write_new_key()
+ write_db( { }, key )
+
+ return os.exit( 0 )
+end
+
+local key = load_key()
+local db = load_db( key )
+
+if cmd == "gen" and #arg > 0 then
+ local length
+ local pattern
+
+ if #arg == 1 then
+ length = default_length
+ pattern = default_pattern
+ elseif #arg == 3 then
+ length = tonumber( arg[ 2 ] )
+ pattern = "[" .. arg[ 3 ] .. "]"
+ else
+ length = tonumber( arg[ 2 ] ) or default_length
+ pattern = tonumber( arg[ 2 ] ) and default_pattern or arg[ 2 ]
+ end
+
+ arg[ 2 ] = length
+ arg[ 3 ] = pattern
+end
+
+if not actions[ cmd ] then
+ io.stderr:write( help .. "\n" )
+ return os.exit( 1 )
+end
+
+if commands[ cmd ].args ~= #arg then
+ io.stderr:write( "Usage: " .. arg[ 0 ] .. " " .. cmd .. " " .. ( commands[ cmd ].syntax or "" ) .. "\n" )
+ return os.exit( 1 )
+end
+
+local err = actions[ cmd ]( db, table.unpack( arg ) )
+
+if err then
+ io.stderr:write( err .. "\n" )
+ return os.exit( 1 )
+end
+
+if commands[ cmd ] then
+ write_db( db, key )
+end
diff --git a/update-old-db.lua b/update-old-db.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 )