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 }