util  Artifact [9c1b0583ee]

Artifact 9c1b0583ee4002076f06b7939115eac8c1d32577c62336510728131cd7728fac:


/* [ʞ] kpw.c - password manager
 *  ↳ derivative of mkpw.c
 *  ~ lexi hale <lexi@hale.su>
 *  © 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.
 *
 *  TODO prevent pw reads from going off the edge of
 *       the screen and fucking up all the shit
 */

#include <unistd.h>
#include <sys/random.h>
#include <sys/syscall.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sodium.h>
#include <termios.h>


#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 <sys/types.h>
#	include <pwd.h>
#	include <stdlib.h>
#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,
	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;
#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 { 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 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);
}
#include<stdio.h>
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];

	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));

	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,
	      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) {
 
		case genpw:   
		case addpw: {
			if (param == 0) return emit_usage(
					op == addpw ? " -a[p] <account> [<pw>]\n"       :
					   /* genpw */" -g[lmup] <account> [<pw len>]\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)) + 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");
				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;
			}
			if (copy_pw) copy(pw, pwlen);
			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, 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 delpw:{ /* kpw -d <acct> */
			if (param == 0) return emit_usage(" -d <account>\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 <acct> */
		case lspw: { /* kpw -t[p] [<prefix>] */
			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);
						}

						if (_g_term_type[1] > plain_term) {
							if (copy_pw) copy(rec.pw.ptr, rec.pw.len);
						}
						found = true;
						break;
					}
				}
			}
			if (scanned == 0) return bad_db_empty;
			if (result != fail) return result;
			else if (!found) return bad_index;
			
			break;
		}

		case createdb: { /* kpw -C [<db>] */
			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");
			break;
		}
	}

#	ifdef _CLIPBOARD
		if (copy_pw) {
			/* 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)) {
		kpw_emit_error_switch
	}

	alert(level, msg);
	return rv;
}