Index: bgrd.c ================================================================== --- bgrd.c +++ bgrd.c @@ -90,11 +90,11 @@ * sent the X id to stdout with a call to write(2), the * correct thing to do. they could have thrown in a call * to setvbuf(3) to explicitly pick a buffering strategy * compatible with their usecase, the sensibly wrong * thing to do. they could have explicitly flushed stdout - * after printf(3)'ing to it, the dumb and error-pront + * after printf(3)'ing to it, the dumb and error-prone * thing to do. * * instead, they did *nothing.* * * so if you run `surf -x` from a terminal, great! DELETED kpw.d/db.md Index: kpw.d/db.md ================================================================== --- kpw.d/db.md +++ kpw.d/db.md @@ -1,18 +0,0 @@ -# kpw db format - -kpw uses a simple binary database format. it consists of a number of values of constant size, followed by a series of encrypted records. - - 1. public key - 2. password salt - 3. encrypt(password, private key) - 4. encrypt(private key, password salt) [for pw verification] - 5. record * - -each record takes the form of - - 1. account name length (1 byte) - 2. account name - 3. password length (1 byte) - 4. password - -records are added simply by encrypting them with the public key and appending them to the end of the file. thus, adding a new password does not require the decryption password. DELETED kpw.d/errtab Index: kpw.d/errtab ================================================================== --- kpw.d/errtab +++ kpw.d/errtab @@ -1,26 +0,0 @@ -ok completed successfully debug 0 -fail unknown error -assert bug detected; please report this -mem out of memory -num not a number -user kpw started as wrong user -option unrecognized option passed -syntax invalid invocation syntax -entropy could not acquire entropy -copy could not copy password -pipe could not create pipe -insane your environment is not sane -index no such entry in database -shm shared memory failure -shm_exist key already in memory notice -no_shm no key in memory notice -db_create database could not be created -db_load database could not be read -db_corrupt database corrupt -db_empty no records in database notice -cancel user canceled operation notice -pw invalid password -pw_match passwords do not match -usage usage displayed to user debug 64 -lib unspecified library error fatal 128 -lib_sodium_init could not initialize libsodium DELETED kpw.d/errtab.awk Index: kpw.d/errtab.awk ================================================================== --- kpw.d/errtab.awk +++ kpw.d/errtab.awk @@ -1,53 +0,0 @@ -BEGIN { - if (emit == "enum") { - print "typedef enum bad {" - } else if (emit == "msg") { - print "#define kpw_emit_error_switch \\" - code = 0 - } -} - -function name(n) { - if (NR > 2) { - return "bad_" n - } else { - return n - } -} - -emit == "enum" { - print "\t" name($1) "," -} - -emit == "msg" { - if (NF >= 4) { - retval = $4 - code = retval + 1 - } else { - retval = code++ - } - - if (NF >= 3) { - level = "a_" $3 - if (level == "a_fatal") { - level = level " | " retval - } - } else { - level = "a_fatal | " retval - } - - print "\t" \ - "case " name($1)":" \ - "level=" level ";" \ - "msg=\""$2"\";" \ - "rv=" retval";" \ - "break; \\" -} - -END { - if (emit == "enum") { - print "} bad;" - } else if (emit == "msg") { - print "" - } -} DELETED kpw.d/kpw.c Index: kpw.d/kpw.c ================================================================== --- kpw.d/kpw.c +++ kpw.d/kpw.c @@ -1,1127 +0,0 @@ -/* [ʞ] kpw.c - password manager - * ↳ derivative of mkpw.c - * ~ lexi hale - * © 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -#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 -# include -# include -#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] []\n" : - /* genpw */" -g[lmusp] []\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 */ - if (param == 0) return emit_usage - (op==delpw ? " -d \n" : - op==regen ? " -r[lmusp] []\n" : - /* op==chpw */ " -c []\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 */ - case lspw: { /* kpw -t[p] [] */ - 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 [] */ - 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; -} DELETED kpw.d/makefile Index: kpw.d/makefile ================================================================== --- kpw.d/makefile +++ kpw.d/makefile @@ -1,25 +0,0 @@ -include ../makerules - -cdeps = compose.c iaia.c tbl.c -cpaths = $(cdeps:%=$(root)/clib/%) - -$(root)/kpw: kpw.c opt.inc err.inc $(cpaths) - $(cc) -I$(root) $< -lsodium -o $@ $(flags) $(cc-post) - -tab = cat $< | awk -v emit=$1 -F'\t+' -f $<.awk >> $@ - -opt.inc: optab optab.awk - :>$@ - $(call tab,cond) - $(call tab,enum) - $(call tab,argtbl) - $(call tab,olong) - $(call tab,oshort) - $(call tab,usage) - -err.inc: errtab errtab.awk - :>$@ - $(call tab,enum) - $(call tab,msg) - -.PHONY: dbg DELETED kpw.d/optab Index: kpw.d/optab ================================================================== --- kpw.d/optab +++ kpw.d/optab @@ -1,21 +0,0 @@ -C create op = createdb create a new password database -R rekey op = rekeydb change database passphrase -M merge op = mergedb merge in another database -! clobber clobber = true disable safety checks -t list op = lspw list all accounts (with passwords if -p is set) -a add op = addpw add password to the database -g gen op = genpw add account with a random password -d del op = delpw delete password from the database -c chg op = chpw change password in the database -r regen op = regen generate new password for existing account -l lower mode = lower generate lowercase password -m mix mode = mix generate mix-case password -u upper mode = upper generate uppercase password -s stupid-mode mode = stupid circumvent dumb pw restrictions -k install-key op = keyin install database key in session memory _SAVEKEY -o logout op = logout delete db key from session memory _SAVEKEY -n no-copy copy_pw = false print password instead of copying to clipboard _CLIPBOARD -p print-pw print = true display passwords onscreen -q quiet _g_alert_quiet = true hide non-fatal reports -v verbose _g_debug_msgs = true display debug reports -h help op = help display help text DELETED kpw.d/optab.awk Index: kpw.d/optab.awk ================================================================== --- kpw.d/optab.awk +++ kpw.d/optab.awk @@ -1,59 +0,0 @@ -BEGIN { - end = "" - if (emit == "enum") { - print "enum args {" - } else if (emit == "argtbl") { - print "struct tblrow argtbl[] = {" - } else if (emit == "olong") { - print "#define kpw_emit_long_option_switch \\" - end = "\\" - } else if (emit == "oshort") { - print "#define kpw_emit_short_option_switch \\" - end = "\\" - } else if (emit == "usage") { - print "const char kpw_usage[] =" - } - globalc = 0 -} -function cify(str) { - gsub(/[- ]/, "_", str); - return str; -} -function say(line) { - if (NF == 5) { - print "\tkpw_only_" $5 "(" line ") " end - globals[globalc] = $5 - ++ globalc - } else { - print "\t" line " " end - } -} - -{ optstr = optstr $1 } -emit == "enum" { say("arg_" cify($2) ",") } -emit == "argtbl" { say("{ arg_" cify($2)", \"" $2 "\" },") } -emit == "olong" { say("case arg_" cify($2) ": " $3 "; break;") } -emit == "oshort" { say("case '" $1 "': " $3 "; break;") } -emit == "usage" { say("\"\\t-"$1", --"$2": "$4"\\n\"") } - -emit == "cond" { - if (NF == 5 && !($5 in condlist)) { - condlist[$5] = 1 - print "#ifdef " $5 - print "# define kpw_only_" $5 "(x...) x" - print "#else" - print "# define kpw_only_" $5 "(...)" - print "#endif" - } -} - -END { - if (emit == "olong" || emit == "oshort") { - print "" - } else if (emit == "usage") { - print ";" - print "const char kpw_optstr[] = \" [-" optstr "] [args]\\n\";" - } else if (emit != "cond") { - print "};" - } -} ADDED kpw/db.md Index: kpw/db.md ================================================================== --- kpw/db.md +++ kpw/db.md @@ -0,0 +1,18 @@ +# kpw db format + +kpw uses a simple binary database format. it consists of a number of values of constant size, followed by a series of encrypted records. + + 1. public key + 2. password salt + 3. encrypt(password, private key) + 4. encrypt(private key, password salt) [for pw verification] + 5. record * + +each record takes the form of + + 1. account name length (1 byte) + 2. account name + 3. password length (1 byte) + 4. password + +records are added simply by encrypting them with the public key and appending them to the end of the file. thus, adding a new password does not require the decryption password. ADDED kpw/errtab Index: kpw/errtab ================================================================== --- kpw/errtab +++ kpw/errtab @@ -0,0 +1,26 @@ +ok completed successfully debug 0 +fail unknown error +assert bug detected; please report this +mem out of memory +num not a number +user kpw started as wrong user +option unrecognized option passed +syntax invalid invocation syntax +entropy could not acquire entropy +copy could not copy password +pipe could not create pipe +insane your environment is not sane +index no such entry in database +shm shared memory failure +shm_exist key already in memory notice +no_shm no key in memory notice +db_create database could not be created +db_load database could not be read +db_corrupt database corrupt +db_empty no records in database notice +cancel user canceled operation notice +pw invalid password +pw_match passwords do not match +usage usage displayed to user debug 64 +lib unspecified library error fatal 128 +lib_sodium_init could not initialize libsodium ADDED kpw/errtab.awk Index: kpw/errtab.awk ================================================================== --- kpw/errtab.awk +++ kpw/errtab.awk @@ -0,0 +1,53 @@ +BEGIN { + if (emit == "enum") { + print "typedef enum bad {" + } else if (emit == "msg") { + print "#define kpw_emit_error_switch \\" + code = 0 + } +} + +function name(n) { + if (NR > 2) { + return "bad_" n + } else { + return n + } +} + +emit == "enum" { + print "\t" name($1) "," +} + +emit == "msg" { + if (NF >= 4) { + retval = $4 + code = retval + 1 + } else { + retval = code++ + } + + if (NF >= 3) { + level = "a_" $3 + if (level == "a_fatal") { + level = level " | " retval + } + } else { + level = "a_fatal | " retval + } + + print "\t" \ + "case " name($1)":" \ + "level=" level ";" \ + "msg=\""$2"\";" \ + "rv=" retval";" \ + "break; \\" +} + +END { + if (emit == "enum") { + print "} bad;" + } else if (emit == "msg") { + print "" + } +} ADDED kpw/kpw.c Index: kpw/kpw.c ================================================================== --- kpw/kpw.c +++ kpw/kpw.c @@ -0,0 +1,1127 @@ +/* [ʞ] kpw.c - password manager + * ↳ derivative of mkpw.c + * ~ lexi hale + * © 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#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 +# include +# include +#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] []\n" : + /* genpw */" -g[lmusp] []\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 */ + if (param == 0) return emit_usage + (op==delpw ? " -d \n" : + op==regen ? " -r[lmusp] []\n" : + /* op==chpw */ " -c []\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 */ + case lspw: { /* kpw -t[p] [] */ + 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 [] */ + 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; +} ADDED kpw/makefile Index: kpw/makefile ================================================================== --- kpw/makefile +++ kpw/makefile @@ -0,0 +1,25 @@ +include ../makerules + +cdeps = compose.c iaia.c tbl.c +cpaths = $(cdeps:%=$(root)/clib/%) + +$(root)/kpw.bin: kpw.c opt.inc err.inc $(cpaths) + $(cc) -I$(root) $< -lsodium -o $@ $(flags) $(cc-post) + +tab = cat $< | awk -v emit=$1 -F'\t+' -f $<.awk >> $@ + +opt.inc: optab optab.awk + :>$@ + $(call tab,cond) + $(call tab,enum) + $(call tab,argtbl) + $(call tab,olong) + $(call tab,oshort) + $(call tab,usage) + +err.inc: errtab errtab.awk + :>$@ + $(call tab,enum) + $(call tab,msg) + +.PHONY: dbg ADDED kpw/optab Index: kpw/optab ================================================================== --- kpw/optab +++ kpw/optab @@ -0,0 +1,21 @@ +C create op = createdb create a new password database +R rekey op = rekeydb change database passphrase +M merge op = mergedb merge in another database +! clobber clobber = true disable safety checks +t list op = lspw list all accounts (with passwords if -p is set) +a add op = addpw add password to the database +g gen op = genpw add account with a random password +d del op = delpw delete password from the database +c chg op = chpw change password in the database +r regen op = regen generate new password for existing account +l lower mode = lower generate lowercase password +m mix mode = mix generate mix-case password +u upper mode = upper generate uppercase password +s stupid-mode mode = stupid circumvent dumb pw restrictions +k install-key op = keyin install database key in session memory _SAVEKEY +o logout op = logout delete db key from session memory _SAVEKEY +n no-copy copy_pw = false print password instead of copying to clipboard _CLIPBOARD +p print-pw print = true display passwords onscreen +q quiet _g_alert_quiet = true hide non-fatal reports +v verbose _g_debug_msgs = true display debug reports +h help op = help display help text ADDED kpw/optab.awk Index: kpw/optab.awk ================================================================== --- kpw/optab.awk +++ kpw/optab.awk @@ -0,0 +1,59 @@ +BEGIN { + end = "" + if (emit == "enum") { + print "enum args {" + } else if (emit == "argtbl") { + print "struct tblrow argtbl[] = {" + } else if (emit == "olong") { + print "#define kpw_emit_long_option_switch \\" + end = "\\" + } else if (emit == "oshort") { + print "#define kpw_emit_short_option_switch \\" + end = "\\" + } else if (emit == "usage") { + print "const char kpw_usage[] =" + } + globalc = 0 +} +function cify(str) { + gsub(/[- ]/, "_", str); + return str; +} +function say(line) { + if (NF == 5) { + print "\tkpw_only_" $5 "(" line ") " end + globals[globalc] = $5 + ++ globalc + } else { + print "\t" line " " end + } +} + +{ optstr = optstr $1 } +emit == "enum" { say("arg_" cify($2) ",") } +emit == "argtbl" { say("{ arg_" cify($2)", \"" $2 "\" },") } +emit == "olong" { say("case arg_" cify($2) ": " $3 "; break;") } +emit == "oshort" { say("case '" $1 "': " $3 "; break;") } +emit == "usage" { say("\"\\t-"$1", --"$2": "$4"\\n\"") } + +emit == "cond" { + if (NF == 5 && !($5 in condlist)) { + condlist[$5] = 1 + print "#ifdef " $5 + print "# define kpw_only_" $5 "(x...) x" + print "#else" + print "# define kpw_only_" $5 "(...)" + print "#endif" + } +} + +END { + if (emit == "olong" || emit == "oshort") { + print "" + } else if (emit == "usage") { + print ";" + print "const char kpw_optstr[] = \" [-" optstr "] [args]\\n\";" + } else if (emit != "cond") { + print "};" + } +} Index: makefile ================================================================== --- makefile +++ makefile @@ -27,9 +27,9 @@ $(cc) $< -lX11 -o$@ $(cc-post) xpriv: xpriv.c $(cc) $< -lrt -lutil -lX11 -o $@ $(cc-post) -kpw: kpw.d/makefile - $(MAKE) root=$(realpath .) flags=$(kpw-flags) -C kpw.d $(realpath .)/$@ +kpw.bin: kpw/makefile + $(MAKE) root=$(realpath .) flags=$(kpw-flags) -C kpw $(realpath .)/$@ .PHONY: kpw