/* [ʞ] 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. * ! 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. */ #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))) #ifdef _CLIPBOARD # include # include # include #else # define copy(str,len) #endif enum /* constants */ { null = 0, true = 1, false = 0 }; #include "err.inc" enum /* db format constants */ { db_pubkey_len = crypto_box_PUBLICKEYBYTES, db_privkey_len = crypto_box_SECRETKEYBYTES, kpw_db_pw_max = 64, }; 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" #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; #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 }; bad copy(const char* str, size_t len) { 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 63; 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 }; 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; } struct entry { pstr account; pstr pw; }; enum term_clear {term_clear_line, term_clear_screen}; enum alert {a_notice, a_warn, a_debug, a_fatal = 1 << 8}; pstr alert_msg[] = { _p("(notice)"), _p("(warn)"), _p("(debug)"), _p("(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 (isatty(2)) { char msgcode[] = "\x1b[1;30m"; char* color = msgcode+5; *color = '0' + (4 - idx); write(2,msgcode, sz(msgcode)); } write(2,alert_msg[idx].ptr,alert_msg[idx].len); if (isatty(2)) 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)); } void term_clear(enum term_clear behavior) { switch(behavior) { case term_clear_line: write(1,"\r\x1b[2K",5); break; case term_clear_screen: write(1,"\r\x1b[3J",5); break; } } void term_bell() { write(1,"\a",1); } typedef char password[kpw_db_pw_max + 1]; bad pwread(char* dest, size_t* out_len, const char* prompt, const size_t plen) { if (isatty(0)) { 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(1, &initial); struct termios nt = initial; nt.c_lflag &= (~ECHO & ~ICANON); tcsetattr(1, TCSANOW, &nt); } *dest = 0; char* p = dest; do { term_clear(term_clear_line); write(1, "\x1b[1m", 4); write(1, prompt, plen); write(1, "\x1b[21m", 5); for(size_t i = 0; i < p - dest; ++i) write(1, "*", 1); 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(term_clear_line); return bad_cancel; case '\b': case '\x7f': if (p > dest) *p--=0; else term_bell(); break; default: if (p - dest != 64) *p++=c; else term_bell(); } } else { /* either EOF or an error - either way, * we're finished here */ break; } } while(1); end_read_loop: term_clear(term_clear_line); *p = 0; if (out_len!=NULL) *out_len = p - dest; /* return the terminal to normal */ tcsetattr(1, TCSANOW, &initial); } else { alert(a_warn, "reading pw from standard input"); ssize_t ct = read(0, dest, kpw_db_pw_max); dest[ct] = 0; } } #include 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 { printf("path is %s", dbpath); db = open(dbpath, flags, 0600); } return db; } int kpw(int argc, const char** argv) { if (argc == 0) return -1; if (argc == 1) { size_t namelen = strlen(argv[0]); say("\x1b[1musage:\x1b[m "); write(2, argv[0], namelen); write(2, kpw_optstr, sz(kpw_optstr)); write(2, kpw_usage, sz(kpw_usage)); return bad_usage; } enum genmode mode = lower; enum {usage,getpw,addpw,delpw,genpw, chpw,keyin,logout,createdb,rekeydb} op = getpw; const char* params[3]; uint8_t param = 0; bool print = false, clobber = false; # ifdef _CLIPBOARD bool copy_pw = true; # endif for (const char** arg = argv + 1; *arg != null; ++arg) { if ((*arg)[0] == '-') { if ((*arg)[1] == '-') { /* long opt */ 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 (sodium_init() < 0) return bad_lib_sodium_init; switch(op) { case getpw:{ /* kpw */ break; } case addpw:{ /* kpw -a [] */ break; } case delpw:{ /* kpw -d */ break; } case genpw:{ /* kpw -g[lmu] [] */ break; } 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. */ uint8_t pub [crypto_box_PUBLICKEYBYTES], priv[crypto_box_SECRETKEYBYTES]; uint8_t 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; # define _str(s) (s),sizeof(s) bad e = pwread(dbpw, &pwlen, _str("database passphrase: ")); if (e != ok) return e; if (isatty(0)) { password dbpw_conf; e = pwread(dbpw_conf, NULL, _str("confirm: ")); # undef _str if (e != ok) return e; if(strcmp(dbpw,dbpw_conf) != 0) return bad_pw_match; } uint8_t salt[crypto_pwhash_SALTBYTES], key[db_privkey_len]; uint8_t salt_enc[crypto_box_SEALBYTES + sz(salt)]; if (syscall(SYS_getrandom, salt, sz(salt), 0) == -1) return bad_entropy; 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; } for (size_t i = 0; i < sz(key); ++i) { priv_enc[i] = priv[i] ^ key[i]; } crypto_box_seal(salt_enc, salt, sz(salt), priv); /* we have everything we need. now we create the * file, failing if it already exists so as not * to clobber anyone's passwords. */ int db; const int flags = O_CREAT | (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; 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); for (int i = 0; i < sz(key); ++i) { printf("%2x ", key[i]); if (!((i+1)%8)) printf("\n"); } break; } } # ifdef _CLIPBOARD if (geteuid() == 0 && copy_pw) { /* 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"; say("\x1b[1;33mwarning:\x1b[m you are running \x1b[4m"); size_t namelen = strlen(argv[0]); write(2,argv[0],namelen); say("\x1b[24m as \x1b[1mroot\x1b[m! dropping to \x1b[1m"); write(2,realuser,strlen(realuser)); setenv("USER", realuser, true); say("\x1b[m to prevent malicious behavior\n"); struct passwd* nobody = getpwnam(realuser); if (nobody == null) { say("\x1b[1;31mfatal:\x1b[m 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); } } # endif /* char buf[len+1]; */ /* *buf = 0; */ /* mkpw(mode,buf,len); */ /* write(1, buf, len + 1); */ # ifdef _CLIPBOARD if (copy_pw) { copy(buf,len); } # endif return ok; } int main (int argc, const char** argv) { 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; }