Index: kpw.d/errtab ================================================================== --- kpw.d/errtab +++ kpw.d/errtab @@ -11,11 +11,12 @@ insane your environment is not sane index no such entry in database 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 Index: kpw.d/kpw.c ================================================================== --- kpw.d/kpw.c +++ kpw.d/kpw.c @@ -82,10 +82,15 @@ } 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; @@ -226,14 +231,87 @@ buf[len] = '\n'; return ok; } -struct entry { - pstr account; - pstr pw; -}; +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) { @@ -243,11 +321,11 @@ } } void term_bell(int tty) { write(tty,"\a",1); } - +#include 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); @@ -311,10 +389,11 @@ } 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"); @@ -346,63 +425,46 @@ } return db; } -void bytedump(uint8_t* 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(uint8_t* bytes, size_t sz) { - if(!_g_debug_msgs) return; - alert(a_debug, "printing hex dump"); - uint8_t* st = bytes; - 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("│ ")); - bytedump(st, 8); - write(2, "\n", 1); - st += 8; - } else if (i == sz - 1) { - write(2, _str("│ ")); - bytedump(st, (bytes + sz) - st); - write(2, "\n", 1); - } - } +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 -dbdecrypt(int db, uint8_t* pubkey, uint8_t* privkey) { +dbunlock(byte* priv_enc, byte* salt, byte* priv) { + const size_t priv_sz = sizeof(key_priv); + byte key [db_privkey_len]; + password dbpw; size_t pwlen; bad e = pwread(true, dbpw, &pwlen,_str("database key: ")); - - uint8_t salt [crypto_pwhash_SALTBYTES], - key [db_privkey_len], - priv_enc [db_privkey_len], - priv [db_privkey_len], - pub [db_pubkey_len]; - uint8_t salt_enc [crypto_box_SEALBYTES + sz(salt)], - salt_dec [sz(salt)]; - - alert(a_debug, "loading public key"); - ssize_t sr = read(db, pub, sz(pub)); - if (sr != sz(pub)) return bad_db_corrupt; - hexdump(pub, sz(pub)); - - alert(a_debug, "loading password salt"); - sr = read(db, salt, sz(salt)); - if (sr != sz(salt)) return bad_db_corrupt; - hexdump(salt, sz(salt)); + 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, @@ -409,55 +471,88 @@ crypto_pwhash_ALG_DEFAULT) != 0) { return bad_mem; } hexdump(key, sz(key)); - alert(a_debug, "loading encrypted private key"); - read(db, priv_enc, sz(priv_enc)); - hexdump(priv_enc, sz(priv_enc)); - - alert(a_debug, "decrypting private 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(priv)); + hexdump(priv, sz(key)); - alert(a_debug, "loading verification hash"); - read(db, salt_enc, sz(salt_enc)); - hexdump(salt_enc, sz(salt_enc)); + 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_enc), pub, priv); + 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)) != 0) return bad_db_corrupt; + 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; } -#include -void bright(int fd, const char* str, size_t len) { +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, - chpw,keyin,logout,createdb,rekeydb} + enum {usage,getpw,addpw,delpw,lspw,genpw,regen, + chpw,keyin,logout,createdb,rekeydb,help} op = getpw; const char* params[3]; uint8_t param = 0; @@ -492,35 +587,32 @@ 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 (op == getpw && param == 0) return emit_usage(NULL); if (sodium_init() < 0) return bad_lib_sodium_init; switch(op) { - case genpw: /* kpw -g[lmu] [] */ - case addpw: { /* kpw -a [] */ + case genpw: + case addpw: { + if (param == 0) return emit_usage( + op == addpw ? " -a[p] []\n" : + /* genpw */" -g[lmup] []\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"); - uint8_t key [db_pubkey_len]; + 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), @@ -565,112 +657,178 @@ } pwlen = len; } if (copy_pw) copy(pw, pwlen); alert(a_debug, "encoding database entry"); - uint8_t acctlen = strlen(acct); - uint8_t plaintext[1 + acctlen + - 1 + pwlen]; + 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, pw, pwlen); hexdump(plaintext, sz(plaintext)); alert(a_debug, "enciphering database entry"); - uint8_t ciphertext[sz(plaintext) + crypto_box_SEALBYTES]; + 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"); - uint8_t ciphertext_len = sz(ciphertext); + db_sz ciphertext_len = sz(ciphertext); write(db, &ciphertext_len, 1); write(db, &ciphertext, ciphertext_len); close(db); break; } case delpw:{ /* kpw -d */ + if (param == 0) return emit_usage(" -d \n"); + if (param != 1) return bad_syntax; + const char* target = params[0]; + bad e; + + 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)]; + + 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; do not copy it */ + alert(a_debug, "found record to delete"); + found = true; + } else { + /* not our target, copy it into the buffer */ + 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); + break; } case getpw: /* kpw */ case lspw: { /* kpw -t[p] [] */ - alert(a_debug, "opening database for reading"); - int db = dbopen(O_RDONLY); - if (db == -1) return bad_db_load; - const char* target; if (param == 1) target = params[0]; else if (param == 0) target = NULL; else return bad_syntax; - uint8_t priv [db_privkey_len], - pub [db_pubkey_len]; + 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 now be positioned - * on first record */ + /* cursor should be positioned on first + * record if we've made it this far */ alert(a_debug, "beginning to scan records"); - read_rec: { - uint8_t acctlen; - if (read(db, &acctlen, 1) != 1) - goto done_reading; - uint8_t ciphertext[acctlen]; - if (read(db, &ciphertext, acctlen) != acctlen) - return bad_db_corrupt; - alert(a_debug, "scanned record"); - hexdump(ciphertext, sz(ciphertext)); - - uint8_t plaintext[sz(ciphertext) - crypto_box_SEALBYTES]; - if(crypto_box_seal_open(plaintext, ciphertext, sz(ciphertext), pub, priv) != 0) - return bad_db_corrupt; - - alert(a_debug, "record deciphered"); - hexdump(plaintext, sz(plaintext)); - - uint8_t record_name_len = plaintext[0], - record_pw_len = plaintext[record_name_len + 1]; - - pstr record_name = {record_name_len, plaintext + 1}, - record_pw = {record_pw_len, - plaintext + record_name_len + 2}; + 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, record_name.ptr, record_name.len); + bright(1, rec.acct.ptr, rec.acct.len); if (print || !isatty(1)) { write(1, ": ", 2); - write(1, record_pw.ptr, record_pw.len); + write(1, rec.pw.ptr, rec.pw.len); } write(1, "\n", 1); } else if (op == getpw) { - if (strncmp(record_name.ptr,target,record_name.len) == 0) { + if (strncmp(rec.acct.ptr,target,rec.acct.len) == 0) { if (print || _g_term_type[1] == plain_term) { - write(1, record_pw.ptr, record_pw.len); + write(1, rec.pw.ptr, rec.pw.len); if(_g_term_type[1] > plain_term) write(1, "\n", 1); } if (_g_term_type[1] > plain_term) { - if (copy_pw) copy(record_pw.ptr, record_pw.len); + if (copy_pw) copy(rec.pw.ptr, rec.pw.len); } - goto done_reading; + found = true; + break; } } - - goto read_rec; } - return bad_index; - - done_reading: break; + if (scanned == 0) return bad_db_empty; + if (result != fail) return result; + else if (!found) return bad_index; + + break; } case createdb: { /* kpw -C [] */ alert(a_debug, "creating new database"); if (clobber) @@ -679,13 +837,13 @@ * 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 [db_pubkey_len], - priv[db_privkey_len]; - uint8_t priv_enc[sz(priv)]; + 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 @@ -709,13 +867,13 @@ 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)]; + 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)); Index: kpw.d/optab ================================================================== --- kpw.d/optab +++ kpw.d/optab @@ -1,12 +1,14 @@ C create op = createdb create a new password database R rekey op = rekeydb change database passphrase +! clobber clobber = true disable safety checks 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 +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 = stupid circumvent dumb pw restrictions k key op = keyin save database key in session memory @@ -13,6 +15,6 @@ o logout op = logout delete db key from session memory 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 +h help op = help display help text