/* [ʞ] 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. * ? 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(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; }