/* [ʞ] 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.
*/
#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)))
#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,
};
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 <stdio.h>
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 <acct> */
break;
}
case addpw:{ /* kpw -a <acct> [<pw>] */
break;
}
case delpw:{ /* kpw -d <acct> */
break;
}
case genpw:{ /* kpw -g[lmu] <acct> [<len>] */
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.
*/
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;
}