/* [ʞ] kpw.c - password manager
* ↳ derivative of mkpw.c
* ~ lexi hale <lexi@hale.su>
* © AGPLv3
* $ cc -O4 kpw.c -okpw [-D_CLIPBOARD]
* - D_CLIPBOARD enables kpw to automatically
* copy passwords to the clipboard. it does
* this by attempting to execute a sequence
* of binaries, and then writing the password
* to STDIN of the binary that succeeds.
* - D_SAVEKEY enables kpw to store the database
* key in persistent memory between invocations,
* leading to quicker decryption and access
* times. only available on systems with SYSV
* shared memory.
* ? generates passwords
* → kpw is unlikely to be portable to non-POSIX
* systems, but should run fine on Linux as well
* as BSDs with getrandom() support.
* → kpw has the following dependencies:
* - libsodium
* ! for getrandom() to work with the version of
* libc on my android phone, the getrandom() call
* had to be converted to use the syscall()
* interface. this is unlikely to cause problems,
* but should be kept in mind.
*
* TODO prevent pw reads from going off the edge of
* the screen and fucking up all the shit
*/
#include <unistd.h>
#include <sys/random.h>
#include <sys/syscall.h>
#include <sys/shm.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sodium.h>
#include <termios.h>
#define sz(a) ( sizeof (a) / sizeof (a) [0] )
#define say(x) (write(2, (x), sizeof (x)))
#define _str(s) (s),sizeof(s)
#ifdef _CLIPBOARD
# include <sys/types.h>
# include <pwd.h>
# include <stdlib.h>
#else
# define copy(str,len)
#endif
enum /* constants */ {
null = 0, true = 1, false = 0,
kpw_shm_key = 0x3CC215A,
};
#include "err.inc"
enum /* db format constants */ {
db_pubkey_len = crypto_box_PUBLICKEYBYTES,
db_privkey_len = crypto_box_SECRETKEYBYTES,
kpw_db_pw_max = 64,
default_pw_len = 32,
};
typedef _Bool bool;
typedef unsigned long long iaia_word_type;
typedef bad iaia_error_type;
enum /* iaia errors */ {
iaia_e_ok = ok,
iaia_e_base = fail,
iaia_e_domain = fail,
iaia_e_overflow = fail,
};
#define _IAIA_FN_ATOI katoi
#define _IAIA_FN_ITOA kitoa
#define _IAIA_EXTERNAL_TYPES
#include "clib/iaia.c"
#define k_static
#include "clib/compose.c"
#undef k_static
typedef enum {
tbl_ok = ok, tbl_error = fail
} tbl_error_type;
typedef unsigned char tbl_word_type;
#include "clib/tbl.c"
#include "opt.inc"
typedef uint8_t key_priv [db_privkey_len];
typedef uint8_t key_pub [db_pubkey_len];
typedef uint8_t db_sz;
typedef uint8_t byte;
#define str_ucase "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define str_lcase "abcdefghijklmnopqrstuvwxyz"
#define str_num "0123456789"
const char* reftbl = str_num str_ucase str_lcase;
const char* reftbl_lcase = str_num str_lcase;
const char* _g_binary_name;
enum { plain_term, ansi_term, color_term } _g_term_type[3];
enum alert {a_notice, a_warn, a_debug, a_fatal = 1 << 8};
pstr alert_msg[] = {
_p("(kpw notice)"), _p("(kpw warn)"),
_p("(kpw debug)"), _p("(kpw fatal)")
};
bool _g_alert_quiet = false,
_g_debug_msgs = false;
void alert(uint16_t kind, const char* msg) {
if (!((kind >= a_fatal) ||
(_g_debug_msgs && kind == a_debug) ||
(!_g_alert_quiet && kind != a_debug))) return;
uint8_t idx;
if (kind & a_fatal) idx = a_debug + 1;
else idx = kind;
if (_g_term_type[2] == color_term) {
char msgcode[] = "\x1b[90m";
char* color = msgcode+3;
*color = '0' + (4 - idx);
write(2,msgcode, sz(msgcode));
} else if (_g_term_type[2] == ansi_term) {
write(2,"\x1b[1m",4);
}
write(2,alert_msg[idx].ptr,
alert_msg[idx].len);
if (_g_term_type[2] != plain_term) write(2,"\x1b[m ",4);
else write(2," ",1);
write(2,msg,strlen(msg));
write(2,"\n",1);
if (kind & a_fatal) exit(kind & (~a_fatal));
}
#ifdef _CLIPBOARD
char* const*
cbd_cmds[] = {
/* NOTE: these commands must be specified in order of
* most- to least-specific. more than one utility may
* be present on a given system, so we need to make sure
* the right one is called. */
(char* const[]){"termux-clipboard-set", null},
(char* const[]){"xsel", "-bi", null},
/* TODO: allow command to be specified by env var */
null
};
enum bad
copy(const char* str, size_t len) {
alert(a_debug, "copying password to clipboard");
if (geteuid() == 0) {
/* on a sane system, what we'd do is hike up the process
* tree til we found a non-root user. alas, this is UNIX. */
const char* realuser = getenv("SUDO_USER");
if (realuser == null) realuser = "nobody";
alert(a_warn, "running as root! dropping privileges to prevent malicious use of copy functionality");
setenv("USER", realuser, true);
struct passwd* nobody = getpwnam(realuser);
if (nobody == null) {
alert(a_fatal | bad_user, "could not get UID to drop privileges; bailing");
return bad_user;
} else {
setenv("HOME", nobody -> pw_dir, true);
setenv("SHELL", "/dev/null", true);
setuid(nobody -> pw_uid);
if (geteuid() == 0)
alert(a_fatal | bad_user, "i don't fucking think so, you sneaky bastard");
}
}
char* const clipboard_env = getenv("mkpw_clipboard_setter");
char* const clipboard_env_arg = getenv("mkpw_clipboard_setter_arg");
// FIXME: allow multiple args
int fds[2];
if (pipe(fds) != 0) return bad_pipe;
if (!fork()) {
close(fds[1]);
dup2(fds[0], 0);
if (clipboard_env != null) {
execvp(clipboard_env, (char* const[]){
clipboard_env, clipboard_env_arg, null});
return bad_copy;
} else for(char* const** cmd = cbd_cmds; *cmd != null; ++cmd) {
execvp((*cmd)[0], *cmd);
}
return bad_copy;
} else {
close(fds[0]);
write(fds[1], str, len);
write(fds[1], "\n", 1);
close(fds[1]);
return ok;
}
}
#endif
enum genmode { upper, mix, lower, stupid };
enum bad
mkpw(enum genmode mode, char* buf, size_t const len) {
const unsigned char chars = (sizeof str_num - 1) +
((mode == upper) ? (sizeof str_ucase - 1) :
((mode == lower) ? (sizeof str_lcase - 1) :
((sizeof str_ucase - 1) + (sizeof str_lcase - 1))));
const char* tbl = (mode == upper) ? reftbl :
((mode == lower) ? reftbl_lcase : reftbl);
unsigned char noise[len];
unsigned char* cur = noise;
/* getrandom(noise, len, 0); // android doesnt like it */
if(syscall(SYS_getrandom, noise, len, 0) == -1)
return bad_entropy;
for(char* ptr = buf; ptr < buf + len; ++ptr, ++cur) {
*ptr = tbl[*cur % chars]; /* such a waste of entropy… :( */
}
if(mode == stupid) {
/* appease systems with stupid password reqs */
buf[0] = '!';
buf[1] = 'a' + (noise[1] % 26);
buf[2] = 'A' + (noise[1] % 26);
}
buf[len] = '\n';
return ok;
}
void
bytedump(byte* bytes, size_t sz) {
for (size_t i = 0; i < sz; ++i) {
char tpl[] ="\x1b[35m \x1b[m";
char* c = tpl + 5;
*c = bytes[i];
if (*c < ' ' || *c > '~') *c='.', write(2, tpl, sz(tpl));
else write(2, c, 1);
}
}
void
hexdump(byte* bytes, size_t sz) {
if(!_g_debug_msgs) return;
alert(a_debug, "printing hex dump");
byte* st = bytes;
write(2, _str("\t\x1b[94m"));
for (size_t i = 0; i < sz; ++i) {
char hex[5] = " ";
kitoa(16, bytes[i], hex, hex + 2, NULL, true);
write(2, hex, 4);
if(!((i+1)%8)) {
write(2, _str("\x1b[;1m│\x1b[m "));
bytedump(st, 8);
write(2, "\n\t\x1b[94m", (i == sz - 1 ? 1 : 7));
st += 8;
} else if (i == sz - 1) {
write(2, _str("\x1b[;1m│\x1b[m "));
bytedump(st, (bytes + sz) - st);
write(2, _str("\n\x1b[m"));
}
}
}
struct dbrecord { pstr acct; pstr pw; };
enum bad
dbtell(int db, db_sz* ciphlen, db_sz* plainlen){
if (read(db, ciphlen, 1) != 1)
return fail;
*plainlen = *ciphlen - crypto_box_SEALBYTES;
return ok;
}
enum bad
dbnext(int db, struct dbrecord* rec, db_sz acctlen,
byte* pub, byte* priv,
byte ciphertext[static acctlen],
byte plaintext[static acctlen - crypto_box_SEALBYTES]) {
db_sz plaintext_sz = acctlen - crypto_box_SEALBYTES;
if (read(db, ciphertext, acctlen) != acctlen)
return bad_db_corrupt;
alert(a_debug, "scanned record");
hexdump(ciphertext, acctlen);
if(crypto_box_seal_open(plaintext, ciphertext, acctlen, pub, priv) != 0)
return bad_db_corrupt;
alert(a_debug, "record deciphered");
hexdump(plaintext, plaintext_sz);
db_sz record_name_len = plaintext[0],
record_pw_len = plaintext[record_name_len + 1];
rec -> acct.len = record_name_len;
rec -> acct.ptr = plaintext + 1;
rec -> pw.len = record_pw_len;
rec -> pw.ptr = plaintext + record_name_len + 2;
return ok;
}
enum bad
dbappend(struct dbrecord rec) {
return ok;
}
enum term_clear {term_clear_line,
term_clear_screen};
void term_clear(int tty, enum term_clear behavior) {
switch(behavior) {
case term_clear_line: write(tty,"\r\x1b[2K",5); break;
case term_clear_screen: write(tty,"\r\x1b[3J",5); break;
}
}
void term_bell(int tty) {
write(tty,"\a",1);
}
typedef char password[kpw_db_pw_max + 1];
bad pwread(bool obscure, char* dest, size_t* out_len, const char* prompt, const size_t plen) {
if (isatty(0)) {
int tty = 1;
if (!isatty(tty)) tty = open("/dev/tty", O_WRONLY);
if (tty == -1) return bad_insane;
struct termios initial; {
/* in order to take PW input, we need to shut
* off echo and canonical mode. now we're in
* charge of reading each keypress. */
tcgetattr(tty, &initial);
struct termios nt = initial;
nt.c_lflag &= (~ECHO & ~ICANON);
tcsetattr(tty, TCSANOW, &nt);
}
*dest = 0;
char* p = dest;
do {
term_clear(tty,term_clear_line);
if (_g_term_type[0] >= ansi_term) write(tty, "\x1b[1m", 4);
write(tty, prompt, plen);
if (_g_term_type[0] >= ansi_term) write(tty, "\x1b[21m", 5);
if (obscure) for(size_t i = 0; i < p - dest; ++i)
write(tty, "*", 1);
else write(tty, dest, p-dest);
char c;
if (read(0, &c, 1) == 1) {
switch (c) {
case '\n': case '\r':
/* accept pw */
if (p > dest) goto end_read_loop;
else break;
case '\x1b': /* escape */
term_clear(tty, term_clear_line);
return bad_cancel;
case '\b': case '\x7f':
if (p > dest) *p--=0;
else term_bell(tty);
break;
default:
if (p - dest != 64) *p++=c;
else term_bell(tty);
}
} else {
/* either EOF or an error - either way,
* we're finished here */
break;
}
} while(1);
end_read_loop: term_clear(tty, term_clear_line);
*p = 0;
if (out_len!=NULL) *out_len = p - dest;
/* return the terminal to normal */
tcsetattr(tty, TCSANOW, &initial);
if (tty != 1) close(tty);
} else {
alert(a_warn, "reading pw from standard input");
ssize_t ct = read(0, dest, kpw_db_pw_max);
dest[ct] = 0;
}
return ok;
}
int
dbopen(int flags) {
const char* dbpath = getenv("kpw_db");
int db;
if (dbpath == NULL) {
const char* cfg = getenv("XDG_CONFIG_HOME");
if (cfg == NULL) {
const char* home = getenv("HOME");
if (home == NULL) exit(bad_insane);
size_t homelen = strlen(home);
pstr path[] = { {homelen, home}, _p("/.config/kpw.db") };
char buf[homelen + path[1].len + 1];
bzero(buf, sz(buf));
impose(path, sz(path), NULL, buf);
db = open(buf, flags, 0600);
} else {
size_t cfglen = strlen(cfg);
pstr path[] = { {cfglen, cfg}, _p("/kpw.db") };
char buf[cfglen + path[1].len + 1];
bzero(buf, sz(buf));
impose(path, sz(path), NULL, buf);
db = open(buf, flags, 0600);
}
} else {
db = open(dbpath, flags, 0600);
}
return db;
}
enum bad
dbheader_load (int db, byte* salt, byte* salt_enc, byte* pub, byte* priv_enc) {
const size_t salt_sz = crypto_pwhash_SALTBYTES,
salt_enc_sz = crypto_box_SEALBYTES + salt_sz,
pub_sz = sizeof(key_pub),
priv_sz = sizeof(key_priv);
alert(a_debug, "loading public key");
ssize_t sr = read(db, pub, pub_sz);
if (sr != pub_sz) return bad_db_corrupt;
hexdump(pub, pub_sz);
alert(a_debug, "loading password salt");
sr = read(db, salt, salt_sz);
if (sr != salt_sz) return bad_db_corrupt;
hexdump(salt, salt_sz);
alert(a_debug, "loading encrypted private key");
read(db, priv_enc, priv_sz);
hexdump(priv_enc, priv_sz);
alert(a_debug, "loading verification hash");
read(db, salt_enc, salt_enc_sz);
hexdump(salt_enc, salt_enc_sz);
return ok;
}
enum bad
dbunlock(byte* priv_enc, byte* salt, byte* priv) {
const size_t priv_sz = sizeof(key_priv);
byte key [db_privkey_len];
/* is the private key loaded into memory? */
#ifdef _SAVEKEY
int shm = shmget(*((key_t*) salt), sizeof(key_priv), 0);
if (shm == -1) {
#endif
/* no key in memory - read password from stdin instead */
password dbpw; size_t pwlen;
bad e = pwread(true, dbpw, &pwlen,_str("database key: "));
if (e != ok) return e;
alert(a_debug, "deriving secret");
if(crypto_pwhash(key, sz(key), dbpw, pwlen, salt,
crypto_pwhash_OPSLIMIT_INTERACTIVE,
crypto_pwhash_MEMLIMIT_INTERACTIVE,
crypto_pwhash_ALG_DEFAULT) != 0) {
return bad_mem;
}
hexdump(key, sz(key));
alert(a_debug, "attempting to decrypt private key");
for (size_t i = 0; i < sz(key); ++i) {
priv[i] = priv_enc[i] ^ key[i];
}
hexdump(priv, sz(key));
#ifdef _SAVEKEY
} else {
/* found a key in memory; loading it into *priv */
alert(a_debug, "using saved key");
key_priv* saved = shmat(shm, 0, 0);
if (saved == (void*)-1)
return bad_shm;
hexdump((byte*)saved, sizeof(key_priv));
memcpy(priv, saved, sizeof(key_priv));
shmdt(saved);
}
#endif
return ok;
}
enum bad
dbverify(byte* salt, byte* salt_enc, byte* pub, byte* priv) {
byte salt_dec [crypto_pwhash_SALTBYTES];
alert(a_debug, "decrypting verification hash");
int r = crypto_box_seal_open(salt_dec, salt_enc,
sz(salt_dec) + crypto_box_SEALBYTES, pub, priv);
if (r != 0) return bad_pw;
hexdump(salt_dec, sz(salt_dec));
if (memcmp(salt,salt_dec,sz(salt_dec)) != 0) return bad_db_corrupt;
else return ok;
}
enum bad
dbdecrypt(int db, byte* pubkey, byte* privkey) {
byte salt [crypto_pwhash_SALTBYTES],
priv_enc [db_privkey_len],
priv [db_privkey_len],
pub [db_pubkey_len];
byte salt_enc [crypto_box_SEALBYTES + sz(salt)];
bad e;
if ((e = dbheader_load(db, salt, salt_enc, pub, priv_enc)) != ok)
return e;
if ((e = dbunlock(priv_enc, salt, priv)) != ok)
return e;
if ((e = dbverify(salt, salt_enc, pub, priv)) != ok)
return e;
/* TODO refactor to avoid unnecessary memcpy */
memcpy(privkey, priv, sz(priv));
memcpy(pubkey, pub, sz(pub));
return ok;
}
void
bright(int fd, const char* str, size_t len) {
if (_g_term_type[fd] >= ansi_term) write(fd, _str("\x1b[1m"));
write(fd, str, len);
if (_g_term_type[fd] >= ansi_term) write(fd, _str("\x1b[21m"));
}
void*
transcribe(void* dest, void* src, size_t sz) {
memcpy(dest, src, sz); return dest + sz;
}
enum bad
emit_usage(const char* text) {
say("\x1b[1musage:\x1b[m ");
write(2, _g_binary_name, strlen(_g_binary_name));
if (text == NULL) {
write(2, kpw_optstr, sz(kpw_optstr));
write(2, kpw_usage, sz(kpw_usage));
} else write(2, text, strlen(text));
return bad_usage;
}
int
kpw(int argc, const char** argv) {
if (argc == 0) return bad_insane;
_g_binary_name = argv[0];
enum genmode
mode = lower;
enum {usage,getpw,addpw,delpw,lspw,
genpw,regen,mergedb,chpw,keyin,
logout,createdb,rekeydb,help}
op = getpw;
const char* params[3];
uint8_t param = 0;
bool print = false,
clobber = false,
no_more_opts = false;
# ifdef _CLIPBOARD
bool copy_pw = true;
# endif
for (const char** arg = argv + 1; *arg != null; ++arg) {
if (!no_more_opts && (*arg)[0] == '-') {
if ((*arg)[1] == '-') { /* long opt */
if((*arg)[2] == 0) {
no_more_opts = true;
continue;
}
unsigned char a;
if (tblget(sz(argtbl), argtbl, *arg + 2, &a) == ok) switch (a) {
kpw_emit_long_option_switch
} else {
return bad_option;
}
} else { /* short opt */
for(const char* ptr = (*arg) + 1; *ptr != 0; ++ptr) {
switch(*ptr) {
kpw_emit_short_option_switch
default: return bad_option;
}
}
}
} else {
if (param > sz(params)) return bad_syntax;
params[param++] = *arg;
}
}
if (op == getpw && param == 0) return emit_usage(NULL);
if (sodium_init() < 0)
return bad_lib_sodium_init;
switch(op) {
# ifdef _SAVEKEY
case logout:
case keyin: {
if (param != 0) return bad_syntax;
int db = dbopen(O_RDONLY);
key_pub pub;
key_priv priv, priv_enc;
byte salt [crypto_pwhash_SALTBYTES],
salt_enc [crypto_box_SEALBYTES + sz(salt)];
bad e;
if ((e = dbheader_load(db, salt, salt_enc, pub, priv_enc)) != ok)
return e;
key_t ipck = *((key_t*)salt);
if (op == keyin) {
if ((e = dbunlock(priv_enc, salt, priv)) != ok)
return e;
if ((e = dbverify(salt, salt_enc, pub, priv)) != ok)
return e;
int shm = shmget(ipck, sizeof(key_priv),
IPC_CREAT | IPC_EXCL | 0600);
if (shm == -1) return bad_shm_exist;
key_priv* saved = shmat(shm, 0, 0);
if (saved == (void*)-1) return bad_shm;
memcpy(saved, priv, sz(priv));
shmdt(saved);
} else {
int shm = shmget(ipck, sizeof(key_priv), 0);
if (shm == -1) return bad_no_shm;
shmctl(shm, IPC_RMID, NULL);
}
return ok;
}
# endif
case genpw:
case addpw: {
if (param == 0) return emit_usage(
op == addpw ? " -a[p] <account> [<pw>]\n" :
/* genpw */" -g[lmusp] <account> [<pw len>]\n");
if (param > 2 || param < 1) return bad_syntax;
const char* acct = params[0],
* prm = (param == 2 ? params[1] : NULL);
alert(a_debug, "opening database");
int db = dbopen(O_RDWR);
if (db == -1) return bad_db_load;
alert(a_debug, "reading in public key");
byte key [db_pubkey_len];
ssize_t e = read(db, key, sz(key));
if(e < sz(key)) return bad_db_corrupt;
lseek(db, 0, SEEK_END);
bool tty_in = isatty(0),
tty_out = isatty(1);
password pw; size_t pwlen;
const char* acct_pw;
if (op == addpw) {
if (prm == NULL) {
pstr prompt_l[] = { _p("- new password for "),
{0, acct}, _p(": "), };
char prompt[pstrsum(prompt_l, sz(prompt_l))];
if (tty_in) pstrcoll(prompt_l, sz(prompt_l), prompt);
bad e = pwread(!print, pw, &pwlen,
prompt, sz(prompt));
if (e != ok) return e;
if (tty_in && !print) {
password pw_conf;
e = pwread(true, pw_conf, NULL, _str("- confirm: "));
if (e != ok) return e;
if (strcmp(pw,pw_conf) != 0)
return bad_pw_match;
}
acct_pw = pw;
} else acct_pw = prm, pwlen = strlen(prm);
} else if (op == genpw) {
unsigned long long len;
if (prm != NULL) {
alert(a_debug, "converting length parameter to integer");
bad e = katoi(10, prm, &len);
if (e != ok) return bad_num;
} else alert(a_debug, "using default password length"),
len = default_pw_len;
alert(a_debug, "generating new password");
if (mkpw(mode, pw, len) == bad_entropy) return bad_entropy;
if (print || !tty_out) {
write(1, pw, len);
if(tty_out) write(1, "\n", 1);
}
pwlen = len;
acct_pw = pw;
}
# ifdef _CLIPBOARD
if (copy_pw) copy(pw, pwlen);
# endif
alert(a_debug, "encoding database entry");
db_sz acctlen = strlen(acct);
byte plaintext[1 + acctlen +
1 + pwlen];
plaintext[0] = acctlen;
strncpy(plaintext + 1, acct, acctlen);
plaintext[1 + acctlen] = pwlen;
strncpy(plaintext + acctlen + 2, acct_pw, pwlen);
hexdump(plaintext, sz(plaintext));
alert(a_debug, "enciphering database entry");
byte ciphertext[sz(plaintext) + crypto_box_SEALBYTES];
crypto_box_seal(ciphertext, plaintext, sz(plaintext), key);
hexdump(ciphertext, sz(ciphertext));
alert(a_debug, "writing ciphertext to db");
db_sz ciphertext_len = sz(ciphertext);
write(db, &ciphertext_len, 1);
write(db, &ciphertext, ciphertext_len);
close(db);
break;
}
case chpw:
case regen:
case delpw:{ /* kpw -d <acct> */
if (param == 0) return emit_usage
(op==delpw ? " -d <account>\n" :
op==regen ? " -r[lmusp] <account> [<pw len>]\n" :
/* op==chpw */ " -c <account> [<new pw>]\n");
if (param < 1 || param > (op == delpw ? 1 : 2))
return bad_syntax;
const char* target = params[0];
const char* delta;
if (param == 2) delta=params[1];
else delta=NULL;
int db = dbopen(O_RDWR);
if (db == -1) return bad_db_load;
const size_t dbsz = lseek(db, 0, SEEK_END);
lseek(db, 0, SEEK_SET);
key_pub pub;
key_priv priv, priv_enc;
byte salt [crypto_pwhash_SALTBYTES],
salt_enc [crypto_box_SEALBYTES + sz(salt)];
bad e;
if ((e = dbheader_load(db, salt, salt_enc, pub, priv_enc)) != ok)
return e;
if ((e = dbunlock(priv_enc, salt, priv)) != ok)
return e;
if ((e = dbverify(salt, salt_enc, pub, priv)) != ok)
return e;
byte newdb [dbsz];
byte* cursor = newdb;
/* header is unchanged, so we copy it back out as is */
cursor = transcribe(cursor, pub, sz(pub));
cursor = transcribe(cursor, salt, sz(salt));
cursor = transcribe(cursor, priv_enc, sz(priv_enc));
cursor = transcribe(cursor, salt_enc, sz(salt_enc));
/* now we iterate through each record, decrypting them
* with the keys we've obtained to compare the name
* against the `target` specified by the user. all
* records that do not match are written back to newdb,
* while records that match are skipped. */
db_sz ctlen, ptlen; bool found = false; size_t scanned = 0;
while((e = dbtell(db, &ctlen, &ptlen)) == ok) {
++ scanned;
/* dbtell gives us the length of the buffers we
* need to allocate, allowing us to keep all the
* work on the stack and avoiding any malloc bs */
byte ctbuf [ctlen],
ptbuf [ptlen];
struct dbrecord rec;
bad d;
if((d = dbnext(db, &rec, ctlen,
pub, priv, ctbuf, ptbuf)) != ok) return d;
if(strlen(target) == rec.acct.len && strncmp(target, rec.acct.ptr, rec.acct.len) == 0) {
/* found a matching record; determine
* what to do to the fucker */
alert(a_debug, "found target record");
found = true;
if (op == delpw) continue;
password pwbuf;
const char* newpass;
size_t pwlen;
if (op == regen) {
alert(a_debug, "generating new password");
/* generating a new password. use the default
* length if the user hasn't supplied one herself,
* or if she has, convert it to an integer. */
if (delta == NULL) pwlen = default_pw_len; else {
unsigned long long value;
bad k = katoi(10, delta, &value);
if (k != ok) return bad_num;
pwlen = value;
}
bad m = mkpw(mode, pwbuf, pwlen);
if (m != ok) return m;
newpass = pwbuf;
} else if (op == chpw) {
/* the user has requested a password change. take
* it from the command line if available, otherwise
* generate a prompt and read from stdin */
if (delta == NULL) {
pstr prompt_l[] = { _p("- new password for "),
{0, target}, _p(": "), };
char prompt[pstrsum(prompt_l, sz(prompt_l))];
if (_g_term_type[0] > plain_term)
pstrcoll(prompt_l, sz(prompt_l), prompt);
bad p = pwread(!print, pwbuf, &pwlen, prompt, sz(prompt));
if (p != ok) return p;
/* prompt again to make sure the user entered
* her new password correctly */
if(!print && _g_term_type[0] > plain_term) {
password passconf;
p = pwread(!print, passconf, NULL, _str("confirm: "));
if (p != ok) return p;
if (strcmp(passconf, pwbuf) != 0)
return bad_pw_match;
}
newpass = pwbuf;
} else newpass = delta, pwlen = strlen(delta);
} else return bad_assert;
if (op == regen && print) {
write(1, newpass, pwlen);
if (_g_term_type[1] > plain_term) write(1, "\n", 1);
}
# ifdef _CLIPBOARD
if (copy_pw) copy(newpass, pwlen);
# endif
/* new pw is pointed to by `newpass`. encrypt it
* and insert it into the new database image */
byte plaintext [1 + rec.acct.len +
1 + pwlen ];
plaintext[0] = rec.acct.len;
memcpy(plaintext + 1, rec.acct.ptr, rec.acct.len);
plaintext[1 + rec.acct.len] = pwlen;
memcpy(plaintext + 2 + rec.acct.len, newpass, pwlen);
alert(a_debug, "enciphering plaintext of modified record");
hexdump(plaintext, sz(plaintext));
byte ciphertext [sz(plaintext) + crypto_box_SEALBYTES];
crypto_box_seal(ciphertext, plaintext, sz(plaintext), pub);
db_sz new_ct_len = sz(ciphertext);
alert(a_debug, "copying ciphertext into database");
cursor = transcribe(cursor, &new_ct_len, 1);
cursor = transcribe(cursor, ciphertext, sz(ciphertext));
} else {
/* not our target, copy it into the buffer as-is */
cursor = transcribe(cursor, &ctlen, sizeof(db_sz));
cursor = transcribe(cursor, ctbuf, sz(ctbuf));
}
} if (e != fail) return e;
if (scanned == 0) return bad_db_empty;
if (!found) return bad_index;
/* newdb should now be an image of the database without
* the offending record. we can now copy it back out to
* disk, truncating the file to start from scratch. */
alert(a_debug, "writing modified db back out to disk");
ftruncate(db,0);
lseek(db, 0, SEEK_SET);
write(db, newdb, cursor-newdb);
/* we're done here, time to close up shop */
close(db);
return ok;
}
case getpw: /* kpw <acct> */
case lspw: { /* kpw -t[p] [<prefix>] */
const char* target;
if (param == 1) target = params[0];
else if (param == 0) target = NULL;
else return bad_syntax;
alert(a_debug, "opening database for reading");
int db = dbopen(O_RDONLY);
if (db == -1) return bad_db_load;
key_pub pub;
key_priv priv;
/* try to decrypt db */ {
bad e = dbdecrypt(db,pub,priv);
if (e != ok) return e;
/* TODO allow multiple tries */
}
/* cursor should be positioned on first
* record if we've made it this far */
alert(a_debug, "beginning to scan records");
struct dbrecord rec;
bool found = (op == lspw);
enum bad result;
db_sz ciphlen, plainlen;
size_t scanned = 0;
while ((result = dbtell(db, &ciphlen, &plainlen)) == ok) {
++ scanned;
byte ctbuf[ciphlen], ptbuf[plainlen];
if ((result = dbnext(db, &rec, ciphlen,
pub, priv,
ctbuf, ptbuf)) != ok)
break;
if(op == lspw) {
bright(1, rec.acct.ptr, rec.acct.len);
if (print || !isatty(1)) {
write(1, ": ", 2);
write(1, rec.pw.ptr, rec.pw.len);
}
write(1, "\n", 1);
} else if (op == getpw) {
if (strncmp(rec.acct.ptr,target,rec.acct.len) == 0) {
if (print || _g_term_type[1] == plain_term) {
write(1, rec.pw.ptr, rec.pw.len);
if(_g_term_type[1] > plain_term)
write(1, "\n", 1);
}
# ifdef _CLIPBOARD
if (_g_term_type[1] > plain_term) {
if (copy_pw) copy(rec.pw.ptr, rec.pw.len);
}
# endif
found = true;
break;
}
}
}
if (scanned == 0) return bad_db_empty;
if (result != fail) return result;
else if (!found) return bad_index;
return ok;
}
case createdb: { /* kpw -C [<db>] */
alert(a_debug, "creating new database");
if (clobber)
alert(a_warn, "will clobber any existing database");
/* before we open our new file, we need to generate
* our keypairs. the private key will be encrypted
* with a blake2 hash of a user-supplied passphrase
* and stored in the database after the unencrypted
* public key.
*/
byte pub [db_pubkey_len],
priv[db_privkey_len];
byte priv_enc[sz(priv)];
alert(a_debug, "generating keypair");
crypto_box_keypair(pub, priv);
/* kpw works very differently compared to
* most password managers. it uses public-key
* encryption so that new passwords can be saved
* to the db without bothering the user for the
* password. now we have our keypair, we can
* prompt the user for a secret passphrase with
* which to encrypt the private key with.
*/
alert(a_notice, "database keypair generated, encrypting");
password dbpw;
size_t pwlen;
bad e = pwread(!print, dbpw, &pwlen, _str("- new database key: "));
if (e != ok) return e;
if (!print && isatty(0)) {
password dbpw_conf;
e = pwread(!print, dbpw_conf, NULL, _str("- confirm: "));
if (e != ok) return e;
if(strcmp(dbpw,dbpw_conf) != 0)
return bad_pw_match;
}
byte salt [crypto_pwhash_SALTBYTES],
key[db_privkey_len];
byte salt_enc[crypto_box_SEALBYTES + sz(salt)];
alert(a_debug, "generating salt");
if (syscall(SYS_getrandom, salt, sz(salt), 0) == -1)
return bad_entropy;
hexdump(salt, sz(salt));
alert(a_debug, "hashing database keyphrase");
if(crypto_pwhash(key, sz(key), dbpw, pwlen, salt,
crypto_pwhash_OPSLIMIT_INTERACTIVE,
crypto_pwhash_MEMLIMIT_INTERACTIVE,
crypto_pwhash_ALG_DEFAULT) != 0) {
return bad_mem;
}
alert(a_debug, "encrypting private key");
hexdump(priv, sz(priv));
for (size_t i = 0; i < sz(key); ++i) {
priv_enc[i] = priv[i] ^ key[i];
}
alert(a_debug, "private key encrypted");
hexdump(priv_enc, sz(priv_enc));
alert(a_debug, "encrypting salt");
crypto_box_seal(salt_enc, salt, sz(salt), pub);
hexdump(salt_enc, sz(salt_enc));
/* we have everything we need. now we create the
* file, failing if it already exists so as not
* to clobber anyone's passwords.
*/
alert(a_debug, "creating new database on disk");
int db;
const int flags = O_CREAT | O_WRONLY |
(clobber ? O_TRUNC : O_EXCL);
if(param == 0)
db = dbopen(flags);
else if (param == 1)
db = open(params[0], flags, 0600);
else return bad_syntax;
if (db == -1) return bad_db_create;
/* the file is safely created; all that's left to
* do is write out the header and then we can call
* it a day.
*/
alert(a_debug, "writing public key");
hexdump(pub, sz(pub));
write(db, pub, sz(pub));
write(db, salt, sz(salt));
write(db, priv_enc, sz(priv_enc));
write(db, salt_enc, sz(salt_enc));
close(db);
alert(a_debug, "database created");
return ok;
}
}
return ok;
}
int
main (int argc, const char** argv) {
const char* colorterm = getenv("COLORTERM");
const char* term = getenv("TERM");
bool color, ansi;
if (colorterm != NULL)
color = true;
else if (term == NULL)
ansi = false, color = false;
else if (strstr(term, "color") == NULL)
ansi = true, color = false;
else color = true;
for (uint8_t i = 0; i < 3; ++i) {
if(isatty(i)) {
_g_term_type[i] = (color ? color_term :
ansi ? ansi_term : plain_term);
} else _g_term_type[i] = plain_term;
}
int e = 0;
const char* msg;
uint16_t level;
uint8_t rv;
switch (e = kpw(argc, argv)) {
kpw_emit_error_switch
}
alert(level, msg);
return rv;
}