totp

Simple cli tool for storing TOTP secrets and generating tokens
git clone https://git.inz.fi/totp/
Log | Files | Refs | Submodules

main.c (9461B)


      1 #include <stdbool.h>
      2 #include <stdint.h>
      3 #include <stdio.h>
      4 #include <stdlib.h>
      5 #include <string.h>
      6 
      7 #include <errno.h>
      8 #include <fcntl.h>
      9 #include <fnmatch.h>
     10 #include <inttypes.h>
     11 #include <termios.h>
     12 #include <time.h>
     13 #include <unistd.h>
     14 
     15 #include <sys/stat.h>
     16 
     17 #include "sha1.h"
     18 #include "sha256.h"
     19 #include "sha512.h"
     20 #include "tiny-AES-c/aes.h"
     21 #include "arg.h"
     22 #include "util.h"
     23 #include "db.h"
     24 #include "token.h"
     25 
     26 #define SECRET_DB_PATH ".local/share/totp"
     27 #define SECRET_DB_FILE "secrets.db"
     28 #define SECRET_DB_NEW_SUFFIX ".new"
     29 
     30 char *argv0;
     31 
     32 static void (*digest_hmacs[])(const void *key, size_t keylen,
     33 			      const void *data, size_t datalen,
     34 			      void *h) = {
     35 	sha1_hmac,
     36 	sha224_hmac,
     37 	sha256_hmac,
     38 	sha384_hmac,
     39 	sha512_hmac,
     40 };
     41 
     42 static size_t digest_sizes[] = {
     43 	SHA1_HASHSIZE,
     44 	SHA224_HASHSIZE,
     45 	SHA256_HASHSIZE,
     46 	SHA384_HASHSIZE,
     47 	SHA512_HASHSIZE,
     48 };
     49 
     50 static void print_base32(FILE *stream, struct bytes data)
     51 {
     52 	const char *chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
     53 	uint8_t *buffer = data.data;
     54 	uint16_t v = 0;
     55 	size_t b = 0;
     56 
     57 	while (buffer < data.end) {
     58 		v = v << 8 | *buffer++;
     59 		b += 8;
     60 		while (b >= 5) {
     61 			fprintf(stream, "%c", chars[(v >> (b - 5)) & 31]);
     62 			b -= 5;
     63 		}
     64 	}
     65 	if (b)
     66 		fprintf(stream, "%c", chars[(v << (5 - b)) & 31]);
     67 }
     68 
     69 void print_key(struct token *token, void *data)
     70 {
     71 	FILE *stream = data;
     72 
     73 	(void)data;
     74 
     75 	fprintf(stream, "%.*s%s%.*s\n",
     76 		(int)bytes_len(token->issuer), token->issuer.data,
     77 		bytes_len(token->issuer) ? ":" : "",
     78 		(int)bytes_len(token->desc), token->desc.data);
     79 }
     80 
     81 static void print_uriencode(FILE *stream, struct bytes data, bool getarg)
     82 {
     83 	const char *escape = ":/@+% &?";
     84 	const char *buf = (const void *)data.data;
     85 	const char *end = (const void *)data.end;
     86 	while (buf < end && *buf) {
     87 		size_t pass = strncspn(buf, end - buf, escape);
     88 		printf("%.*s", (int)pass, buf);
     89 		buf += pass;
     90 
     91 		while (buf < end && *buf && strchr(escape, *buf)) {
     92 			if (*buf == ' ' && getarg)
     93 				fprintf(stream, "+");
     94 			else
     95 				fprintf(stream, "%%%02" PRIx8, *(uint8_t *)buf);
     96 			buf++;
     97 		}
     98 	}
     99 }
    100 
    101 void print_keyuri(struct token *token,
    102 		  void *data)
    103 {
    104 	FILE *stream = data;
    105 
    106 	fputs("otpauth://totp/", stream);
    107 	if (bytes_len(token->issuer)) {
    108 		print_uriencode(stream, token->issuer, false);
    109 		fputc(':', stream);
    110 	}
    111 	print_uriencode(stream, token->desc, false);
    112 	fputs("?secret=", stream);
    113 	print_base32(stream, token->key);
    114 	if (bytes_len(token->issuer)) {
    115 		fputs("&issuer=", stream);
    116 		print_uriencode(stream, token->issuer, true);
    117 	}
    118 	fprintf(stream, "&algorithm=%s&digits=%" PRIu8 "&period=%" PRIu8 "\n",
    119 		digest_names[token->digest],
    120 		token->digits,
    121 		token->period);
    122 }
    123 
    124 struct generate_data {
    125 	const char *filter;
    126 	bool found;
    127 	time_t time;
    128 };
    129 
    130 void generate_token(struct token *token, void *data)
    131 {
    132 	struct generate_data *d = data;
    133 	uint32_t modulo = 1;
    134 	uint8_t i;
    135 	char descbuf[2 * UINT8_MAX + 2];
    136 	char *dp = descbuf;
    137 
    138 	if (bytes_len(token->issuer)) {
    139 		dp = mempushb(dp, token->issuer);
    140 		*dp++ = ':';
    141 	}
    142 	dp = mempushb(dp, token->desc);
    143 	*dp = '\0';
    144 
    145 	if (fnmatch(d->filter, descbuf, FNM_NOESCAPE))
    146 		return;
    147 
    148 	d->found = true;
    149 	for (i = 0; i < token->digits; i++)
    150 		modulo *= 10;
    151 
    152 	printf("%0*" PRIu32 "\n", (int)token->digits,
    153 	       totp(token->key.data, bytes_len(token->key), d->time, token->period, token->t0, digest_hmacs[token->digest], digest_sizes[token->digest]) % modulo);
    154 }
    155 
    156 struct write_filter_data {
    157 	int fd;
    158 	const char *filter;
    159 	struct AES_ctx *c;
    160 };
    161 
    162 void write_filter_key(struct token *token,
    163 		      void *data)
    164 {
    165 	struct write_filter_data *d = data;
    166 
    167 	if (d->filter) {
    168 		char descbuf[UINT8_MAX * 2 + 2];
    169 		char *w = descbuf;
    170 
    171 		if (bytes_len(token->issuer)) {
    172 			w = mempushb(w, token->issuer);
    173 			*w++ = ':';
    174 		}
    175 		*(char *)mempushb(w, token->desc) = '\0';
    176 
    177 		if (!fnmatch(d->filter, descbuf, FNM_NOESCAPE))
    178 			return;
    179 	}
    180 
    181 	db_add_key(d->fd, d->c, token);
    182 }
    183 
    184 enum cmd {
    185 	CMD_NONE,
    186 	CMD_TOK,
    187 	CMD_LIST,
    188 	CMD_ADD,
    189 	CMD_DEL,
    190 	CMD_EXP
    191 };
    192 
    193 void usage()
    194 {
    195 	fprintf(stderr,
    196 		"Usage: totp [OPTIONS]\n"
    197 		"-f <file>\tuse file as database\n"
    198 		"-k <pass>\tpassphrase for database encryption\n"
    199 		"-K <file>\tread encryption passphrase from file\n"
    200 		"-T <time>\tunix time for token generation\n"
    201 		"-l\tlist known secrets\n"
    202 		"-a <uri>\tadd uri to secrets\n"
    203 		"-d <filter>\tremove secrets matching filter\n"
    204 		"-t <filter>\tgenerate tokens for secrets matching filter\n"
    205 		"-T <time>\toverride current time for token generation\n"
    206 		"-e\texport secrets\n");
    207 	exit(1);
    208 }
    209 
    210 static void setecho(bool echo)
    211 {
    212 	struct termios tio;
    213 	if (tcgetattr(STDIN_FILENO, &tio))
    214 		return;
    215 	if (echo)
    216 		tio.c_lflag |= ECHO;
    217 	else
    218 		tio.c_lflag &= ~ECHO;
    219 	tcsetattr(STDIN_FILENO, TCSANOW, &tio);
    220 }
    221 
    222 int main(int argc, char *argv[])
    223 {
    224 	int fd;
    225 	int r;
    226 	struct sha1 d;
    227 	enum cmd cmd = CMD_NONE;
    228 	char *totpuri;
    229 	const char *key = NULL;
    230 	const char *keyfile = NULL;
    231 	const char *keyquery = NULL;
    232 	char *secretfile = NULL;
    233 	char *newsecretfile = NULL;
    234 	bool free_secretfile = true;
    235 	uint8_t keybuf[AES_KEYLEN + AES_BLOCKLEN];
    236 	size_t keylen = 0;
    237 	struct generate_data gd = { NULL, false, time(NULL) };
    238 	struct token token;
    239 	char *t;
    240 	struct AES_ctx c;
    241 	struct AES_ctx wc;
    242 	int wfd;
    243 
    244 	ARGBEGIN {
    245 	case 'l':
    246 		cmd = CMD_LIST;
    247 		break;
    248 	case 'a':
    249 		cmd = CMD_ADD;
    250 		totpuri = EARGF(usage());
    251 		break;
    252 	case 'd':
    253 		cmd = CMD_DEL;
    254 		keyquery = EARGF(usage());
    255 		break;
    256 	case 't':
    257 		cmd = CMD_TOK;
    258 		keyquery = EARGF(usage());
    259 		break;
    260 	case 'T':
    261 		gd.time = strtoull(EARGF(usage()), NULL, 10);
    262 		break;
    263 	case 'e':
    264 		cmd = CMD_EXP;
    265 		break;
    266 	case 'k':
    267 		key = EARGF(usage());
    268 		break;
    269 	case 'K':
    270 		keyfile = EARGF(usage());
    271 		break;
    272 	case 'f':
    273 		secretfile = EARGF(usage());
    274 		free_secretfile = false;
    275 		break;
    276 	default:
    277 		usage();
    278 		break;
    279 
    280 	} ARGEND
    281 
    282 	if (cmd == CMD_NONE && !argc)
    283 		usage();
    284 
    285 	sha1_init(&d);
    286 
    287 	if (key) {
    288 		sha1_update(&d, key, strlen(key));
    289 	} else {
    290 		size_t l = 0;
    291 		if (keyfile && strcmp(keyfile, "-")) {
    292 			fd = open(keyfile, O_RDONLY);
    293 		} else {
    294 			fd = STDIN_FILENO;
    295 
    296 			if (!keyfile) {
    297 				fprintf(stderr, "Enter passphrase: ");
    298 				setecho(false);
    299 			}
    300 		}
    301 
    302 		while ((r = read(fd, d.buffer + l,
    303 				 sizeof(d.buffer) - l)) > 0) {
    304 			size_t ll = strncspn((const char *)d.buffer + l, r, "\r\n");
    305 
    306 			if (ll < (size_t)r) {
    307 				l += ll;
    308 				break;
    309 			}
    310 
    311 			l += r;
    312 			if (l < sizeof(d.buffer))
    313 				continue;
    314 			sha1_update(&d, d.buffer, sizeof(d.buffer));
    315 			l = 0;
    316 		}
    317 
    318 		if (l)
    319 			sha1_update(&d, d.buffer, l);
    320 
    321 		if (!keyfile) {
    322 			fprintf(stderr, "\n");
    323 			setecho(true);
    324 		} else if (strcmp(keyfile, "-")) {
    325 			close(fd);
    326 		}
    327 	}
    328 
    329 	sha1_finish(&d);
    330 
    331 	while (keylen + sizeof(d.h) < sizeof(keybuf)) {
    332 		memcpy(keybuf + keylen, d.h, sizeof(d.h));
    333 		memcpy(d.buffer, d.h, sizeof(d.h));
    334 		sha1_init(&d);
    335 		sha1_update(&d, d.buffer, sizeof(d.h)); 
    336 		sha1_finish(&d);
    337 		keylen += sizeof(d.h);
    338 	}
    339 	memcpy(keybuf + keylen, d.h, sizeof(keybuf) - keylen);
    340 
    341 	srand(time(NULL));
    342 
    343 	if (!secretfile) {
    344 		const char *home = getenv("HOME");
    345 		secretfile = malloc(strlen(home) + sizeof(SECRET_DB_PATH) + sizeof(SECRET_DB_FILE) + 1);
    346 		sprintf(secretfile, "%s/%s/%s", home, SECRET_DB_PATH, SECRET_DB_FILE);
    347 	}
    348 
    349 	newsecretfile = malloc(strlen(secretfile) + sizeof(SECRET_DB_NEW_SUFFIX));
    350 	sprintf(newsecretfile, "%s%s", secretfile, SECRET_DB_NEW_SUFFIX);
    351 
    352 	for (t = strtok(secretfile + 1, "/"); (t = strtok(NULL, "/")); ) {
    353 		if (mkdir(secretfile, 0700) && errno != EEXIST)
    354 			croak("Could not create secret db dir: %s", totp_strerror(errno));
    355 		t[-1] = '/';
    356 	}
    357 
    358 	switch (cmd) {
    359 	case CMD_LIST:
    360 		fd = db_open_read(secretfile, &c, keybuf);
    361 		if (fd < 0 && errno != ENOENT)
    362 			croak("Opening existing db failed: %s", totp_strerror(errno));
    363 		db_foreach(fd, &c, print_key, stdout);
    364 		close(fd);
    365 		break;
    366 
    367 	case CMD_ADD:
    368 		token = token_parse_uri(totpuri);
    369 		if (!token.valid)
    370 			croak("Invalid uri");
    371 
    372 		fd = db_open_read(secretfile, &c, keybuf);
    373 		if (fd < 0 && errno != ENOENT)
    374 			croak("Opening existing db failed: %s", totp_strerror(errno));
    375 
    376 		wfd = db_open_write(newsecretfile, &wc, keybuf);
    377 		if (wfd < 0)
    378 			croak("Could not open temporary secret file: %s", totp_strerror(errno));
    379 		if (fd >= 0) {
    380 			db_foreach(fd, &c, write_filter_key,
    381 				   &(struct write_filter_data){ .fd = wfd, .c = &wc });
    382 			close(fd);
    383 		}
    384 		db_add_key(wfd, &wc, &token);
    385 		close(wfd);
    386 
    387 		rename(newsecretfile, secretfile);
    388 
    389 		break;
    390 
    391 	case CMD_NONE:
    392 		keyquery = argv[0];
    393 		/* fall-through */
    394 	case CMD_TOK:
    395 		fd = db_open_read(secretfile, &c, keybuf);
    396 		if (fd < 0)
    397 			croak("Could not open secret file: %s", totp_strerror(errno));
    398 		gd.filter = keyquery;
    399 		db_foreach(fd, &c, generate_token, &gd);
    400 		close(fd);
    401 		if (!gd.found)
    402 			croak("No secrets matching filter found");
    403 		break;
    404 
    405 	case CMD_DEL:
    406 		fd = db_open_read(secretfile, &c, keybuf);
    407 		if (fd < 0) {
    408 			if (errno == ENOENT)
    409 				break;
    410 			croak("Could not open secret file: %s", totp_strerror(errno));
    411 		}
    412 		wfd = db_open_write(newsecretfile, &wc, keybuf);
    413 		if (wfd < 0)
    414 			croak("Could not open temporary secret file: %s", totp_strerror(errno));
    415 		db_foreach(fd, &c, write_filter_key,
    416 			   &(struct write_filter_data){
    417 				.fd = wfd, .filter = keyquery,
    418 				.c = &wc
    419 			   });
    420 		close(wfd);
    421 		close(fd);
    422 		rename(newsecretfile, secretfile);
    423 		break;
    424 
    425 	case CMD_EXP:
    426 		fd = db_open_read(secretfile, &c, keybuf);
    427 		if (fd < 0)
    428 			croak("Could not open secret file: %s", totp_strerror(errno));
    429 		db_foreach(fd, &c, print_keyuri, stdout);
    430 		close(fd);
    431 		break;
    432 	}
    433 
    434 	if (free_secretfile)
    435 		free(secretfile);
    436 	free(newsecretfile);
    437 
    438 	return 0;
    439 }