Index: kpw.d/errtab ================================================================== --- kpw.d/errtab +++ kpw.d/errtab @@ -1,16 +1,20 @@ ok completed successfully debug 0 fail unknown error 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 db_create database could not be created +db_load database could not be read +db_corrupt database corrupt 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 fatal +lib_sodium_init could not initialize libsodium Index: kpw.d/kpw.c ================================================================== --- kpw.d/kpw.c +++ kpw.d/kpw.c @@ -15,10 +15,13 @@ * ! 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 @@ -31,10 +34,11 @@ #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 @@ -50,10 +54,11 @@ 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; @@ -84,10 +89,11 @@ #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; #ifdef _CLIPBOARD char* const* cbd_cmds[] = { /* NOTE: these commands must be specified in order of * most- to least-specific. more than one utility may @@ -97,17 +103,79 @@ (char* const[]){"xsel", "-bi", null}, /* TODO: allow command to be specified by env var */ null }; -bad +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)); +} + +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 63; + 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[]){ @@ -127,11 +195,11 @@ } #endif enum genmode { upper, mix, lower, stupid }; -bad +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)))); @@ -166,44 +234,10 @@ }; 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; } @@ -211,11 +245,11 @@ 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) { +bad pwread(bool obscure, 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. */ @@ -228,16 +262,17 @@ *dest = 0; char* p = dest; do { term_clear(term_clear_line); - write(1, "\x1b[1m", 4); + if (_g_term_type[1] >= ansi_term) write(1, "\x1b[1m", 4); write(1, prompt, plen); - write(1, "\x1b[21m", 5); + if (_g_term_type[1] >= ansi_term) write(1, "\x1b[21m", 5); - for(size_t i = 0; i < p - dest; ++i) + if (obscure) for(size_t i = 0; i < p - dest; ++i) write(1, "*", 1); + else write(1, dest, p-dest); char c; if (read(0, &c, 1) == 1) { switch (c) { case '\n': case '\r': @@ -309,37 +344,35 @@ 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; - } + if (argc == 0) return bad_insane; + _g_binary_name = argv[0]; enum genmode mode = lower; - enum {usage,getpw,addpw,delpw,genpw, + enum {usage,getpw,addpw,delpw,lspw,genpw, chpw,keyin,logout,createdb,rekeydb} op = getpw; const char* params[3]; uint8_t param = 0; bool print = false, - clobber = false; + clobber = false, + no_more_opts = false; # ifdef _CLIPBOARD bool copy_pw = true; # endif for (const char** arg = argv + 1; *arg != null; ++arg) { - if ((*arg)[0] == '-') { + 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; @@ -355,28 +388,96 @@ } else { if (param > sz(params)) return bad_syntax; params[param++] = *arg; } } + + if (op == getpw && param == 0) { + 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; + } if (sodium_init() < 0) return bad_lib_sodium_init; switch(op) { case getpw:{ /* kpw */ break; } - case addpw:{ /* kpw -a [] */ + case genpw: /* kpw -g[lmu] [] */ + case addpw: { /* kpw -a [] */ + 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"); + uint8_t 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)) + 1]; + if (tty_in) pstrcoll(prompt_l, sz(prompt_l), prompt); + + bad e = pwread(!print, pw, &pwlen, + prompt, sz(prompt) - 1); + 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; + + } 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"); + mkpw(mode, pw, len); + if (print || !tty_out) { + write(1, pw, len); + if(tty_out) write(1, "\n", 1); + } + pwlen = len; + } + if (copy_pw) copy(pw, pwlen); + alert(a_debug, "enciphering password"); + break; } case delpw:{ /* kpw -d */ break; } - case genpw:{ /* kpw -g[lmu] [] */ + case lspw: { /* kpw -t[p] [] */ break; } case createdb: { /* kpw -C [] */ alert(a_debug, "creating new database"); @@ -386,12 +487,12 @@ * 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 pub [db_pubkey_len], + priv[db_privkey_len]; uint8_t priv_enc[sz(priv)]; alert(a_debug, "generating keypair"); crypto_box_keypair(pub, priv); @@ -402,21 +503,18 @@ * 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: ")); + bad e = pwread(!print, dbpw, &pwlen, _str("- database passphrase: ")); if (e != ok) return e; - if (isatty(0)) { + if (!print && isatty(0)) { password dbpw_conf; - e = pwread(dbpw_conf, NULL, _str("confirm: ")); -# undef _str + e = pwread(!print, dbpw_conf, NULL, _str("- confirm: ")); if (e != ok) return e; if(strcmp(dbpw,dbpw_conf) != 0) return bad_pw_match; } @@ -444,78 +542,73 @@ /* 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); + 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. + */ 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"); - } + alert(a_debug, "database created"); 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); + /* copy(buf,len); */ } # endif 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)) { Index: kpw.d/optab ================================================================== --- kpw.d/optab +++ kpw.d/optab @@ -1,17 +1,18 @@ C create op = createdb create a new password database -R rekey op = rekeydb change database passphrase and key +R rekey op = rekeydb change database passphrase 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 +t list op = lspw list all accounts (with passwords if -p is set) c chpw op = chpw change password in the database 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 = stupid circumvent dumb pw restrictions k key op = keyin save database key in session memory o logout op = logout delete db key from session memory -n nocopy copy_pw = false don't copy generated pw to clipboard _CLIPBOARD -p print print = true display generated password +n no-copy copy_pw = false print password instead of copying to clipboard _CLIPBOARD +p print print = true display passwords onscreen q quiet _g_alert_quiet = true hide non-fatal reports v verbose _g_debug_msgs = true display debug reports ! clobber clobber = true disable safety checks Index: kpw.d/optab.awk ================================================================== --- kpw.d/optab.awk +++ kpw.d/optab.awk @@ -13,11 +13,14 @@ } 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 @@ -25,13 +28,13 @@ print "\t" line " " end } } { optstr = optstr $1 } -emit == "enum" { say("arg_" $2 ",") } -emit == "argtbl" { say("{ arg_" $2", \"" $2 "\" },") } -emit == "olong" { say("case arg_" $2 ": " $3 "; break;") } +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)) { Index: mkpw.c ================================================================== --- mkpw.c +++ mkpw.c @@ -186,10 +186,12 @@ return bad_user; } else { setenv("HOME", nobody -> pw_dir, true); setenv("SHELL", "/dev/null", true); setuid(nobody -> pw_uid); + if (geteuid() == 0) + say("\x1b[1;31mnice try:\x1b[m i don't fucking think so, you sneaky bastard"); } } # endif Index: readme.md ================================================================== --- readme.md +++ readme.md @@ -1,5 +1,6 @@ # util various odds and ends. all code in this repository is licensed under the AGPL unless otherwise noted. * **safekill.c**: utility to help keep from accidentally killing important windows; compile with `cc -Ofast safekill.c -lX11 -lc -osafekill` * **bgrd.c**: it’s… a long story. just read the header. +* **kpw**: an extremely simple, lightweight, secure password manager for POSIX OSes written in C. depends on libsodium for crypto primitives. compile with `make kpw`.