Index: clib/compose.c ================================================================== --- clib/compose.c +++ clib/compose.c @@ -20,11 +20,11 @@ typedef struct pstr { size_t len; union { const char* ptr; char* mutptr; }; } pstr; -#define _p(str) { sizeof str - 1, str } +#define _p(str) { sizeof (str) - 1, (str) } typedef struct safestr { union { const char* ptr; char* mutptr; @@ -57,10 +57,11 @@ if (lst[i].ptr == NULL) continue; lst[i].len = strlen(lst[i].ptr); } len += lst[i].len; } + return len; }) char* pstrcoll(pstr* lst, size_t ct, char* ptr) fn({ for (size_t i = 0; i < ct; ++i) { if (lst[i].len == 0) continue; ADDED kpw.d/db.md Index: kpw.d/db.md ================================================================== --- kpw.d/db.md +++ kpw.d/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 (4 bytes) + 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.d/errtab Index: kpw.d/errtab ================================================================== --- kpw.d/errtab +++ kpw.d/errtab @@ -0,0 +1,16 @@ +ok completed successfully debug 0 +fail unknown error +mem out of memory +user kpw started as wrong user +option unrecognized option passed +syntax invalid invocation syntax +entropy could not acquire entropy +copy could not copy password +insane your environment is not sane +db_create database could not be created +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 ADDED kpw.d/errtab.awk Index: kpw.d/errtab.awk ================================================================== --- kpw.d/errtab.awk +++ kpw.d/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.d/kpw.c Index: kpw.d/kpw.c ================================================================== --- kpw.d/kpw.c +++ kpw.d/kpw.c @@ -0,0 +1,527 @@ +/* [ʞ] 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; +} ADDED kpw.d/makefile Index: kpw.d/makefile ================================================================== --- kpw.d/makefile +++ kpw.d/makefile @@ -0,0 +1,25 @@ +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 ADDED kpw.d/optab Index: kpw.d/optab ================================================================== --- kpw.d/optab +++ kpw.d/optab @@ -0,0 +1,17 @@ +C create op = createdb create a new password database +R rekey op = rekeydb change database passphrase and key +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 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 +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 ADDED kpw.d/optab.awk Index: kpw.d/optab.awk ================================================================== --- kpw.d/optab.awk +++ kpw.d/optab.awk @@ -0,0 +1,56 @@ +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 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_" $2 ",") } +emit == "argtbl" { say("{ arg_" $2", \"" $2 "\" },") } +emit == "olong" { say("case arg_" $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 @@ -8,11 +8,11 @@ all: ctl conv xutil gen xutil: xpriv safekill # X11 tools -gen: mkpw rosshil +gen: mkpw kpw rosshil # procedural generators conv: ord # converters ctl: nkvd.so bgrd # manipulating disobedient software @@ -26,5 +26,10 @@ safekill: safekill.c $(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 .)/$@ + +.PHONY: kpw Index: makerules ================================================================== --- makerules +++ makerules @@ -31,5 +31,8 @@ %.proj: %/makefile cd $* && make $* %.proj: %/make.sh cd $* && ./make.sh + +dep/%: + make -C dep $* Index: mkpw.c ================================================================== --- mkpw.c +++ mkpw.c @@ -9,10 +9,15 @@ * to STDIN of the binary that succeeds. * ? generates passwords * → mkpw 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 @@ -45,10 +50,12 @@ ok = 0, fail = 1, bad_user, bad_option, bad_syntax, + bad_entropy, + bad_copy, bad_usage = 64, /* fleabsd idiom */ } bad; typedef _Bool bool; typedef unsigned long long iaia_word_type; @@ -197,11 +204,12 @@ /* *buf = 0; */ unsigned char noise[len]; for (size_t i = 0; i