spm.c (51458B)
1 /* See LICENSE file for copyright and license details. */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <unistd.h> 6 #include <ncurses.h> 7 #include <sodium.h> 8 #include <termios.h> 9 #include <errno.h> 10 #include <time.h> 11 #include <sys/wait.h> 12 #include <sys/ioctl.h> 13 #include <sys/stat.h> 14 #include <stdarg.h> 15 16 #include "config.def.h" 17 18 /* arbitrary sizes */ 19 #define MAX_PATH 256 20 #define MAX_PASS 2048 21 #define XOR_KEY 0x42 22 23 /* macros */ 24 #define XSTR(x) STR(x) 25 #define STR(x) #x 26 #define LENGTH(X) (sizeof(X) / sizeof(X[0])) 27 #define DB_MAGIC get_magic() 28 #define VERIFY_STRING get_verify() 29 #define SALT_LEN crypto_pwhash_SALTBYTES 30 #define KEY_LEN crypto_secretbox_KEYBYTES 31 #define NONCE_LEN crypto_secretbox_NONCEBYTES 32 #define MAC_LEN crypto_secretbox_MACBYTES 33 34 enum { 35 VIEW_LIST = 0, 36 VIEW_ENTRY, 37 VIEW_SEARCH, 38 }; 39 40 /* types */ 41 typedef struct Entry { 42 char *path; 43 char *title; 44 char *user; 45 char *pass; 46 char *notes; 47 time_t modified; 48 struct Entry *next; 49 } Entry; 50 51 typedef struct { 52 Entry *entries; 53 char *dbpath; 54 unsigned char key[KEY_LEN]; 55 unsigned char salt[SALT_LEN]; 56 int view; 57 Entry *selected; 58 char search[MAX_PATH]; 59 } DB; 60 61 typedef struct { 62 const char *cmd; 63 void (*func)(void); 64 } Binding; 65 66 /* globals */ 67 static DB db; 68 static WINDOW *mainwin; 69 static WINDOW *statuswin; 70 71 static const unsigned char _magic[] = { 72 'S' ^ XOR_KEY, 'L' ^ XOR_KEY, 'P' ^ XOR_KEY, 'A' ^ XOR_KEY, 73 'S' ^ XOR_KEY, 'S' ^ XOR_KEY, '0' ^ XOR_KEY, '2' ^ XOR_KEY 74 }; 75 76 static const unsigned char _verify[] = { 77 'S' ^ XOR_KEY, 'L' ^ XOR_KEY, 'P' ^ XOR_KEY, 'A' ^ XOR_KEY, 78 'S' ^ XOR_KEY, 'S' ^ XOR_KEY, 'O' ^ XOR_KEY, 'K' ^ XOR_KEY 79 }; 80 81 /* function declarations */ 82 static void cleanup(void); 83 static int createdb(const char *path, const char *masterpass); 84 static const char *get_magic(void); 85 static const char *get_verify(void); 86 static int loaddb(const char *path, const char *masterpass); 87 static int savedb(void); 88 static Entry *addentry(const char *path, const char *title); 89 static void delentry(Entry *e); 90 static int encrypt(const char *plain, char **cipher); 91 static int encrypt_data(const unsigned char *data, size_t len, unsigned char **encrypted, size_t *outlen); 92 static int decrypt_data(const unsigned char *encrypted, size_t len, unsigned char **decrypted, size_t *outlen); 93 static int write_encrypted_string(FILE *fp, const char *str); 94 static int read_encrypted_string(FILE *fp, char **str); 95 static void die(const char *fmt, ...); 96 static void ui_init(void); 97 static void ui_cleanup(void); 98 static void ui_draw(void); 99 static void ui_status(const char *fmt, ...); 100 static void handle_resize(void); 101 static WINDOW *create_dialog(int height, int width, const char *title); 102 static void cmd_add(void); 103 static void cmd_edit(void); 104 static void cmd_copy(void); 105 static void cmd_generate(void); 106 static void cmd_search(void); 107 static char *readpass(const char *prompt); 108 static char *genpass(int len); 109 static int copyto_clipboard(const char *text); 110 111 /* implementation */ 112 static void 113 delentry(Entry *e) 114 { 115 Entry *prev; 116 117 if (!e) 118 return; 119 120 /* Find previous entry */ 121 if (e == db.entries) { 122 db.entries = e->next; 123 } else { 124 for (prev = db.entries; prev && prev->next != e; prev = prev->next) 125 ; 126 if (prev) 127 prev->next = e->next; 128 } 129 130 /* Securely free all fields */ 131 if (e->path) { 132 sodium_memzero(e->path, strlen(e->path)); 133 free(e->path); 134 } 135 136 if (e->title) { 137 sodium_memzero(e->title, strlen(e->title)); 138 free(e->title); 139 } 140 141 if (e->user) { 142 sodium_memzero(e->user, strlen(e->user)); 143 free(e->user); 144 } 145 146 if (e->pass) { 147 sodium_memzero(e->pass, strlen(e->pass)); 148 free(e->pass); 149 } 150 151 if (e->notes) { 152 sodium_memzero(e->notes, strlen(e->notes)); 153 free(e->notes); 154 } 155 156 /* Zero out the entry structure before freeing */ 157 sodium_memzero(e, sizeof(Entry)); 158 free(e); 159 } 160 161 static int 162 encrypt_data(const unsigned char *data, size_t len, unsigned char **encrypted, size_t *outlen) 163 { 164 unsigned char nonce[NONCE_LEN]; 165 size_t clen = MAC_LEN + len; 166 unsigned char *c; 167 char *b64; 168 size_t b64len; 169 170 /* Allocate buffer for encrypted data + nonce */ 171 c = malloc(NONCE_LEN + clen); 172 if (!c) return -1; 173 174 /* Generate random nonce */ 175 randombytes_buf(nonce, sizeof(nonce)); 176 memcpy(c, nonce, NONCE_LEN); 177 178 /* Encrypt */ 179 if (crypto_secretbox_easy(c + NONCE_LEN, data, len, nonce, db.key) != 0) { 180 free(c); 181 return -1; 182 } 183 184 /* Convert to base64 */ 185 b64len = sodium_base64_encoded_len(NONCE_LEN + clen, sodium_base64_VARIANT_ORIGINAL); 186 b64 = malloc(b64len); 187 if (!b64) { 188 free(c); 189 return -1; 190 } 191 192 sodium_bin2base64(b64, b64len, 193 c, NONCE_LEN + clen, 194 sodium_base64_VARIANT_ORIGINAL); 195 196 *encrypted = (unsigned char *)b64; 197 *outlen = strlen(b64); 198 199 free(c); 200 return 0; 201 } 202 203 static int 204 decrypt_data(const unsigned char *encrypted, size_t len, unsigned char **decrypted, size_t *outlen) 205 { 206 unsigned char *bin; 207 size_t bin_len; 208 unsigned char *p; 209 int ret = -1; 210 211 /* Decode base64 */ 212 bin = malloc(len); /* Will be smaller than base64 */ 213 if (!bin) return -1; 214 215 if (sodium_base642bin(bin, len, 216 (const char *)encrypted, len, 217 NULL, &bin_len, 218 NULL, 219 sodium_base64_VARIANT_ORIGINAL) != 0) { 220 free(bin); 221 return -1; 222 } 223 224 if (bin_len <= NONCE_LEN + MAC_LEN) { 225 free(bin); 226 return -1; 227 } 228 229 /* Allocate output buffer */ 230 *outlen = bin_len - NONCE_LEN - MAC_LEN; 231 p = malloc(*outlen + 1); 232 if (!p) { 233 free(bin); 234 return -1; 235 } 236 237 /* Decrypt */ 238 if (crypto_secretbox_open_easy(p, 239 bin + NONCE_LEN, 240 bin_len - NONCE_LEN, 241 bin, /* nonce at start */ 242 db.key) == 0) { 243 p[*outlen] = '\0'; 244 *decrypted = p; 245 ret = 0; 246 } else { 247 free(p); 248 } 249 250 free(bin); 251 return ret; 252 } 253 254 static int 255 write_encrypted_string(FILE *fp, const char *str) 256 { 257 unsigned char *encrypted; 258 size_t enclen; 259 uint32_t len; 260 261 if (!str) { 262 len = 0; 263 fwrite(&len, sizeof(len), 1, fp); 264 return 0; 265 } 266 267 if (encrypt_data((unsigned char *)str, strlen(str), &encrypted, &enclen) != 0) 268 return -1; 269 270 len = enclen; 271 if (fwrite(&len, sizeof(len), 1, fp) != 1) { 272 free(encrypted); 273 return -1; 274 } 275 276 if (fwrite(encrypted, 1, enclen, fp) != enclen) { 277 free(encrypted); 278 return -1; 279 } 280 281 free(encrypted); 282 return 0; 283 } 284 285 static int 286 read_encrypted_string(FILE *fp, char **str) 287 { 288 uint32_t len; 289 unsigned char *encrypted; 290 unsigned char *decrypted; 291 size_t declen; 292 293 *str = NULL; /* Initialize to NULL */ 294 295 if (fread(&len, sizeof(len), 1, fp) != 1) { 296 return -1; 297 } 298 299 if (len == 0) { 300 return 0; 301 } 302 303 encrypted = malloc(len); 304 if (!encrypted) { 305 return -1; 306 } 307 308 if (fread(encrypted, 1, len, fp) != len) { 309 free(encrypted); 310 return -1; 311 } 312 313 if (decrypt_data(encrypted, len, &decrypted, &declen) != 0) { 314 free(encrypted); 315 return -1; 316 } 317 318 free(encrypted); 319 *str = (char *)decrypted; 320 return 0; 321 } 322 323 static void 324 die(const char *fmt, ...) 325 { 326 va_list ap; 327 328 va_start(ap, fmt); 329 vfprintf(stderr, fmt, ap); 330 va_end(ap); 331 fprintf(stderr, "\n"); 332 exit(1); 333 } 334 335 static void 336 cleanup(void) 337 { 338 Entry *e, *next; 339 340 for (e = db.entries; e != NULL; e = next) { 341 next = e->next; 342 if (e->path) free(e->path); 343 if (e->title) free(e->title); 344 if (e->user) free(e->user); 345 if (e->pass) { 346 sodium_memzero(e->pass, strlen(e->pass)); 347 free(e->pass); 348 } 349 if (e->notes) free(e->notes); 350 free(e); 351 } 352 353 sodium_memzero(&db, sizeof(db)); 354 } 355 356 static int 357 savedb(void) 358 { 359 FILE *fp; 360 Entry *e; 361 uint32_t count = 0; 362 uint32_t version = 2; 363 unsigned char *encrypted; 364 size_t enclen; 365 366 if (!(fp = fopen(db.dbpath, "wb"))) 367 return -1; 368 369 /* Write magic and version */ 370 fwrite(DB_MAGIC, 1, strlen(DB_MAGIC), fp); 371 fwrite(&version, sizeof(version), 1, fp); 372 373 /* Write salt */ 374 fwrite(db.salt, 1, sizeof(db.salt), fp); 375 376 /* Create verification block with constant string */ 377 if (encrypt_data((const unsigned char *)VERIFY_STRING, 378 strlen(VERIFY_STRING), 379 &encrypted, &enclen) != 0) { 380 fclose(fp); 381 return -1; 382 } 383 384 /* Write verification block */ 385 fwrite(&enclen, sizeof(enclen), 1, fp); 386 fwrite(encrypted, 1, enclen, fp); 387 free(encrypted); 388 389 /* Count entries */ 390 for (e = db.entries; e; e = e->next) 391 count++; 392 393 /* Write entry count */ 394 fwrite(&count, sizeof(count), 1, fp); 395 396 /* Write each entry */ 397 for (e = db.entries; e; e = e->next) { 398 if (write_encrypted_string(fp, e->path) != 0 || 399 write_encrypted_string(fp, e->title) != 0 || 400 write_encrypted_string(fp, e->user) != 0 || 401 write_encrypted_string(fp, e->pass) != 0 || 402 write_encrypted_string(fp, e->notes) != 0) { 403 fclose(fp); 404 return -1; 405 } 406 fwrite(&e->modified, sizeof(e->modified), 1, fp); 407 } 408 409 fclose(fp); 410 return 0; 411 } 412 413 static const char * 414 get_magic(void) 415 { 416 static char magic[sizeof(_magic) + 1]; 417 for (size_t i = 0; i < sizeof(_magic); i++) 418 magic[i] = _magic[i] ^ XOR_KEY; 419 magic[sizeof(_magic)] = '\0'; 420 return magic; 421 } 422 423 static const char * 424 get_verify(void) 425 { 426 static char verify[sizeof(_verify) + 1]; 427 for (size_t i = 0; i < sizeof(_verify); i++) 428 verify[i] = _verify[i] ^ XOR_KEY; 429 verify[sizeof(_verify)] = '\0'; 430 return verify; 431 } 432 433 static int 434 createdb(const char *path, const char *masterpass) 435 { 436 FILE *fp; 437 uint32_t version = 2; 438 unsigned char *encrypted; 439 size_t enclen; 440 441 if (access(path, F_OK) == 0) 442 return -1; 443 444 if (!(fp = fopen(path, "wb"))) 445 return -1; 446 447 if (chmod(path, S_IRUSR|S_IWUSR) != 0){ 448 fclose(fp); 449 unlink(path); 450 return -1; 451 } 452 453 /* Write magic and version */ 454 fwrite(DB_MAGIC, 1, strlen(DB_MAGIC), fp); 455 fwrite(&version, sizeof(version), 1, fp); 456 457 /* Generate new salt and derive key */ 458 randombytes_buf(db.salt, sizeof(db.salt)); 459 if (crypto_pwhash(db.key, sizeof(db.key), 460 masterpass, strlen(masterpass), 461 db.salt, 462 crypto_pwhash_OPSLIMIT_INTERACTIVE, 463 crypto_pwhash_MEMLIMIT_INTERACTIVE, 464 crypto_pwhash_ALG_DEFAULT) != 0) { 465 fclose(fp); 466 return -1; 467 } 468 469 /* Write salt */ 470 fwrite(db.salt, 1, sizeof(db.salt), fp); 471 472 if (encrypt_data((const unsigned char *)VERIFY_STRING, 473 strlen(VERIFY_STRING), 474 &encrypted, &enclen) != 0) { 475 fclose(fp); 476 return -1; 477 } 478 479 fwrite(&enclen, sizeof(enclen), 1, fp); 480 fwrite(encrypted, 1, enclen, fp); 481 free(encrypted); 482 483 uint32_t count = 0; 484 fwrite(&count, sizeof(count), 1, fp); 485 486 fclose(fp); 487 return 0; 488 } 489 490 static Entry * 491 addentry(const char *path, const char *title) 492 { 493 Entry *e; 494 495 e = calloc(1, sizeof(Entry)); 496 if (!e) 497 return NULL; 498 499 e->path = strdup(path); 500 e->title = strdup(title); 501 e->next = db.entries; 502 db.entries = e; 503 504 return e; 505 } 506 507 static void 508 cmd_edit(void) 509 { 510 WINDOW *win; 511 char buf[MAX_PATH]; 512 char *pass = NULL; 513 int y = 1; 514 int c; 515 516 if (!db.selected) 517 return; 518 519 /* Create centered dialog window */ 520 win = create_dialog(14, COLS - 20, "Edit Entry"); 521 if (!win) { 522 ui_status("Failed to create dialog window"); 523 return; 524 } 525 526 /* Header with instructions */ 527 wattron(win, A_BOLD); 528 mvwprintw(win, y++, 2, "Editing Entry"); 529 wattroff(win, A_BOLD); 530 wattron(win, A_DIM); 531 mvwprintw(win, y++, 2, "(Press Enter to keep current value)"); 532 wattroff(win, A_DIM); 533 y++; /* Add space */ 534 535 keypad(win, TRUE); /* Enable special keys */ 536 537 /* Path input */ 538 wattron(win, A_BOLD); 539 mvwprintw(win, y, 2, "Path: "); 540 wattroff(win, A_BOLD); 541 mvwprintw(win, y, 8, "[%s]", db.selected->path ? db.selected->path : ""); 542 wmove(win, y++, 8); 543 wclrtoeol(win); 544 nodelay(win, TRUE); 545 echo(); 546 547 /* Check for ESC */ 548 c = wgetch(win); 549 if (c == 27) { 550 delwin(win); 551 touchwin(mainwin); 552 touchwin(statuswin); 553 ui_status("Edit cancelled"); 554 return; 555 } 556 if (c != ERR) ungetch(c); 557 nodelay(win, FALSE); 558 559 wgetnstr(win, buf, sizeof(buf)); 560 if (strlen(buf)) { 561 free(db.selected->path); 562 db.selected->path = strdup(buf); 563 } 564 565 /* Title input */ 566 wattron(win, A_BOLD); 567 mvwprintw(win, y, 2, "Title: "); 568 wattroff(win, A_BOLD); 569 mvwprintw(win, y, 9, "[%s]", db.selected->title ? db.selected->title : ""); 570 wmove(win, y++, 9); 571 wclrtoeol(win); 572 nodelay(win, TRUE); 573 574 c = wgetch(win); 575 if (c == 27) { 576 delwin(win); 577 touchwin(mainwin); 578 touchwin(statuswin); 579 ui_status("Edit cancelled"); 580 return; 581 } 582 if (c != ERR) ungetch(c); 583 nodelay(win, FALSE); 584 585 wgetnstr(win, buf, sizeof(buf)); 586 if (strlen(buf)) { 587 free(db.selected->title); 588 db.selected->title = strdup(buf); 589 } 590 591 /* Username input */ 592 wattron(win, A_BOLD); 593 mvwprintw(win, y, 2, "Username: "); 594 wattroff(win, A_BOLD); 595 mvwprintw(win, y, 12, "[%s]", db.selected->user ? db.selected->user : ""); 596 wmove(win, y++, 12); 597 wclrtoeol(win); 598 nodelay(win, TRUE); 599 600 c = wgetch(win); 601 if (c == 27) { 602 delwin(win); 603 touchwin(mainwin); 604 touchwin(statuswin); 605 ui_status("Edit cancelled"); 606 return; 607 } 608 if (c != ERR) ungetch(c); 609 nodelay(win, FALSE); 610 611 wgetnstr(win, buf, sizeof(buf)); 612 if (strlen(buf)) { 613 free(db.selected->user); 614 db.selected->user = strdup(buf); 615 } 616 617 /* Notes input */ 618 wattron(win, A_BOLD); 619 mvwprintw(win, y, 2, "Notes: "); 620 wattroff(win, A_BOLD); 621 mvwprintw(win, y, 9, "[%s]", db.selected->notes ? db.selected->notes : ""); 622 wmove(win, y++, 9); 623 wclrtoeol(win); 624 nodelay(win, TRUE); 625 626 c = wgetch(win); 627 if (c == 27) { 628 delwin(win); 629 touchwin(mainwin); 630 touchwin(statuswin); 631 ui_status("Edit cancelled"); 632 return; 633 } 634 if (c != ERR) ungetch(c); 635 nodelay(win, FALSE); 636 637 wgetnstr(win, buf, sizeof(buf)); 638 if (strlen(buf)) { 639 free(db.selected->notes); 640 db.selected->notes = strdup(buf); 641 } 642 643 /* Password handling */ 644 y++; 645 wattron(win, A_BOLD); 646 mvwprintw(win, y++, 2, "Change password? (y/n/ESC)"); 647 wattroff(win, A_BOLD); 648 noecho(); 649 wrefresh(win); 650 c = wgetch(win); 651 652 if (c == 27) { /* ESC */ 653 delwin(win); 654 touchwin(mainwin); 655 touchwin(statuswin); 656 ui_status("Edit cancelled"); 657 return; 658 } 659 660 if (c == 'y' || c == 'Y') { 661 wattron(win, A_BOLD); 662 mvwprintw(win, y++, 2, "Generate new random password? (y/n/ESC)"); 663 wattroff(win, A_BOLD); 664 wrefresh(win); 665 c = wgetch(win); 666 667 if (c == 27) { /* ESC */ 668 delwin(win); 669 touchwin(mainwin); 670 touchwin(statuswin); 671 ui_status("Edit cancelled"); 672 return; 673 } 674 675 if (c == 'y' || c == 'Y') { 676 /* Generate new password */ 677 pass = genpass(PW_LEN); 678 if (pass) { 679 wattron(win, A_BOLD); 680 mvwprintw(win, y++, 2, "New password generated and copied to clipboard"); 681 wattroff(win, A_BOLD); 682 wrefresh(win); 683 if (copyto_clipboard(pass) == 0) { 684 ui_status("Generated password copied to clipboard"); 685 } 686 } 687 } else { 688 /* Manual password entry */ 689 wattron(win, A_BOLD); 690 mvwprintw(win, y++, 2, "Enter new password"); 691 wattroff(win, A_BOLD); 692 wattron(win, A_DIM); 693 mvwprintw(win, y++, 2, "(Password will not be shown while typing)"); 694 wattroff(win, A_DIM); 695 wrefresh(win); 696 pass = readpass(""); 697 698 if (!pass) { /* ESC was pressed in readpass */ 699 delwin(win); 700 touchwin(mainwin); 701 touchwin(statuswin); 702 ui_status("Edit cancelled"); 703 return; 704 } 705 } 706 707 /* Update password if provided */ 708 if (pass && strlen(pass) > 0) { 709 unsigned char *encrypted; 710 size_t enclen; 711 712 /* Clean up old password */ 713 if (db.selected->pass) { 714 sodium_memzero(db.selected->pass, strlen(db.selected->pass)); 715 free(db.selected->pass); 716 db.selected->pass = NULL; 717 } 718 719 /* Encrypt and store new password */ 720 if (encrypt_data((const unsigned char *)pass, strlen(pass), 721 &encrypted, &enclen) != 0) { 722 ui_status("Failed to encrypt password"); 723 sodium_memzero(pass, strlen(pass)); 724 free(pass); 725 delwin(win); 726 touchwin(mainwin); 727 touchwin(statuswin); 728 refresh(); 729 return; 730 } 731 db.selected->pass = (char *)encrypted; 732 } 733 } 734 735 /* Cleanup sensitive data */ 736 if (pass) { 737 sodium_memzero(pass, strlen(pass)); 738 free(pass); 739 } 740 741 /* Update modification time and save */ 742 db.selected->modified = time(NULL); 743 if (savedb() != 0) { 744 ui_status("Failed to save database"); 745 } else { 746 ui_status("Entry updated successfully"); 747 } 748 749 /* Clean up window */ 750 delwin(win); 751 touchwin(mainwin); 752 touchwin(statuswin); 753 ui_draw(); 754 } 755 756 static char * 757 readpass(const char *prompt) 758 { 759 char *pass = NULL; 760 WINDOW *win; 761 762 /* Create a new window for password input */ 763 win = create_dialog(5, COLS-8, "Password Input"); 764 765 /* Show prompt and info */ 766 mvwprintw(win, 1, 2, "%s", prompt); 767 wattron(win, A_DIM); /* Dimmed text for info message */ 768 mvwprintw(win, 2, 2, "(Password will not be shown while typing)"); 769 wattroff(win, A_DIM); 770 wrefresh(win); 771 772 /* Read password */ 773 pass = malloc(MAX_PASS); 774 if (pass) { 775 noecho(); /* Ensure password isn't shown */ 776 curs_set(1); /* Show cursor while typing */ 777 wmove(win, 1, 2 + strlen(prompt)); 778 wgetnstr(win, pass, MAX_PASS-1); 779 curs_set(0); /* Hide cursor again */ 780 } 781 782 /* Clean up */ 783 delwin(win); 784 touchwin(mainwin); 785 touchwin(statuswin); 786 wrefresh(mainwin); 787 wrefresh(statuswin); 788 789 return pass; 790 } 791 792 static void 793 ui_status(const char *fmt, ...) 794 { 795 va_list ap; 796 797 werase(statuswin); 798 wmove(statuswin, 0, 0); 799 wattron(statuswin, COLOR_PAIR(COLOR_UI_STATUS)); 800 va_start(ap, fmt); 801 vw_printw(statuswin, fmt, ap); 802 va_end(ap); 803 wattroff(statuswin, COLOR_PAIR(COLOR_UI_STATUS)); 804 wrefresh(statuswin); 805 } 806 807 static int 808 encrypt(const char *plain, char **cipher) 809 { 810 unsigned char nonce[crypto_secretbox_NONCEBYTES]; 811 unsigned char *c; 812 unsigned char *message = (unsigned char *)plain; 813 char *b64; 814 size_t clen = crypto_secretbox_MACBYTES + strlen(plain); 815 size_t mlen = strlen(plain); 816 size_t b64len; 817 818 /* Allocate buffer for encrypted data + nonce */ 819 c = malloc(crypto_secretbox_NONCEBYTES + clen); 820 if (!c) return -1; 821 822 /* Generate random nonce */ 823 randombytes_buf(nonce, sizeof(nonce)); 824 memcpy(c, nonce, sizeof(nonce)); 825 826 /* Encrypt */ 827 if (crypto_secretbox_easy(c + crypto_secretbox_NONCEBYTES, 828 message, mlen, 829 nonce, db.key) != 0) { 830 free(c); 831 return -1; 832 } 833 834 /* Convert to base64 */ 835 b64len = sodium_base64_encoded_len(crypto_secretbox_NONCEBYTES + clen, 836 sodium_base64_VARIANT_ORIGINAL); 837 b64 = malloc(b64len); 838 if (!b64) { 839 free(c); 840 return -1; 841 } 842 843 sodium_bin2base64(b64, b64len, 844 c, crypto_secretbox_NONCEBYTES + clen, 845 sodium_base64_VARIANT_ORIGINAL); 846 847 *cipher = b64; 848 free(c); 849 return 0; 850 } 851 852 static char * 853 genpass(int len) 854 { 855 856 char *pass; 857 int i; 858 859 if (len < 1) 860 return NULL; 861 862 pass = malloc(len + 1); 863 if (!pass) 864 return NULL; 865 866 for (i = 0; i < len; i++) 867 pass[i] = charset[randombytes_uniform(sizeof(charset) - 1)]; 868 pass[len] = '\0'; 869 870 return pass; 871 } 872 873 static void 874 handle_resize(void) 875 { 876 endwin(); 877 refresh(); 878 clear(); 879 880 /* Recreate windows with new size */ 881 if (mainwin) 882 delwin(mainwin); 883 if (statuswin) 884 delwin(statuswin); 885 886 mainwin = newwin(LINES - 3, COLS, 0, 0); 887 statuswin = newwin(3, COLS, LINES - 3, 0); 888 889 /* Reset window properties */ 890 keypad(mainwin, TRUE); 891 scrollok(mainwin, TRUE); 892 wbkgd(statuswin, COLOR_PAIR(COLOR_STATUS_BAR)); 893 box(statuswin, 0, 0); 894 895 /* Redraw everything */ 896 ui_draw(); 897 refresh(); 898 wrefresh(mainwin); 899 wrefresh(statuswin); 900 } 901 902 static WINDOW * 903 create_dialog(int height, int width, const char *title) 904 { 905 WINDOW *win; 906 int starty, startx; 907 908 /* Ensure minimum size and maximum size */ 909 height = height < 3 ? 3 : height; 910 width = width < 20 ? 20 : width; 911 912 /* Don't exceed screen size */ 913 if (height > LINES - 2) 914 height = LINES - 2; 915 if (width > COLS - 2) 916 width = COLS - 2; 917 918 /* Center the window */ 919 starty = (LINES - height) / 2; 920 startx = (COLS - width) / 2; 921 922 /* Create window with border */ 923 win = newwin(height, width, starty, startx); 924 box(win, 0, 0); 925 926 /* Add title if provided */ 927 if (title) { 928 int title_len = strlen(title); 929 if (title_len > width - 4) 930 title_len = width - 4; 931 wattron(win, COLOR_PAIR(COLOR_DIALOG_HEADER)); 932 933 mvwprintw(win, 0, (width - title_len - 2) / 2, " %.*s ", title_len, title); 934 wattroff(win, COLOR_PAIR(COLOR_DIALOG_HEADER)); 935 } 936 937 /* Ensure the window is visible */ 938 wrefresh(win); 939 return win; 940 } 941 942 static int 943 copyto_clipboard(const char *text) 944 { 945 FILE *pipe; 946 size_t text_len; 947 const char *cmd = NULL; 948 pid_t pid; 949 950 if (!text) 951 return -1; 952 953 text_len = strlen(text); 954 955 if (getenv("WAYLAND_DISPLAY")) { 956 cmd = "wl-copy"; 957 } else if (getenv("DISPLAY")) { 958 cmd = "xclip -selection clipboard"; 959 } else { 960 ui_status("No clipboard available (neither Wayland nor X11)"); 961 return -1; 962 } 963 964 /* Copy to clipboard */ 965 pipe = popen(cmd, "w"); 966 if (!pipe) { 967 return -1; 968 } 969 if (fwrite(text, 1, text_len, pipe) != text_len) { 970 pclose(pipe); 971 return -1; 972 } 973 if (pclose(pipe) != 0) { 974 return -1; 975 } 976 977 /* Fork process for clipboard clearing */ 978 if ((pid = fork()) == 0) { 979 /* Child process */ 980 sleep(clr_clipboard); 981 pipe = popen(cmd, "w"); 982 if (pipe) { 983 fwrite("", 1, 0, pipe); 984 pclose(pipe); 985 } 986 _exit(0); 987 } 988 989 return 0; 990 } 991 992 static void 993 ui_init(void) 994 { 995 996 /* Initialize colors */ 997 start_color(); 998 for (size_t i = 0; i < LENGTH(colors); i++) { 999 init_pair(i + 1, colors[i][0], colors[i][1]); 1000 } 1001 1002 /* Create main windows with borders */ 1003 mainwin = newwin(LINES - 3, COLS, 0, 0); 1004 statuswin = newwin(3, COLS, LINES - 3, 0); 1005 1006 /* Enable keypad and scrolling */ 1007 keypad(mainwin, TRUE); 1008 scrollok(mainwin, TRUE); 1009 1010 /* Set up status window */ 1011 wbkgd(statuswin, COLOR_PAIR(COLOR_STATUS_BAR)); 1012 box(statuswin, 0, 0); 1013 } 1014 1015 static void 1016 ui_cleanup(void) 1017 { 1018 if (mainwin) delwin(mainwin); 1019 if (statuswin) delwin(statuswin); 1020 endwin(); 1021 } 1022 1023 static void 1024 ui_draw(void) 1025 { 1026 Entry *e; 1027 int y = 0; 1028 char timestr[32]; 1029 struct tm *tm; 1030 int available_width = COLS; 1031 /* Declare all column widths at function start */ 1032 int time_width = 19; /* Fixed width for timestamp */ 1033 int path_width; /* Will be set in each view */ 1034 int title_width; 1035 int user_width; 1036 1037 werase(mainwin); 1038 1039 switch (db.view) { 1040 1041 case VIEW_LIST: 1042 /* Calculate column widths */ 1043 time_width = 19; /* Fixed width for timestamp */ 1044 path_width = (available_width - time_width - 6) / 3; 1045 title_width = path_width; 1046 user_width = available_width - time_width - path_width - title_width - 6; 1047 1048 /* Draw header without separators */ 1049 wattron(mainwin, COLOR_PAIR(COLOR_TOP_BAR) | A_BOLD); 1050 mvwhline(mainwin, 0, 0, ' ', available_width); // Clear the line first with spaces 1051 1052 /* Draw headers with proper spacing */ 1053 mvwprintw(mainwin, 0, 0, "%-*s", time_width, "Modified"); 1054 mvwprintw(mainwin, 0, time_width + 2, "%-*s", path_width, "Category"); 1055 mvwprintw(mainwin, 0, time_width + path_width + 4, "%-*s", title_width, "Title"); 1056 mvwprintw(mainwin, 0, time_width + path_width + title_width + 6, "%-*s", user_width, "Username"); 1057 1058 wattroff(mainwin, COLOR_PAIR(COLOR_TOP_BAR) | A_BOLD); 1059 1060 /* Draw header background */ 1061 // wattron(mainwin, A_REVERSE); 1062 // mvwhline(mainwin, 0, 0, ' ', available_width); 1063 1064 /* Draw centered headers */ 1065 // wattron(mainwin, COLOR_PAIR(COLOR_TOP_BAR) | A_BOLD); /* Use your defined color pair */ 1066 // mvwprintw(mainwin, 0, 0, "%*s%*s", 1067 // mod_len, "Modified", mod_pad, ""); 1068 // mvwprintw(mainwin, 0, time_width + 1, "%*s%*s", 1069 // path_len, "Path", path_pad, ""); 1070 // mvwprintw(mainwin, 0, time_width + path_width + 2, "%*s%*s", 1071 // title_len, "Title", title_pad, ""); 1072 // mvwprintw(mainwin, 0, time_width + path_width + title_width + 3, "%*s%*s", 1073 // user_len, "Username", user_pad, ""); 1074 // wattroff(mainwin, COLOR_PAIR(COLOR_TOP_BAR) | A_BOLD); 1075 // wattroff(mainwin, A_REVERSE); 1076 1077 /* Separator line */ 1078 // wattron(mainwin, A_DIM); 1079 // mvwaddch(mainwin, 1, time_width, ACS_VLINE); 1080 // mvwaddch(mainwin, 1, time_width + path_width + 1, ACS_VLINE); 1081 // mvwaddch(mainwin, 1, time_width + path_width + title_width + 2, ACS_VLINE); 1082 // mvwhline(mainwin, 1, 0, ACS_HLINE, available_width); 1083 // wattroff(mainwin, A_DIM); 1084 1085 /* Entries (left-aligned) */ 1086 y = 2; 1087 for (e = db.entries; e && y < LINES-2; e = e->next) { 1088 if (e == db.selected) 1089 1090 wattron(mainwin, COLOR_PAIR(COLOR_SELECTED)); 1091 1092 tm = localtime(&e->modified); 1093 strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M", tm); 1094 1095 /* Left align entries */ 1096 mvwprintw(mainwin, y, 0, "%-*s", time_width, timestr); 1097 waddch(mainwin, ' '); 1098 mvwprintw(mainwin, y, time_width + 1, "%-*s", path_width, e->path ? e->path : ""); 1099 waddch(mainwin, ' '); 1100 mvwprintw(mainwin, y, time_width + path_width + 2, "%-*s", title_width, e->title ? e->title : ""); 1101 waddch(mainwin, ' '); 1102 mvwprintw(mainwin, y, time_width + path_width + title_width + 3, "%-*s", user_width, e->user ? e->user : ""); 1103 1104 if (e == db.selected) 1105 wattroff(mainwin, COLOR_PAIR(COLOR_SELECTED)); 1106 1107 y++; 1108 } 1109 break; 1110 1111 case VIEW_ENTRY: 1112 /* Detail view implementation remains the same */ 1113 if (db.selected) { 1114 tm = localtime(&db.selected->modified); 1115 strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M", tm); 1116 1117 wattron(mainwin, A_BOLD); 1118 mvwprintw(mainwin, y++, 0, "Details"); 1119 wattroff(mainwin, A_BOLD); 1120 mvwhline(mainwin, y++, 0, ACS_HLINE, available_width); 1121 1122 mvwprintw(mainwin, y++, 0, "Modified: %s", timestr); 1123 mvwprintw(mainwin, y++, 0, "Path: %s", db.selected->path ? db.selected->path : ""); 1124 mvwprintw(mainwin, y++, 0, "Title: %s", db.selected->title ? db.selected->title : ""); 1125 mvwprintw(mainwin, y++, 0, "Username: %s", db.selected->user ? db.selected->user : ""); 1126 mvwprintw(mainwin, y++, 0, "Password: [Hidden - Press 'y' to copy]"); 1127 1128 if (db.selected->notes) { 1129 mvwprintw(mainwin, y++, 0, "Notes: %s", db.selected->notes); 1130 } 1131 } 1132 break; 1133 1134 case VIEW_SEARCH: { 1135 int matches = 0; 1136 int i = 0; 1137 int idx = 0; 1138 Entry *first_match = NULL; 1139 Entry **matched_entries = NULL; 1140 1141 /* Calculate column widths */ 1142 path_width = (available_width - time_width - 6) / 3; 1143 title_width = path_width; 1144 user_width = available_width - time_width - path_width - title_width - 6; 1145 1146 /* Ensure the columns fit within available space */ 1147 if (path_width + title_width + user_width + 6 > available_width) { 1148 ui_status("Column widths exceed available space!"); 1149 break; 1150 } 1151 1152 /* First collect all matches across all fields */ 1153 for (e = db.entries; e; e = e->next) { 1154 if (strstr(e->path, db.search) || 1155 strstr(e->title, db.search) || 1156 (e->user && strstr(e->user, db.search))) { 1157 matches++; 1158 } 1159 } 1160 1161 /* Create array of matches */ 1162 if (matches > 0) { 1163 matched_entries = malloc(matches * sizeof(Entry*)); 1164 if (!matched_entries) { 1165 ui_status("Memory allocation failed"); 1166 break; 1167 } 1168 1169 /* Fill match array */ 1170 for (e = db.entries; e; e = e->next) { 1171 if (strstr(e->path, db.search) || 1172 strstr(e->title, db.search) || 1173 (e->user && strstr(e->user, db.search))) { 1174 matched_entries[idx++] = e; 1175 if (!first_match) first_match = e; 1176 } 1177 } 1178 1179 /* Handle selection */ 1180 if (matches == 1) { 1181 db.selected = first_match; 1182 } else if (!db.selected || 1183 !(strstr(db.selected->path, db.search) || 1184 strstr(db.selected->title, db.search) || 1185 (db.selected->user && strstr(db.selected->user, db.search)))) { 1186 db.selected = first_match; 1187 } 1188 } 1189 1190 /* Draw header - center title */ 1191 int title_length = snprintf(NULL, 0, "Search Results for \"%s\" (%d matches)", db.search, matches); 1192 int title_start = (available_width - title_length) / 2; 1193 wattron(mainwin, A_BOLD | A_REVERSE); 1194 mvwhline(mainwin, 0, 0, ' ', available_width); 1195 mvwprintw(mainwin, 0, title_start, "Search Results for \"%s\" (%d matches)", db.search, matches); 1196 wattroff(mainwin, A_BOLD | A_REVERSE); 1197 1198 /* Draw separator with field headers */ 1199 wattron(mainwin, A_DIM); 1200 mvwhline(mainwin, 1, 0, ACS_HLINE, available_width); 1201 wattroff(mainwin, A_DIM); 1202 1203 /* Draw matched entries */ 1204 y = 2; 1205 for (i = 0; i < matches && y < LINES-2; i++) { 1206 e = matched_entries[i]; 1207 1208 if (e == db.selected) 1209 wattron(mainwin, COLOR_PAIR(COLOR_SELECTED)); 1210 1211 /* Draw entry with all fields */ 1212 mvwprintw(mainwin, y, 0, "%-*s", path_width, e->path ? e->path : ""); 1213 mvwprintw(mainwin, y, path_width, "%-*s", title_width, e->title ? e->title : ""); 1214 mvwprintw(mainwin, y, path_width + title_width, "%-*s", user_width, e->user ? e->user : ""); 1215 1216 if (e == db.selected) 1217 wattroff(mainwin, COLOR_PAIR(COLOR_SELECTED)); 1218 1219 y++; 1220 } 1221 1222 /* Clean up */ 1223 if (matched_entries) 1224 free(matched_entries); 1225 1226 break; 1227 } 1228 } 1229 /* Status bar */ 1230 wmove(statuswin, 1, 1); 1231 wprintw(statuswin, " [a]dd [d]elete [e]dit [y]ank [/]search [tab]view [q]uit"); 1232 wrefresh(mainwin); 1233 wrefresh(statuswin); 1234 } 1235 1236 static void 1237 cmd_add(void) 1238 { 1239 WINDOW *win; 1240 char path[MAX_PATH] = {0}; 1241 char title[MAX_PATH] = {0}; 1242 char user[MAX_PATH] = {0}; 1243 char notes[MAX_PATH] = {0}; 1244 char *pass = NULL; 1245 Entry *e; 1246 int input_y = 3; 1247 int label_x = 2; /* Position for labels */ 1248 int input_x = 15; /* Position for input fields */ 1249 // int max_field_width; /* Maximum width for input fields */ 1250 1251 /* Create centered dialog window */ 1252 win = create_dialog(16, COLS - 20, "Add New Entry"); 1253 if (!win) { 1254 ui_status("Failed to create dialog window"); 1255 return; 1256 } 1257 1258 /* Calculate maximum field width */ 1259 // max_field_width = COLS - input_x - 23; /* Leave some margin */ 1260 1261 /* Header */ 1262 wattron(win, A_BOLD); 1263 mvwprintw(win, 1, label_x, "New Entry"); 1264 wattroff(win, A_BOLD); 1265 1266 /* Print labels */ 1267 mvwprintw(win, input_y, label_x, "Path:"); 1268 wattron(win, A_DIM); 1269 mvwprintw(win, input_y + 1, input_x, "(category/name, e.g., 'email/gmail')"); 1270 wattroff(win, A_DIM); 1271 1272 mvwprintw(win, input_y + 3, label_x, "Title:"); 1273 wattron(win, A_DIM); 1274 mvwprintw(win, input_y + 4, input_x, "(descriptive name)"); 1275 wattroff(win, A_DIM); 1276 1277 mvwprintw(win, input_y + 6, label_x, "Username:"); 1278 wattron(win, A_DIM); 1279 mvwprintw(win, input_y + 7, input_x, "(optional)"); 1280 wattroff(win, A_DIM); 1281 1282 mvwprintw(win, input_y + 9, label_x, "Notes:"); 1283 wattron(win, A_DIM); 1284 mvwprintw(win, input_y + 10, input_x, "(optional)"); 1285 wattroff(win, A_DIM); 1286 1287 /* Get input with proper positioning */ 1288 echo(); 1289 1290 /* Path input */ 1291 wmove(win, input_y, input_x); 1292 wclrtoeol(win); 1293 wrefresh(win); 1294 wgetnstr(win, path, sizeof(path)-1); 1295 1296 /* Title input */ 1297 wmove(win, input_y + 3, input_x); 1298 wclrtoeol(win); 1299 wrefresh(win); 1300 wgetnstr(win, title, sizeof(title)-1); 1301 1302 /* Username input */ 1303 wmove(win, input_y + 6, input_x); 1304 wclrtoeol(win); 1305 wrefresh(win); 1306 wgetnstr(win, user, sizeof(user)-1); 1307 1308 /* Notes input */ 1309 wmove(win, input_y + 9, input_x); 1310 wclrtoeol(win); 1311 wrefresh(win); 1312 wgetnstr(win, notes, sizeof(notes)-1); 1313 1314 noecho(); 1315 1316 /* Password section */ 1317 mvwprintw(win, input_y + 12, label_x, "Generate random password? (y/n): "); 1318 wrefresh(win); 1319 1320 int c = wgetch(win); 1321 1322 if (c == 'y' || c == 'Y') { 1323 pass = genpass(PW_LEN); 1324 if (pass) { 1325 wclear(win); /* Clear the window first */ 1326 box(win, 0, 0); /* Redraw the box */ 1327 mvwprintw(win, 1, label_x, "Password Generated"); 1328 wattron(win, A_BOLD); 1329 mvwprintw(win, 3, label_x, "Password generated and copied to clipboard"); 1330 wattroff(win, A_BOLD); 1331 wrefresh(win); 1332 if (copyto_clipboard(pass) == 0) { 1333 ui_status("Generated password copied to clipboard"); 1334 } 1335 } 1336 } else { 1337 wclear(win); /* Clear window */ 1338 box(win, 0, 0); /* Redraw box */ 1339 mvwprintw(win, 1, label_x, "Password Entry"); 1340 wattron(win, A_DIM); 1341 mvwprintw(win, 3, label_x, "Enter password (will not be shown)"); 1342 wattroff(win, A_DIM); 1343 wrefresh(win); 1344 pass = readpass(""); 1345 } 1346 1347 /* Check input and create entry */ 1348 if (strlen(path) == 0 || strlen(title) == 0) { 1349 ui_status("Cancelled: empty path or title"); 1350 if (pass) { 1351 sodium_memzero(pass, strlen(pass)); 1352 free(pass); 1353 } 1354 delwin(win); 1355 touchwin(mainwin); 1356 touchwin(statuswin); 1357 refresh(); 1358 return; 1359 } 1360 1361 e = addentry(path, title); 1362 if (!e) { 1363 ui_status("Failed to create entry"); 1364 if (pass) { 1365 sodium_memzero(pass, strlen(pass)); 1366 free(pass); 1367 } 1368 delwin(win); 1369 touchwin(mainwin); 1370 touchwin(statuswin); 1371 refresh(); 1372 return; 1373 } 1374 1375 /* Set optional fields */ 1376 if (strlen(user) > 0) { 1377 e->user = strdup(user); 1378 } 1379 if (strlen(notes) > 0) { 1380 e->notes = strdup(notes); 1381 } 1382 1383 /* Handle password encryption */ 1384 if (pass && strlen(pass) > 0) { 1385 unsigned char *encrypted; 1386 size_t enclen; 1387 1388 if (encrypt_data((const unsigned char *)pass, strlen(pass), 1389 &encrypted, &enclen) != 0) { 1390 ui_status("Failed to encrypt password"); 1391 delentry(e); 1392 sodium_memzero(pass, strlen(pass)); 1393 free(pass); 1394 delwin(win); 1395 touchwin(mainwin); 1396 touchwin(statuswin); 1397 refresh(); 1398 return; 1399 } 1400 e->pass = (char *)encrypted; 1401 } 1402 1403 /* Cleanup */ 1404 if (pass) { 1405 sodium_memzero(pass, strlen(pass)); 1406 free(pass); 1407 } 1408 1409 e->modified = time(NULL); 1410 if (savedb() != 0) { 1411 ui_status("Failed to save database"); 1412 } else { 1413 ui_status("Entry added successfully"); 1414 } 1415 1416 /* Clean up window */ 1417 delwin(win); 1418 touchwin(mainwin); 1419 touchwin(statuswin); 1420 ui_draw(); 1421 } 1422 1423 static void 1424 cmd_generate(void) 1425 { 1426 char *pass; 1427 1428 if (!db.selected) 1429 return; 1430 1431 pass = genpass(PW_LEN); /* default length */ 1432 if (pass) { 1433 if (db.selected->pass) 1434 free(db.selected->pass); 1435 encrypt(pass, &db.selected->pass); 1436 copyto_clipboard(pass); 1437 sodium_memzero(pass, strlen(pass)); 1438 free(pass); 1439 db.selected->modified = time(NULL); 1440 savedb(); 1441 ui_status("Generated and copied to clipboard"); 1442 } 1443 } 1444 1445 static void 1446 cmd_copy(void) 1447 { 1448 char *plain = NULL; 1449 unsigned char *decrypted = NULL; 1450 size_t declen; 1451 1452 if (!db.selected || !db.selected->pass) { 1453 ui_status("No password to copy"); 1454 return; 1455 } 1456 1457 /* Decrypt the password */ 1458 if (decrypt_data((unsigned char *)db.selected->pass, strlen(db.selected->pass), 1459 &decrypted, &declen) != 0) { 1460 ui_status("Failed to decrypt password"); 1461 return; 1462 } 1463 1464 /* Convert to string and copy */ 1465 plain = (char *)decrypted; 1466 1467 if (copyto_clipboard(plain) == 0) { 1468 ui_status("Password copied to clipboard"); 1469 } else { 1470 ui_status("Failed to copy to clipboard"); 1471 } 1472 1473 /* Clean up */ 1474 sodium_memzero(plain, strlen(plain)); 1475 free(plain); 1476 } 1477 1478 static void 1479 cmd_search(void) 1480 { 1481 int c; 1482 int pos = 0; 1483 char searchbuf[256] = {0}; 1484 1485 werase(statuswin); 1486 box(statuswin, 0, 0); 1487 mvwprintw(statuswin, 1, 1, "Search (ESC to cancel): "); 1488 wrefresh(statuswin); 1489 1490 noecho(); 1491 cbreak(); 1492 1493 wmove(statuswin, 1, 23); 1494 1495 while ((c = wgetch(statuswin)) != '\n' && c != 27) { 1496 if (c == KEY_BACKSPACE || c == 127) { 1497 if (pos > 0) { 1498 pos--; 1499 searchbuf[pos] = '\0'; 1500 mvwaddch(statuswin, 1, 23 + pos, ' '); 1501 wmove(statuswin, 1, 23 + pos); 1502 } 1503 } else if (pos < sizeof(searchbuf) - 1) { 1504 searchbuf[pos] = c; 1505 searchbuf[pos + 1] = '\0'; 1506 mvwaddch(statuswin, 1, 23 + pos, c); 1507 pos++; 1508 } 1509 wrefresh(statuswin); 1510 } 1511 1512 if (c == 27) { /* ESC pressed */ 1513 db.search[0] = '\0'; 1514 db.view = VIEW_LIST; 1515 } else { 1516 strncpy(db.search, searchbuf, sizeof(db.search)-1); 1517 db.search[sizeof(db.search)-1] = '\0'; 1518 db.view = VIEW_SEARCH; 1519 } 1520 1521 werase(statuswin); 1522 box(statuswin, 0, 0); 1523 wrefresh(statuswin); 1524 } 1525 1526 static int 1527 loaddb(const char *path, const char *masterpass) 1528 { 1529 FILE *fp; 1530 char magic[sizeof(DB_MAGIC)]; 1531 uint32_t version; 1532 uint32_t count; 1533 Entry *e; 1534 size_t enclen; 1535 unsigned char *encrypted; 1536 unsigned char *decrypted; 1537 size_t declen; 1538 1539 1540 if (!(fp = fopen(path, "rb"))) { 1541 return -1; 1542 } 1543 1544 /* Read and verify magic */ 1545 if (fread(magic, 1, strlen(DB_MAGIC), fp) != strlen(DB_MAGIC) || 1546 memcmp(magic, DB_MAGIC, strlen(DB_MAGIC)) != 0) { 1547 fclose(fp); 1548 return -1; 1549 } 1550 1551 /* Read version */ 1552 if (fread(&version, sizeof(version), 1, fp) != 1) { 1553 fclose(fp); 1554 return -1; 1555 } 1556 1557 if (version != 2) { 1558 fclose(fp); 1559 return -1; 1560 } 1561 1562 /* Read salt and derive key */ 1563 if (fread(db.salt, 1, sizeof(db.salt), fp) != sizeof(db.salt)) { 1564 fclose(fp); 1565 return -1; 1566 } 1567 1568 if (crypto_pwhash(db.key, sizeof(db.key), 1569 masterpass, strlen(masterpass), 1570 db.salt, 1571 crypto_pwhash_OPSLIMIT_INTERACTIVE, 1572 crypto_pwhash_MEMLIMIT_INTERACTIVE, 1573 crypto_pwhash_ALG_DEFAULT) != 0) { 1574 fclose(fp); 1575 return -1; 1576 } 1577 1578 /* Read and verify encrypted block */ 1579 if (fread(&enclen, sizeof(enclen), 1, fp) != 1) { 1580 fclose(fp); 1581 return -1; 1582 } 1583 1584 encrypted = malloc(enclen); 1585 if (!encrypted) { 1586 fclose(fp); 1587 return -1; 1588 } 1589 1590 if (fread(encrypted, 1, enclen, fp) != enclen) { 1591 free(encrypted); 1592 fclose(fp); 1593 return -1; 1594 } 1595 1596 if (decrypt_data(encrypted, enclen, &decrypted, &declen) != 0) { 1597 free(encrypted); 1598 fclose(fp); 1599 return -2; /* Wrong password */ 1600 } 1601 1602 free(encrypted); 1603 1604 /* Verify decrypted content */ 1605 if (declen != strlen(VERIFY_STRING) || 1606 memcmp(decrypted, VERIFY_STRING, strlen(VERIFY_STRING)) != 0) { 1607 free(decrypted); 1608 fclose(fp); 1609 return -2; 1610 } 1611 1612 free(decrypted); 1613 1614 /* Read entry count */ 1615 if (fread(&count, sizeof(count), 1, fp) != 1) { 1616 fclose(fp); 1617 return -1; 1618 } 1619 1620 /* Read entries */ 1621 while (count-- > 0) { 1622 char *path = NULL, *title = NULL; 1623 int ret; 1624 1625 /* Read path */ 1626 ret = read_encrypted_string(fp, &path); 1627 if (ret != 0) { 1628 if (path) free(path); 1629 fclose(fp); 1630 return -1; 1631 } 1632 1633 /* Read title */ 1634 ret = read_encrypted_string(fp, &title); 1635 if (ret != 0) { 1636 if (path) free(path); 1637 if (title) free(title); 1638 fclose(fp); 1639 return -1; 1640 } 1641 1642 e = addentry(path, title); 1643 if (!e) { 1644 free(path); 1645 free(title); 1646 fclose(fp); 1647 return -1; 1648 } 1649 1650 free(path); 1651 free(title); 1652 1653 /* Read other fields */ 1654 if (read_encrypted_string(fp, &e->user) != 0) { 1655 fclose(fp); 1656 return -1; 1657 } 1658 1659 if (read_encrypted_string(fp, &e->pass) != 0) { 1660 fclose(fp); 1661 return -1; 1662 } 1663 1664 if (read_encrypted_string(fp, &e->notes) != 0) { 1665 fclose(fp); 1666 return -1; 1667 } 1668 1669 if (fread(&e->modified, sizeof(e->modified), 1, fp) != 1) { 1670 fclose(fp); 1671 return -1; 1672 } 1673 1674 } 1675 1676 fclose(fp); 1677 return 0; 1678 } 1679 1680 int 1681 main(int argc, char *argv[]) 1682 { 1683 Entry *e; 1684 char *pass; 1685 int ch, result; 1686 1687 /* Check arguments */ 1688 if (argc != 2) 1689 die("usage: %s dbfile", argv[0]); 1690 1691 /* Initialize sodium */ 1692 if (sodium_init() < 0) 1693 die("sodium_init failed"); 1694 1695 /* initialize ncurses */ 1696 initscr(); 1697 raw(); 1698 noecho(); 1699 keypad(stdscr, TRUE); 1700 1701 /* Initialize UI */ 1702 ui_init(); 1703 1704 /* Show initial status */ 1705 ui_status("Press '?' for help, 'q' to quit"); 1706 ui_draw(); 1707 1708 /* Handle database */ 1709 db.dbpath = argv[1]; 1710 if (access(db.dbpath, F_OK) == 0) { 1711 /* Existing database: authenticate */ 1712 for (int attempts = 3; attempts > 0; attempts--) { 1713 pass = readpass("Master Password: "); 1714 if (!pass) { 1715 ui_cleanup(); 1716 die("failed to read password"); 1717 } 1718 1719 result = loaddb(db.dbpath, pass); 1720 sodium_memzero(pass, strlen(pass)); 1721 free(pass); 1722 1723 if (result == 0) { 1724 break; 1725 } else if (result == -2) { 1726 if (attempts > 1) { 1727 ui_status("Wrong password. %d attempts remaining", attempts - 1); 1728 sleep(1); 1729 } else { 1730 ui_cleanup(); 1731 die("too many wrong password attempts"); 1732 } 1733 } else { 1734 ui_cleanup(); 1735 die("failed to load database"); 1736 } 1737 } 1738 } else { 1739 /* New database: create */ 1740 char *pass2; 1741 1742 pass = readpass("New Master Password: "); 1743 if (!pass) { 1744 ui_cleanup(); 1745 die("failed to read password"); 1746 } 1747 1748 pass2 = readpass("Confirm Master Password: "); 1749 if (!pass2) { 1750 sodium_memzero(pass, strlen(pass)); 1751 free(pass); 1752 ui_cleanup(); 1753 die("failed to read password confirmation"); 1754 } 1755 1756 if (strcmp(pass, pass2) != 0) { 1757 sodium_memzero(pass, strlen(pass)); 1758 sodium_memzero(pass2, strlen(pass2)); 1759 free(pass); 1760 free(pass2); 1761 ui_cleanup(); 1762 die("passwords do not match"); 1763 } 1764 1765 sodium_memzero(pass2, strlen(pass2)); 1766 free(pass2); 1767 1768 if (createdb(db.dbpath, pass) != 0) { 1769 sodium_memzero(pass, strlen(pass)); 1770 free(pass); 1771 ui_cleanup(); 1772 die("failed to create database"); 1773 } 1774 1775 sodium_memzero(pass, strlen(pass)); 1776 free(pass); 1777 } 1778 1779 /* Show initial status */ 1780 clear(); 1781 refresh(); 1782 ui_status("Press '?' for help, 'q' to quit"); 1783 ui_draw(); 1784 wrefresh(mainwin); 1785 wrefresh(statuswin); 1786 doupdate(); 1787 1788 /* Main loop */ 1789 while ((ch = getch()) != 'q') { 1790 if (ch == ERR) { 1791 if (errno == EINTR) 1792 continue; 1793 /* Only die on serious errors, not just ERR from getch */ 1794 if (errno != 0) { 1795 ui_cleanup(); 1796 die("input error: %s", strerror(errno)); 1797 } 1798 continue; 1799 } 1800 1801 switch (ch) { 1802 1803 case KEY_RESIZE: 1804 handle_resize(); 1805 break; 1806 1807 case '?': 1808 werase(mainwin); 1809 for (int i = 0; i < sizeof(help_text) / sizeof(help_text[0]); i++) { 1810 mvwprintw(mainwin, i + 2, 2, "%s", help_text[i]); 1811 } 1812 1813 wrefresh(mainwin); 1814 ui_status("Press any key to continue"); 1815 getch(); 1816 break; 1817 1818 case 27: /* ESC */ 1819 if (db.view == VIEW_SEARCH) { 1820 db.search[0] = '\0'; 1821 db.view = VIEW_LIST; 1822 1823 werase(statuswin); 1824 box(statuswin, 0, 0); 1825 wrefresh(statuswin); 1826 nodelay(stdscr, TRUE); /* Don't wait for second ESC byte */ 1827 getch(); /* Clear any pending ESC sequence */ 1828 nodelay(stdscr, FALSE); 1829 } 1830 break; 1831 1832 case 'j': 1833 if (db.view == VIEW_SEARCH) { 1834 Entry **matches = NULL; 1835 int match_count = 0; 1836 1837 /* Count and collect matches */ 1838 for (e = db.entries; e; e = e->next) { 1839 if (strstr(e->path, db.search) || 1840 strstr(e->title, db.search) || 1841 (e->user && strstr(e->user, db.search))) { 1842 match_count++; 1843 } 1844 } 1845 1846 if (match_count > 0) { 1847 matches = malloc(match_count * sizeof(Entry*)); 1848 if (matches) { 1849 int idx = 0; 1850 for (e = db.entries; e; e = e->next) { 1851 if (strstr(e->path, db.search) || 1852 strstr(e->title, db.search) || 1853 (e->user && strstr(e->user, db.search))) { 1854 matches[idx++] = e; 1855 } 1856 } 1857 1858 /* Find current selection and move to next */ 1859 for (int i = 0; i < match_count; i++) { 1860 if (matches[i] == db.selected && i < match_count - 1) { 1861 db.selected = matches[i + 1]; 1862 break; 1863 } 1864 } 1865 free(matches); 1866 } 1867 } 1868 } else if (db.selected && db.selected->next) { 1869 db.selected = db.selected->next; 1870 } else if (!db.selected && db.entries) { 1871 db.selected = db.entries; 1872 } 1873 break; 1874 1875 case 'k': 1876 if (db.view == VIEW_SEARCH) { 1877 Entry **matches = NULL; 1878 int match_count = 0; 1879 1880 /* Count and collect matches */ 1881 for (e = db.entries; e; e = e->next) { 1882 if (strstr(e->path, db.search) || 1883 strstr(e->title, db.search) || 1884 (e->user && strstr(e->user, db.search))) { 1885 match_count++; 1886 } 1887 } 1888 1889 if (match_count > 0) { 1890 matches = malloc(match_count * sizeof(Entry*)); 1891 if (matches) { 1892 int idx = 0; 1893 for (e = db.entries; e; e = e->next) { 1894 if (strstr(e->path, db.search) || 1895 strstr(e->title, db.search) || 1896 (e->user && strstr(e->user, db.search))) { 1897 matches[idx++] = e; 1898 } 1899 } 1900 1901 /* Find current selection and move to previous */ 1902 for (int i = 0; i < match_count; i++) { 1903 if (matches[i] == db.selected && i > 0) { 1904 db.selected = matches[i - 1]; 1905 break; 1906 } 1907 } 1908 free(matches); 1909 } 1910 } 1911 } else if (db.selected) { 1912 Entry *prev; 1913 for (prev = db.entries; prev && prev->next != db.selected; prev = prev->next) 1914 ; 1915 db.selected = prev; 1916 } 1917 break; 1918 1919 case 'a': /* add entry */ 1920 cmd_add(); 1921 break; 1922 1923 case 'd': /* delete entry */ 1924 if (db.selected) { 1925 char c; 1926 ui_status("Delete entry? (y/n)"); 1927 c = getch(); 1928 if (c == 'y' || c == 'Y') { 1929 Entry *next = db.selected->next; 1930 delentry(db.selected); 1931 db.selected = next; 1932 savedb(); 1933 ui_status("Entry deleted"); 1934 } else { 1935 ui_status("Deletion cancelled"); 1936 } 1937 } 1938 break; 1939 1940 case 'e': /* edit entry */ 1941 if (db.selected) { 1942 cmd_edit(); 1943 savedb(); 1944 } 1945 break; 1946 1947 case 'y': /* copy password */ 1948 cmd_copy(); 1949 break; 1950 1951 case 'g': /* generate password */ 1952 if (db.selected) { 1953 cmd_generate(); 1954 savedb(); 1955 } 1956 break; 1957 1958 case '/': /* search */ 1959 cmd_search(); 1960 break; 1961 1962 case '\t': /* toggle view */ 1963 if (db.view == VIEW_LIST && db.selected) 1964 db.view = VIEW_ENTRY; 1965 else 1966 db.view = VIEW_LIST; 1967 break; 1968 1969 case ERR: 1970 /* Handle input error */ 1971 if (errno == EINTR) 1972 continue; 1973 ui_cleanup(); 1974 die("input error: %s", strerror(errno)); 1975 break; 1976 } 1977 1978 /* Update display */ 1979 ui_draw(); 1980 } 1981 1982 /* Cleanup and exit */ 1983 ui_cleanup(); 1984 cleanup(); 1985 1986 /* Zero out sensitive memory */ 1987 sodium_memzero(&db, sizeof(db)); 1988 1989 return 0; 1990 }