spm

spm - simple password manager
Log | Files | Refs | LICENSE

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 }