spm

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

commit 2cc182b977305800e90ff9500200d227b506f19b
Author: SeMi <sebastian.michalk@protonmail.com>
Date:   Sat, 30 Nov 2024 22:12:28 +0100

initial commit

Diffstat:
ALICENSE | 21+++++++++++++++++++++
AMakefile | 38++++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.mk | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Areadme | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspm.c | 1991+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 2211 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 nyangkosense + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,38 @@ +# spm - simple password management + +include config.mk + +SRC = spm.c +OBJ = ${SRC:.c=.o} + +all: options spm + +options: + @echo slpass build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +.c.o: + ${CC} -c ${CFLAGS} $< + +${OBJ}: config.mk + +spm: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} + +clean: + rm -f spm ${OBJ} + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp -f spm ${DESTDIR}${PREFIX}/bin + chmod 755 ${DESTDIR}${PREFIX}/bin/spm +# mkdir -p ${DESTDIR}${MANPREFIX}/man1 +# chmod 644 ${DESTDIR}${MANPREFIX}/man1/slpass.1 + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/spm + rm -f ${DESTDIR}${MANPREFIX}/man1/spm.1 + +.PHONY: all options clean install uninstall diff --git a/config.def.h b/config.def.h @@ -0,0 +1,54 @@ +/* config.def.h */ +/* Color definitions */ +/* from curses.h */ +/* COLOR_BLACK 0 + COLOR_RED 1 + COLOR_GREEN 2 + COLOR_YELLOW 3 + COLOR_BLUE 4 + COLOR_MAGENTA 5 + COLOR_CYAN 6 + COLOR_WHITE 7 */ +/* + * init_color(COLOR_RED, 700, 0, 0); + * param 1 : color name + * param 2, 3, 4 : rgb content min = 0, max = 1000 */ + +static const int colors[][2] = { + /* fg bg */ + { COLOR_MAGENTA, COLOR_BLACK }, /* [0] status bar */ + { COLOR_BLACK, COLOR_GREEN }, /* [1] selected item */ + { COLOR_CYAN, COLOR_BLACK }, /* [2] dialog headers */ + { COLOR_MAGENTA, COLOR_BLACK }, /* [3] top bar headers */ + { COLOR_WHITE, COLOR_RED }, /* [3] top bar headers */ +}; + +/* Color pair indices */ +#define COLOR_STATUS_BAR 1 +#define COLOR_SELECTED 2 +#define COLOR_DIALOG_HEADER 3 +#define COLOR_TOP_BAR 4 +#define COLOR_UI_STATUS 5 + +/* password gen */ +#define PW_LEN 32 /* default pw length */ +/* charset for password gen */ +const char charset[] = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789!@#$%^&*()_+-=[]{}"; +/* misc */ +#define clr_clipboard 15 /* timer to clear clipboard in seconds */ +#define KEY_ESC 27 /* define esc key */ +/* help */ +const char *help_text[] = { + "Keys:", + "q - quit", + "j/k - move up/down", + "a - add entry", + "d - delete entry", + "e - edit entry", + "y - copy password to clipboard", + "g - generate new password", + "/ - search", + "Tab - toggle view", + "? - show this help"}; diff --git a/config.mk b/config.mk @@ -0,0 +1,54 @@ +# spm version +VERSION = 0.1 + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +# Linux/BSD common flags +CFLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os +LDFLAGS = -s +LIBS = -lncurses -lsodium + +# OS-specific flags +UNAME_S := $(shell uname -s) + +# Linux +ifeq ($(UNAME_S),Linux) + # Void Linux specific paths (default) + INCS = -I/usr/include + LIBS += -L/usr/lib +endif + +# OpenBSD +ifeq ($(UNAME_S),OpenBSD) + INCS = -I${PREFIX}/include + LIBS += -L${PREFIX}/lib +endif + +# FreeBSD +ifeq ($(UNAME_S),FreeBSD) + INCS = -I${PREFIX}/include + LIBS += -L${PREFIX}/lib +endif + +# NetBSD +ifeq ($(UNAME_S),NetBSD) + INCS = -I${PREFIX}/include + LIBS += -L${PREFIX}/lib +endif + +# Debug build +# Uncomment these for debugging +#CFLAGS = -std=c99 -pedantic -Wall -O0 -g +#LDFLAGS = -g + +# compiler and linker +CC = cc + +# Feature flags +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -DVERSION=\"${VERSION}\" + +# Final flags +CFLAGS += ${INCS} ${CPPFLAGS} +LDFLAGS += ${LIBS} diff --git a/readme b/readme @@ -0,0 +1,53 @@ +spm - simple password manager +================================ + +A terminal-based password manager using libsodium encryption + +Description +---------- +spm is a minimal password manager that: +- Stores encrypted passwords in a local database +- Uses ncurses for terminal UI +- Supports password generation +- Copies passwords to X11/Wayland clipboard +- Has no GUI or browser dependencies + +Requirements +----------- +In order to build spm, you need: + +- libsodium (encryption) +- ncurses (UI) +- xclip (X11) or wl-clipboard (Wayland) + +e.g. with apt: + apt install ncurses-dev libsodium-dev xclip + + +Installation +----------- + make + sudo make install + +Configuration +------------ +See config.def.h for configuration + +Usage +----- +Create database: + spm database + +Open existing: + spm database + +Keys: + j/k - navigate + a - add entry + d - delete entry + e - edit entry + y - copy password + g - generate password + / - search + Tab - toggle view + q - quit diff --git a/spm.c b/spm.c @@ -0,0 +1,1990 @@ +/* See LICENSE file for copyright and license details. */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <ncurses.h> +#include <sodium.h> +#include <termios.h> +#include <errno.h> +#include <time.h> +#include <sys/wait.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <stdarg.h> + +#include "config.def.h" + +/* arbitrary sizes */ +#define MAX_PATH 256 +#define MAX_PASS 2048 +#define XOR_KEY 0x42 + +/* macros */ +#define XSTR(x) STR(x) +#define STR(x) #x +#define LENGTH(X) (sizeof(X) / sizeof(X[0])) +#define DB_MAGIC get_magic() +#define VERIFY_STRING get_verify() +#define SALT_LEN crypto_pwhash_SALTBYTES +#define KEY_LEN crypto_secretbox_KEYBYTES +#define NONCE_LEN crypto_secretbox_NONCEBYTES +#define MAC_LEN crypto_secretbox_MACBYTES + +enum { + VIEW_LIST = 0, + VIEW_ENTRY, + VIEW_SEARCH, +}; + +/* types */ +typedef struct Entry { + char *path; + char *title; + char *user; + char *pass; + char *notes; + time_t modified; + struct Entry *next; +} Entry; + +typedef struct { + Entry *entries; + char *dbpath; + unsigned char key[KEY_LEN]; + unsigned char salt[SALT_LEN]; + int view; + Entry *selected; + char search[MAX_PATH]; +} DB; + +typedef struct { + const char *cmd; + void (*func)(void); +} Binding; + +/* globals */ +static DB db; +static WINDOW *mainwin; +static WINDOW *statuswin; + +static const unsigned char _magic[] = { + 'S' ^ XOR_KEY, 'L' ^ XOR_KEY, 'P' ^ XOR_KEY, 'A' ^ XOR_KEY, + 'S' ^ XOR_KEY, 'S' ^ XOR_KEY, '0' ^ XOR_KEY, '2' ^ XOR_KEY +}; + +static const unsigned char _verify[] = { + 'S' ^ XOR_KEY, 'L' ^ XOR_KEY, 'P' ^ XOR_KEY, 'A' ^ XOR_KEY, + 'S' ^ XOR_KEY, 'S' ^ XOR_KEY, 'O' ^ XOR_KEY, 'K' ^ XOR_KEY +}; + +/* function declarations */ +static void cleanup(void); +static int createdb(const char *path, const char *masterpass); +static const char *get_magic(void); +static const char *get_verify(void); +static int loaddb(const char *path, const char *masterpass); +static int savedb(void); +static Entry *addentry(const char *path, const char *title); +static void delentry(Entry *e); +static int encrypt(const char *plain, char **cipher); +static int encrypt_data(const unsigned char *data, size_t len, unsigned char **encrypted, size_t *outlen); +static int decrypt_data(const unsigned char *encrypted, size_t len, unsigned char **decrypted, size_t *outlen); +static int write_encrypted_string(FILE *fp, const char *str); +static int read_encrypted_string(FILE *fp, char **str); +static void die(const char *fmt, ...); +static void ui_init(void); +static void ui_cleanup(void); +static void ui_draw(void); +static void ui_status(const char *fmt, ...); +static void handle_resize(void); +static WINDOW *create_dialog(int height, int width, const char *title); +static void cmd_add(void); +static void cmd_edit(void); +static void cmd_copy(void); +static void cmd_generate(void); +static void cmd_search(void); +static char *readpass(const char *prompt); +static char *genpass(int len); +static int copyto_clipboard(const char *text); + +/* implementation */ +static void +delentry(Entry *e) +{ + Entry *prev; + + if (!e) + return; + + /* Find previous entry */ + if (e == db.entries) { + db.entries = e->next; + } else { + for (prev = db.entries; prev && prev->next != e; prev = prev->next) + ; + if (prev) + prev->next = e->next; + } + + /* Securely free all fields */ + if (e->path) { + sodium_memzero(e->path, strlen(e->path)); + free(e->path); + } + + if (e->title) { + sodium_memzero(e->title, strlen(e->title)); + free(e->title); + } + + if (e->user) { + sodium_memzero(e->user, strlen(e->user)); + free(e->user); + } + + if (e->pass) { + sodium_memzero(e->pass, strlen(e->pass)); + free(e->pass); + } + + if (e->notes) { + sodium_memzero(e->notes, strlen(e->notes)); + free(e->notes); + } + + /* Zero out the entry structure before freeing */ + sodium_memzero(e, sizeof(Entry)); + free(e); +} + +static int +encrypt_data(const unsigned char *data, size_t len, unsigned char **encrypted, size_t *outlen) +{ + unsigned char nonce[NONCE_LEN]; + size_t clen = MAC_LEN + len; + unsigned char *c; + char *b64; + size_t b64len; + + /* Allocate buffer for encrypted data + nonce */ + c = malloc(NONCE_LEN + clen); + if (!c) return -1; + + /* Generate random nonce */ + randombytes_buf(nonce, sizeof(nonce)); + memcpy(c, nonce, NONCE_LEN); + + /* Encrypt */ + if (crypto_secretbox_easy(c + NONCE_LEN, data, len, nonce, db.key) != 0) { + free(c); + return -1; + } + + /* Convert to base64 */ + b64len = sodium_base64_encoded_len(NONCE_LEN + clen, sodium_base64_VARIANT_ORIGINAL); + b64 = malloc(b64len); + if (!b64) { + free(c); + return -1; + } + + sodium_bin2base64(b64, b64len, + c, NONCE_LEN + clen, + sodium_base64_VARIANT_ORIGINAL); + + *encrypted = (unsigned char *)b64; + *outlen = strlen(b64); + + free(c); + return 0; +} + +static int +decrypt_data(const unsigned char *encrypted, size_t len, unsigned char **decrypted, size_t *outlen) +{ + unsigned char *bin; + size_t bin_len; + unsigned char *p; + int ret = -1; + + /* Decode base64 */ + bin = malloc(len); /* Will be smaller than base64 */ + if (!bin) return -1; + + if (sodium_base642bin(bin, len, + (const char *)encrypted, len, + NULL, &bin_len, + NULL, + sodium_base64_VARIANT_ORIGINAL) != 0) { + free(bin); + return -1; + } + + if (bin_len <= NONCE_LEN + MAC_LEN) { + free(bin); + return -1; + } + + /* Allocate output buffer */ + *outlen = bin_len - NONCE_LEN - MAC_LEN; + p = malloc(*outlen + 1); + if (!p) { + free(bin); + return -1; + } + + /* Decrypt */ + if (crypto_secretbox_open_easy(p, + bin + NONCE_LEN, + bin_len - NONCE_LEN, + bin, /* nonce at start */ + db.key) == 0) { + p[*outlen] = '\0'; + *decrypted = p; + ret = 0; + } else { + free(p); + } + + free(bin); + return ret; +} + +static int +write_encrypted_string(FILE *fp, const char *str) +{ + unsigned char *encrypted; + size_t enclen; + uint32_t len; + + if (!str) { + len = 0; + fwrite(&len, sizeof(len), 1, fp); + return 0; + } + + if (encrypt_data((unsigned char *)str, strlen(str), &encrypted, &enclen) != 0) + return -1; + + len = enclen; + if (fwrite(&len, sizeof(len), 1, fp) != 1) { + free(encrypted); + return -1; + } + + if (fwrite(encrypted, 1, enclen, fp) != enclen) { + free(encrypted); + return -1; + } + + free(encrypted); + return 0; +} + +static int +read_encrypted_string(FILE *fp, char **str) +{ + uint32_t len; + unsigned char *encrypted; + unsigned char *decrypted; + size_t declen; + + *str = NULL; /* Initialize to NULL */ + + if (fread(&len, sizeof(len), 1, fp) != 1) { + return -1; + } + + if (len == 0) { + return 0; + } + + encrypted = malloc(len); + if (!encrypted) { + return -1; + } + + if (fread(encrypted, 1, len, fp) != len) { + free(encrypted); + return -1; + } + + if (decrypt_data(encrypted, len, &decrypted, &declen) != 0) { + free(encrypted); + return -1; + } + + free(encrypted); + *str = (char *)decrypted; + return 0; +} + +static void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, "\n"); + exit(1); +} + +static void +cleanup(void) +{ + Entry *e, *next; + + for (e = db.entries; e != NULL; e = next) { + next = e->next; + if (e->path) free(e->path); + if (e->title) free(e->title); + if (e->user) free(e->user); + if (e->pass) { + sodium_memzero(e->pass, strlen(e->pass)); + free(e->pass); + } + if (e->notes) free(e->notes); + free(e); + } + + sodium_memzero(&db, sizeof(db)); +} + +static int +savedb(void) +{ + FILE *fp; + Entry *e; + uint32_t count = 0; + uint32_t version = 2; + unsigned char *encrypted; + size_t enclen; + + if (!(fp = fopen(db.dbpath, "wb"))) + return -1; + + /* Write magic and version */ + fwrite(DB_MAGIC, 1, strlen(DB_MAGIC), fp); + fwrite(&version, sizeof(version), 1, fp); + + /* Write salt */ + fwrite(db.salt, 1, sizeof(db.salt), fp); + + /* Create verification block with constant string */ + if (encrypt_data((const unsigned char *)VERIFY_STRING, + strlen(VERIFY_STRING), + &encrypted, &enclen) != 0) { + fclose(fp); + return -1; + } + + /* Write verification block */ + fwrite(&enclen, sizeof(enclen), 1, fp); + fwrite(encrypted, 1, enclen, fp); + free(encrypted); + + /* Count entries */ + for (e = db.entries; e; e = e->next) + count++; + + /* Write entry count */ + fwrite(&count, sizeof(count), 1, fp); + + /* Write each entry */ + for (e = db.entries; e; e = e->next) { + if (write_encrypted_string(fp, e->path) != 0 || + write_encrypted_string(fp, e->title) != 0 || + write_encrypted_string(fp, e->user) != 0 || + write_encrypted_string(fp, e->pass) != 0 || + write_encrypted_string(fp, e->notes) != 0) { + fclose(fp); + return -1; + } + fwrite(&e->modified, sizeof(e->modified), 1, fp); + } + + fclose(fp); + return 0; +} + +static const char * +get_magic(void) +{ + static char magic[sizeof(_magic) + 1]; + for (size_t i = 0; i < sizeof(_magic); i++) + magic[i] = _magic[i] ^ XOR_KEY; + magic[sizeof(_magic)] = '\0'; + return magic; +} + +static const char * +get_verify(void) +{ + static char verify[sizeof(_verify) + 1]; + for (size_t i = 0; i < sizeof(_verify); i++) + verify[i] = _verify[i] ^ XOR_KEY; + verify[sizeof(_verify)] = '\0'; + return verify; +} + +static int +createdb(const char *path, const char *masterpass) +{ + FILE *fp; + uint32_t version = 2; + unsigned char *encrypted; + size_t enclen; + + if (access(path, F_OK) == 0) + return -1; + + if (!(fp = fopen(path, "wb"))) + return -1; + + if (chmod(path, S_IRUSR|S_IWUSR) != 0){ + fclose(fp); + unlink(path); + return -1; + } + + /* Write magic and version */ + fwrite(DB_MAGIC, 1, strlen(DB_MAGIC), fp); + fwrite(&version, sizeof(version), 1, fp); + + /* Generate new salt and derive key */ + randombytes_buf(db.salt, sizeof(db.salt)); + if (crypto_pwhash(db.key, sizeof(db.key), + masterpass, strlen(masterpass), + db.salt, + crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE, + crypto_pwhash_ALG_DEFAULT) != 0) { + fclose(fp); + return -1; + } + + /* Write salt */ + fwrite(db.salt, 1, sizeof(db.salt), fp); + + if (encrypt_data((const unsigned char *)VERIFY_STRING, + strlen(VERIFY_STRING), + &encrypted, &enclen) != 0) { + fclose(fp); + return -1; + } + + fwrite(&enclen, sizeof(enclen), 1, fp); + fwrite(encrypted, 1, enclen, fp); + free(encrypted); + + uint32_t count = 0; + fwrite(&count, sizeof(count), 1, fp); + + fclose(fp); + return 0; +} + +static Entry * +addentry(const char *path, const char *title) +{ + Entry *e; + + e = calloc(1, sizeof(Entry)); + if (!e) + return NULL; + + e->path = strdup(path); + e->title = strdup(title); + e->next = db.entries; + db.entries = e; + + return e; +} + +static void +cmd_edit(void) +{ + WINDOW *win; + char buf[MAX_PATH]; + char *pass = NULL; + int y = 1; + int c; + + if (!db.selected) + return; + + /* Create centered dialog window */ + win = create_dialog(14, COLS - 20, "Edit Entry"); + if (!win) { + ui_status("Failed to create dialog window"); + return; + } + + /* Header with instructions */ + wattron(win, A_BOLD); + mvwprintw(win, y++, 2, "Editing Entry"); + wattroff(win, A_BOLD); + wattron(win, A_DIM); + mvwprintw(win, y++, 2, "(Press Enter to keep current value)"); + wattroff(win, A_DIM); + y++; /* Add space */ + + keypad(win, TRUE); /* Enable special keys */ + + /* Path input */ + wattron(win, A_BOLD); + mvwprintw(win, y, 2, "Path: "); + wattroff(win, A_BOLD); + mvwprintw(win, y, 8, "[%s]", db.selected->path ? db.selected->path : ""); + wmove(win, y++, 8); + wclrtoeol(win); + nodelay(win, TRUE); + echo(); + + /* Check for ESC */ + c = wgetch(win); + if (c == 27) { + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_status("Edit cancelled"); + return; + } + if (c != ERR) ungetch(c); + nodelay(win, FALSE); + + wgetnstr(win, buf, sizeof(buf)); + if (strlen(buf)) { + free(db.selected->path); + db.selected->path = strdup(buf); + } + + /* Title input */ + wattron(win, A_BOLD); + mvwprintw(win, y, 2, "Title: "); + wattroff(win, A_BOLD); + mvwprintw(win, y, 9, "[%s]", db.selected->title ? db.selected->title : ""); + wmove(win, y++, 9); + wclrtoeol(win); + nodelay(win, TRUE); + + c = wgetch(win); + if (c == 27) { + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_status("Edit cancelled"); + return; + } + if (c != ERR) ungetch(c); + nodelay(win, FALSE); + + wgetnstr(win, buf, sizeof(buf)); + if (strlen(buf)) { + free(db.selected->title); + db.selected->title = strdup(buf); + } + + /* Username input */ + wattron(win, A_BOLD); + mvwprintw(win, y, 2, "Username: "); + wattroff(win, A_BOLD); + mvwprintw(win, y, 12, "[%s]", db.selected->user ? db.selected->user : ""); + wmove(win, y++, 12); + wclrtoeol(win); + nodelay(win, TRUE); + + c = wgetch(win); + if (c == 27) { + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_status("Edit cancelled"); + return; + } + if (c != ERR) ungetch(c); + nodelay(win, FALSE); + + wgetnstr(win, buf, sizeof(buf)); + if (strlen(buf)) { + free(db.selected->user); + db.selected->user = strdup(buf); + } + + /* Notes input */ + wattron(win, A_BOLD); + mvwprintw(win, y, 2, "Notes: "); + wattroff(win, A_BOLD); + mvwprintw(win, y, 9, "[%s]", db.selected->notes ? db.selected->notes : ""); + wmove(win, y++, 9); + wclrtoeol(win); + nodelay(win, TRUE); + + c = wgetch(win); + if (c == 27) { + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_status("Edit cancelled"); + return; + } + if (c != ERR) ungetch(c); + nodelay(win, FALSE); + + wgetnstr(win, buf, sizeof(buf)); + if (strlen(buf)) { + free(db.selected->notes); + db.selected->notes = strdup(buf); + } + + /* Password handling */ + y++; + wattron(win, A_BOLD); + mvwprintw(win, y++, 2, "Change password? (y/n/ESC)"); + wattroff(win, A_BOLD); + noecho(); + wrefresh(win); + c = wgetch(win); + + if (c == 27) { /* ESC */ + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_status("Edit cancelled"); + return; + } + + if (c == 'y' || c == 'Y') { + wattron(win, A_BOLD); + mvwprintw(win, y++, 2, "Generate new random password? (y/n/ESC)"); + wattroff(win, A_BOLD); + wrefresh(win); + c = wgetch(win); + + if (c == 27) { /* ESC */ + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_status("Edit cancelled"); + return; + } + + if (c == 'y' || c == 'Y') { + /* Generate new password */ + pass = genpass(PW_LEN); + if (pass) { + wattron(win, A_BOLD); + mvwprintw(win, y++, 2, "New password generated and copied to clipboard"); + wattroff(win, A_BOLD); + wrefresh(win); + if (copyto_clipboard(pass) == 0) { + ui_status("Generated password copied to clipboard"); + } + } + } else { + /* Manual password entry */ + wattron(win, A_BOLD); + mvwprintw(win, y++, 2, "Enter new password"); + wattroff(win, A_BOLD); + wattron(win, A_DIM); + mvwprintw(win, y++, 2, "(Password will not be shown while typing)"); + wattroff(win, A_DIM); + wrefresh(win); + pass = readpass(""); + + if (!pass) { /* ESC was pressed in readpass */ + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_status("Edit cancelled"); + return; + } + } + + /* Update password if provided */ + if (pass && strlen(pass) > 0) { + unsigned char *encrypted; + size_t enclen; + + /* Clean up old password */ + if (db.selected->pass) { + sodium_memzero(db.selected->pass, strlen(db.selected->pass)); + free(db.selected->pass); + db.selected->pass = NULL; + } + + /* Encrypt and store new password */ + if (encrypt_data((const unsigned char *)pass, strlen(pass), + &encrypted, &enclen) != 0) { + ui_status("Failed to encrypt password"); + sodium_memzero(pass, strlen(pass)); + free(pass); + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + refresh(); + return; + } + db.selected->pass = (char *)encrypted; + } + } + + /* Cleanup sensitive data */ + if (pass) { + sodium_memzero(pass, strlen(pass)); + free(pass); + } + + /* Update modification time and save */ + db.selected->modified = time(NULL); + if (savedb() != 0) { + ui_status("Failed to save database"); + } else { + ui_status("Entry updated successfully"); + } + + /* Clean up window */ + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_draw(); +} + +static char * +readpass(const char *prompt) +{ + char *pass = NULL; + WINDOW *win; + + /* Create a new window for password input */ + win = create_dialog(5, COLS-8, "Password Input"); + + /* Show prompt and info */ + mvwprintw(win, 1, 2, "%s", prompt); + wattron(win, A_DIM); /* Dimmed text for info message */ + mvwprintw(win, 2, 2, "(Password will not be shown while typing)"); + wattroff(win, A_DIM); + wrefresh(win); + + /* Read password */ + pass = malloc(MAX_PASS); + if (pass) { + noecho(); /* Ensure password isn't shown */ + curs_set(1); /* Show cursor while typing */ + wmove(win, 1, 2 + strlen(prompt)); + wgetnstr(win, pass, MAX_PASS-1); + curs_set(0); /* Hide cursor again */ + } + + /* Clean up */ + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + wrefresh(mainwin); + wrefresh(statuswin); + + return pass; +} + +static void +ui_status(const char *fmt, ...) +{ + va_list ap; + + werase(statuswin); + wmove(statuswin, 0, 0); + wattron(statuswin, COLOR_PAIR(COLOR_UI_STATUS)); + va_start(ap, fmt); + vw_printw(statuswin, fmt, ap); + va_end(ap); + wattroff(statuswin, COLOR_PAIR(COLOR_UI_STATUS)); + wrefresh(statuswin); +} + +static int +encrypt(const char *plain, char **cipher) +{ + unsigned char nonce[crypto_secretbox_NONCEBYTES]; + unsigned char *c; + unsigned char *message = (unsigned char *)plain; + char *b64; + size_t clen = crypto_secretbox_MACBYTES + strlen(plain); + size_t mlen = strlen(plain); + size_t b64len; + + /* Allocate buffer for encrypted data + nonce */ + c = malloc(crypto_secretbox_NONCEBYTES + clen); + if (!c) return -1; + + /* Generate random nonce */ + randombytes_buf(nonce, sizeof(nonce)); + memcpy(c, nonce, sizeof(nonce)); + + /* Encrypt */ + if (crypto_secretbox_easy(c + crypto_secretbox_NONCEBYTES, + message, mlen, + nonce, db.key) != 0) { + free(c); + return -1; + } + + /* Convert to base64 */ + b64len = sodium_base64_encoded_len(crypto_secretbox_NONCEBYTES + clen, + sodium_base64_VARIANT_ORIGINAL); + b64 = malloc(b64len); + if (!b64) { + free(c); + return -1; + } + + sodium_bin2base64(b64, b64len, + c, crypto_secretbox_NONCEBYTES + clen, + sodium_base64_VARIANT_ORIGINAL); + + *cipher = b64; + free(c); + return 0; +} + +static char * +genpass(int len) +{ + + char *pass; + int i; + + if (len < 1) + return NULL; + + pass = malloc(len + 1); + if (!pass) + return NULL; + + for (i = 0; i < len; i++) + pass[i] = charset[randombytes_uniform(sizeof(charset) - 1)]; + pass[len] = '\0'; + + return pass; +} + +static void +handle_resize(void) +{ + endwin(); + refresh(); + clear(); + + /* Recreate windows with new size */ + if (mainwin) + delwin(mainwin); + if (statuswin) + delwin(statuswin); + + mainwin = newwin(LINES - 3, COLS, 0, 0); + statuswin = newwin(3, COLS, LINES - 3, 0); + + /* Reset window properties */ + keypad(mainwin, TRUE); + scrollok(mainwin, TRUE); + wbkgd(statuswin, COLOR_PAIR(COLOR_STATUS_BAR)); + box(statuswin, 0, 0); + + /* Redraw everything */ + ui_draw(); + refresh(); + wrefresh(mainwin); + wrefresh(statuswin); +} + +static WINDOW * +create_dialog(int height, int width, const char *title) +{ + WINDOW *win; + int starty, startx; + + /* Ensure minimum size and maximum size */ + height = height < 3 ? 3 : height; + width = width < 20 ? 20 : width; + + /* Don't exceed screen size */ + if (height > LINES - 2) + height = LINES - 2; + if (width > COLS - 2) + width = COLS - 2; + + /* Center the window */ + starty = (LINES - height) / 2; + startx = (COLS - width) / 2; + + /* Create window with border */ + win = newwin(height, width, starty, startx); + box(win, 0, 0); + + /* Add title if provided */ + if (title) { + int title_len = strlen(title); + if (title_len > width - 4) + title_len = width - 4; + wattron(win, COLOR_PAIR(COLOR_DIALOG_HEADER)); + + mvwprintw(win, 0, (width - title_len - 2) / 2, " %.*s ", title_len, title); + wattroff(win, COLOR_PAIR(COLOR_DIALOG_HEADER)); + } + + /* Ensure the window is visible */ + wrefresh(win); + return win; +} + +static int +copyto_clipboard(const char *text) +{ + FILE *pipe; + size_t text_len; + const char *cmd = NULL; + pid_t pid; + + if (!text) + return -1; + + text_len = strlen(text); + + if (getenv("WAYLAND_DISPLAY")) { + cmd = "wl-copy"; + } else if (getenv("DISPLAY")) { + cmd = "xclip -selection clipboard"; + } else { + ui_status("No clipboard available (neither Wayland nor X11)"); + return -1; + } + + /* Copy to clipboard */ + pipe = popen(cmd, "w"); + if (!pipe) { + return -1; + } + if (fwrite(text, 1, text_len, pipe) != text_len) { + pclose(pipe); + return -1; + } + if (pclose(pipe) != 0) { + return -1; + } + + /* Fork process for clipboard clearing */ + if ((pid = fork()) == 0) { + /* Child process */ + sleep(clr_clipboard); + pipe = popen(cmd, "w"); + if (pipe) { + fwrite("", 1, 0, pipe); + pclose(pipe); + } + _exit(0); + } + + return 0; +} + +static void +ui_init(void) +{ + + /* Initialize colors */ + start_color(); + for (size_t i = 0; i < LENGTH(colors); i++) { + init_pair(i + 1, colors[i][0], colors[i][1]); + } + + /* Create main windows with borders */ + mainwin = newwin(LINES - 3, COLS, 0, 0); + statuswin = newwin(3, COLS, LINES - 3, 0); + + /* Enable keypad and scrolling */ + keypad(mainwin, TRUE); + scrollok(mainwin, TRUE); + + /* Set up status window */ + wbkgd(statuswin, COLOR_PAIR(COLOR_STATUS_BAR)); + box(statuswin, 0, 0); +} + +static void +ui_cleanup(void) +{ + if (mainwin) delwin(mainwin); + if (statuswin) delwin(statuswin); + endwin(); +} + +static void +ui_draw(void) +{ + Entry *e; + int y = 0; + char timestr[32]; + struct tm *tm; + int available_width = COLS; + /* Declare all column widths at function start */ + int time_width = 19; /* Fixed width for timestamp */ + int path_width; /* Will be set in each view */ + int title_width; + int user_width; + + werase(mainwin); + + switch (db.view) { + + case VIEW_LIST: + /* Calculate column widths */ + time_width = 19; /* Fixed width for timestamp */ + path_width = (available_width - time_width - 6) / 3; + title_width = path_width; + user_width = available_width - time_width - path_width - title_width - 6; + + /* Draw header without separators */ + wattron(mainwin, COLOR_PAIR(COLOR_TOP_BAR) | A_BOLD); + mvwhline(mainwin, 0, 0, ' ', available_width); // Clear the line first with spaces + + /* Draw headers with proper spacing */ + mvwprintw(mainwin, 0, 0, "%-*s", time_width, "Modified"); + mvwprintw(mainwin, 0, time_width + 2, "%-*s", path_width, "Category"); + mvwprintw(mainwin, 0, time_width + path_width + 4, "%-*s", title_width, "Title"); + mvwprintw(mainwin, 0, time_width + path_width + title_width + 6, "%-*s", user_width, "Username"); + + wattroff(mainwin, COLOR_PAIR(COLOR_TOP_BAR) | A_BOLD); + + /* Draw header background */ +// wattron(mainwin, A_REVERSE); +// mvwhline(mainwin, 0, 0, ' ', available_width); + + /* Draw centered headers */ +// wattron(mainwin, COLOR_PAIR(COLOR_TOP_BAR) | A_BOLD); /* Use your defined color pair */ +// mvwprintw(mainwin, 0, 0, "%*s%*s", +// mod_len, "Modified", mod_pad, ""); +// mvwprintw(mainwin, 0, time_width + 1, "%*s%*s", +// path_len, "Path", path_pad, ""); +// mvwprintw(mainwin, 0, time_width + path_width + 2, "%*s%*s", +// title_len, "Title", title_pad, ""); +// mvwprintw(mainwin, 0, time_width + path_width + title_width + 3, "%*s%*s", +// user_len, "Username", user_pad, ""); +// wattroff(mainwin, COLOR_PAIR(COLOR_TOP_BAR) | A_BOLD); +// wattroff(mainwin, A_REVERSE); + + /* Separator line */ + // wattron(mainwin, A_DIM); + // mvwaddch(mainwin, 1, time_width, ACS_VLINE); + // mvwaddch(mainwin, 1, time_width + path_width + 1, ACS_VLINE); + // mvwaddch(mainwin, 1, time_width + path_width + title_width + 2, ACS_VLINE); + // mvwhline(mainwin, 1, 0, ACS_HLINE, available_width); + // wattroff(mainwin, A_DIM); + + /* Entries (left-aligned) */ + y = 2; + for (e = db.entries; e && y < LINES-2; e = e->next) { + if (e == db.selected) + + wattron(mainwin, COLOR_PAIR(COLOR_SELECTED)); + + tm = localtime(&e->modified); + strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M", tm); + + /* Left align entries */ + mvwprintw(mainwin, y, 0, "%-*s", time_width, timestr); + waddch(mainwin, ' '); + mvwprintw(mainwin, y, time_width + 1, "%-*s", path_width, e->path ? e->path : ""); + waddch(mainwin, ' '); + mvwprintw(mainwin, y, time_width + path_width + 2, "%-*s", title_width, e->title ? e->title : ""); + waddch(mainwin, ' '); + mvwprintw(mainwin, y, time_width + path_width + title_width + 3, "%-*s", user_width, e->user ? e->user : ""); + + if (e == db.selected) + wattroff(mainwin, COLOR_PAIR(COLOR_SELECTED)); + + y++; + } + break; + + case VIEW_ENTRY: + /* Detail view implementation remains the same */ + if (db.selected) { + tm = localtime(&db.selected->modified); + strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M", tm); + + wattron(mainwin, A_BOLD); + mvwprintw(mainwin, y++, 0, "Details"); + wattroff(mainwin, A_BOLD); + mvwhline(mainwin, y++, 0, ACS_HLINE, available_width); + + mvwprintw(mainwin, y++, 0, "Modified: %s", timestr); + mvwprintw(mainwin, y++, 0, "Path: %s", db.selected->path ? db.selected->path : ""); + mvwprintw(mainwin, y++, 0, "Title: %s", db.selected->title ? db.selected->title : ""); + mvwprintw(mainwin, y++, 0, "Username: %s", db.selected->user ? db.selected->user : ""); + mvwprintw(mainwin, y++, 0, "Password: [Hidden - Press 'y' to copy]"); + + if (db.selected->notes) { + mvwprintw(mainwin, y++, 0, "Notes: %s", db.selected->notes); + } + } + break; + +case VIEW_SEARCH: { + int matches = 0; + int i = 0; + int idx = 0; + Entry *first_match = NULL; + Entry **matched_entries = NULL; + + /* Calculate column widths */ + path_width = (available_width - time_width - 6) / 3; + title_width = path_width; + user_width = available_width - time_width - path_width - title_width - 6; + + /* Ensure the columns fit within available space */ + if (path_width + title_width + user_width + 6 > available_width) { + ui_status("Column widths exceed available space!"); + break; + } + + /* First collect all matches across all fields */ + for (e = db.entries; e; e = e->next) { + if (strstr(e->path, db.search) || + strstr(e->title, db.search) || + (e->user && strstr(e->user, db.search))) { + matches++; + } + } + + /* Create array of matches */ + if (matches > 0) { + matched_entries = malloc(matches * sizeof(Entry*)); + if (!matched_entries) { + ui_status("Memory allocation failed"); + break; + } + + /* Fill match array */ + for (e = db.entries; e; e = e->next) { + if (strstr(e->path, db.search) || + strstr(e->title, db.search) || + (e->user && strstr(e->user, db.search))) { + matched_entries[idx++] = e; + if (!first_match) first_match = e; + } + } + + /* Handle selection */ + if (matches == 1) { + db.selected = first_match; + } else if (!db.selected || + !(strstr(db.selected->path, db.search) || + strstr(db.selected->title, db.search) || + (db.selected->user && strstr(db.selected->user, db.search)))) { + db.selected = first_match; + } + } + + /* Draw header - center title */ + int title_length = snprintf(NULL, 0, "Search Results for \"%s\" (%d matches)", db.search, matches); + int title_start = (available_width - title_length) / 2; + wattron(mainwin, A_BOLD | A_REVERSE); + mvwhline(mainwin, 0, 0, ' ', available_width); + mvwprintw(mainwin, 0, title_start, "Search Results for \"%s\" (%d matches)", db.search, matches); + wattroff(mainwin, A_BOLD | A_REVERSE); + + /* Draw separator with field headers */ + wattron(mainwin, A_DIM); + mvwhline(mainwin, 1, 0, ACS_HLINE, available_width); + wattroff(mainwin, A_DIM); + + /* Draw matched entries */ + y = 2; + for (i = 0; i < matches && y < LINES-2; i++) { + e = matched_entries[i]; + + if (e == db.selected) + wattron(mainwin, COLOR_PAIR(COLOR_SELECTED)); + + /* Draw entry with all fields */ + mvwprintw(mainwin, y, 0, "%-*s", path_width, e->path ? e->path : ""); + mvwprintw(mainwin, y, path_width, "%-*s", title_width, e->title ? e->title : ""); + mvwprintw(mainwin, y, path_width + title_width, "%-*s", user_width, e->user ? e->user : ""); + + if (e == db.selected) + wattroff(mainwin, COLOR_PAIR(COLOR_SELECTED)); + + y++; + } + + /* Clean up */ + if (matched_entries) + free(matched_entries); + + break; + } +} + /* Status bar */ + wmove(statuswin, 1, 1); + wprintw(statuswin, " [a]dd [d]elete [e]dit [y]ank [/]search [tab]view [q]uit"); + wrefresh(mainwin); + wrefresh(statuswin); +} + +static void +cmd_add(void) +{ + WINDOW *win; + char path[MAX_PATH] = {0}; + char title[MAX_PATH] = {0}; + char user[MAX_PATH] = {0}; + char notes[MAX_PATH] = {0}; + char *pass = NULL; + Entry *e; + int input_y = 3; + int label_x = 2; /* Position for labels */ + int input_x = 15; /* Position for input fields */ +// int max_field_width; /* Maximum width for input fields */ + + /* Create centered dialog window */ + win = create_dialog(16, COLS - 20, "Add New Entry"); + if (!win) { + ui_status("Failed to create dialog window"); + return; + } + + /* Calculate maximum field width */ + // max_field_width = COLS - input_x - 23; /* Leave some margin */ + + /* Header */ + wattron(win, A_BOLD); + mvwprintw(win, 1, label_x, "New Entry"); + wattroff(win, A_BOLD); + + /* Print labels */ + mvwprintw(win, input_y, label_x, "Path:"); + wattron(win, A_DIM); + mvwprintw(win, input_y + 1, input_x, "(category/name, e.g., 'email/gmail')"); + wattroff(win, A_DIM); + + mvwprintw(win, input_y + 3, label_x, "Title:"); + wattron(win, A_DIM); + mvwprintw(win, input_y + 4, input_x, "(descriptive name)"); + wattroff(win, A_DIM); + + mvwprintw(win, input_y + 6, label_x, "Username:"); + wattron(win, A_DIM); + mvwprintw(win, input_y + 7, input_x, "(optional)"); + wattroff(win, A_DIM); + + mvwprintw(win, input_y + 9, label_x, "Notes:"); + wattron(win, A_DIM); + mvwprintw(win, input_y + 10, input_x, "(optional)"); + wattroff(win, A_DIM); + + /* Get input with proper positioning */ + echo(); + + /* Path input */ + wmove(win, input_y, input_x); + wclrtoeol(win); + wrefresh(win); + wgetnstr(win, path, sizeof(path)-1); + + /* Title input */ + wmove(win, input_y + 3, input_x); + wclrtoeol(win); + wrefresh(win); + wgetnstr(win, title, sizeof(title)-1); + + /* Username input */ + wmove(win, input_y + 6, input_x); + wclrtoeol(win); + wrefresh(win); + wgetnstr(win, user, sizeof(user)-1); + + /* Notes input */ + wmove(win, input_y + 9, input_x); + wclrtoeol(win); + wrefresh(win); + wgetnstr(win, notes, sizeof(notes)-1); + + noecho(); + + /* Password section */ + mvwprintw(win, input_y + 12, label_x, "Generate random password? (y/n): "); + wrefresh(win); + + int c = wgetch(win); + + if (c == 'y' || c == 'Y') { + pass = genpass(PW_LEN); + if (pass) { + wclear(win); /* Clear the window first */ + box(win, 0, 0); /* Redraw the box */ + mvwprintw(win, 1, label_x, "Password Generated"); + wattron(win, A_BOLD); + mvwprintw(win, 3, label_x, "Password generated and copied to clipboard"); + wattroff(win, A_BOLD); + wrefresh(win); + if (copyto_clipboard(pass) == 0) { + ui_status("Generated password copied to clipboard"); + } + } + } else { + wclear(win); /* Clear window */ + box(win, 0, 0); /* Redraw box */ + mvwprintw(win, 1, label_x, "Password Entry"); + wattron(win, A_DIM); + mvwprintw(win, 3, label_x, "Enter password (will not be shown)"); + wattroff(win, A_DIM); + wrefresh(win); + pass = readpass(""); + } + + /* Check input and create entry */ + if (strlen(path) == 0 || strlen(title) == 0) { + ui_status("Cancelled: empty path or title"); + if (pass) { + sodium_memzero(pass, strlen(pass)); + free(pass); + } + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + refresh(); + return; + } + + e = addentry(path, title); + if (!e) { + ui_status("Failed to create entry"); + if (pass) { + sodium_memzero(pass, strlen(pass)); + free(pass); + } + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + refresh(); + return; + } + + /* Set optional fields */ + if (strlen(user) > 0) { + e->user = strdup(user); + } + if (strlen(notes) > 0) { + e->notes = strdup(notes); + } + + /* Handle password encryption */ + if (pass && strlen(pass) > 0) { + unsigned char *encrypted; + size_t enclen; + + if (encrypt_data((const unsigned char *)pass, strlen(pass), + &encrypted, &enclen) != 0) { + ui_status("Failed to encrypt password"); + delentry(e); + sodium_memzero(pass, strlen(pass)); + free(pass); + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + refresh(); + return; + } + e->pass = (char *)encrypted; + } + + /* Cleanup */ + if (pass) { + sodium_memzero(pass, strlen(pass)); + free(pass); + } + + e->modified = time(NULL); + if (savedb() != 0) { + ui_status("Failed to save database"); + } else { + ui_status("Entry added successfully"); + } + + /* Clean up window */ + delwin(win); + touchwin(mainwin); + touchwin(statuswin); + ui_draw(); +} + +static void +cmd_generate(void) +{ + char *pass; + + if (!db.selected) + return; + + pass = genpass(PW_LEN); /* default length */ + if (pass) { + if (db.selected->pass) + free(db.selected->pass); + encrypt(pass, &db.selected->pass); + copyto_clipboard(pass); + sodium_memzero(pass, strlen(pass)); + free(pass); + db.selected->modified = time(NULL); + savedb(); + ui_status("Generated and copied to clipboard"); + } +} + +static void +cmd_copy(void) +{ + char *plain = NULL; + unsigned char *decrypted = NULL; + size_t declen; + + if (!db.selected || !db.selected->pass) { + ui_status("No password to copy"); + return; + } + + /* Decrypt the password */ + if (decrypt_data((unsigned char *)db.selected->pass, strlen(db.selected->pass), + &decrypted, &declen) != 0) { + ui_status("Failed to decrypt password"); + return; + } + + /* Convert to string and copy */ + plain = (char *)decrypted; + + if (copyto_clipboard(plain) == 0) { + ui_status("Password copied to clipboard"); + } else { + ui_status("Failed to copy to clipboard"); + } + + /* Clean up */ + sodium_memzero(plain, strlen(plain)); + free(plain); +} + +static void +cmd_search(void) +{ + int c; + int pos = 0; + char searchbuf[256] = {0}; + + werase(statuswin); + box(statuswin, 0, 0); + mvwprintw(statuswin, 1, 1, "Search (ESC to cancel): "); + wrefresh(statuswin); + + noecho(); + cbreak(); + + wmove(statuswin, 1, 23); + + while ((c = wgetch(statuswin)) != '\n' && c != 27) { + if (c == KEY_BACKSPACE || c == 127) { + if (pos > 0) { + pos--; + searchbuf[pos] = '\0'; + mvwaddch(statuswin, 1, 23 + pos, ' '); + wmove(statuswin, 1, 23 + pos); + } + } else if (pos < sizeof(searchbuf) - 1) { + searchbuf[pos] = c; + searchbuf[pos + 1] = '\0'; + mvwaddch(statuswin, 1, 23 + pos, c); + pos++; + } + wrefresh(statuswin); + } + + if (c == 27) { /* ESC pressed */ + db.search[0] = '\0'; + db.view = VIEW_LIST; + } else { + strncpy(db.search, searchbuf, sizeof(db.search)-1); + db.search[sizeof(db.search)-1] = '\0'; + db.view = VIEW_SEARCH; + } + + werase(statuswin); + box(statuswin, 0, 0); + wrefresh(statuswin); +} + +static int +loaddb(const char *path, const char *masterpass) +{ + FILE *fp; + char magic[sizeof(DB_MAGIC)]; + uint32_t version; + uint32_t count; + Entry *e; + size_t enclen; + unsigned char *encrypted; + unsigned char *decrypted; + size_t declen; + + + if (!(fp = fopen(path, "rb"))) { + return -1; + } + + /* Read and verify magic */ + if (fread(magic, 1, strlen(DB_MAGIC), fp) != strlen(DB_MAGIC) || + memcmp(magic, DB_MAGIC, strlen(DB_MAGIC)) != 0) { + fclose(fp); + return -1; + } + + /* Read version */ + if (fread(&version, sizeof(version), 1, fp) != 1) { + fclose(fp); + return -1; + } + + if (version != 2) { + fclose(fp); + return -1; + } + + /* Read salt and derive key */ + if (fread(db.salt, 1, sizeof(db.salt), fp) != sizeof(db.salt)) { + fclose(fp); + return -1; + } + + if (crypto_pwhash(db.key, sizeof(db.key), + masterpass, strlen(masterpass), + db.salt, + crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE, + crypto_pwhash_ALG_DEFAULT) != 0) { + fclose(fp); + return -1; + } + + /* Read and verify encrypted block */ + if (fread(&enclen, sizeof(enclen), 1, fp) != 1) { + fclose(fp); + return -1; + } + + encrypted = malloc(enclen); + if (!encrypted) { + fclose(fp); + return -1; + } + + if (fread(encrypted, 1, enclen, fp) != enclen) { + free(encrypted); + fclose(fp); + return -1; + } + + if (decrypt_data(encrypted, enclen, &decrypted, &declen) != 0) { + free(encrypted); + fclose(fp); + return -2; /* Wrong password */ + } + + free(encrypted); + + /* Verify decrypted content */ + if (declen != strlen(VERIFY_STRING) || + memcmp(decrypted, VERIFY_STRING, strlen(VERIFY_STRING)) != 0) { + free(decrypted); + fclose(fp); + return -2; + } + + free(decrypted); + + /* Read entry count */ + if (fread(&count, sizeof(count), 1, fp) != 1) { + fclose(fp); + return -1; + } + + /* Read entries */ + while (count-- > 0) { + char *path = NULL, *title = NULL; + int ret; + + /* Read path */ + ret = read_encrypted_string(fp, &path); + if (ret != 0) { + if (path) free(path); + fclose(fp); + return -1; + } + + /* Read title */ + ret = read_encrypted_string(fp, &title); + if (ret != 0) { + if (path) free(path); + if (title) free(title); + fclose(fp); + return -1; + } + + e = addentry(path, title); + if (!e) { + free(path); + free(title); + fclose(fp); + return -1; + } + + free(path); + free(title); + + /* Read other fields */ + if (read_encrypted_string(fp, &e->user) != 0) { + fclose(fp); + return -1; + } + + if (read_encrypted_string(fp, &e->pass) != 0) { + fclose(fp); + return -1; + } + + if (read_encrypted_string(fp, &e->notes) != 0) { + fclose(fp); + return -1; + } + + if (fread(&e->modified, sizeof(e->modified), 1, fp) != 1) { + fclose(fp); + return -1; + } + + } + + fclose(fp); + return 0; +} + +int +main(int argc, char *argv[]) +{ + Entry *e; + char *pass; + int ch, result; + + /* Check arguments */ + if (argc != 2) + die("usage: %s dbfile", argv[0]); + + /* Initialize sodium */ + if (sodium_init() < 0) + die("sodium_init failed"); + + /* initialize ncurses */ + initscr(); + raw(); + noecho(); + keypad(stdscr, TRUE); + + /* Initialize UI */ + ui_init(); + + /* Show initial status */ + ui_status("Press '?' for help, 'q' to quit"); + ui_draw(); + + /* Handle database */ + db.dbpath = argv[1]; + if (access(db.dbpath, F_OK) == 0) { + /* Existing database: authenticate */ + for (int attempts = 3; attempts > 0; attempts--) { + pass = readpass("Master Password: "); + if (!pass) { + ui_cleanup(); + die("failed to read password"); + } + + result = loaddb(db.dbpath, pass); + sodium_memzero(pass, strlen(pass)); + free(pass); + + if (result == 0) { + break; + } else if (result == -2) { + if (attempts > 1) { + ui_status("Wrong password. %d attempts remaining", attempts - 1); + sleep(1); + } else { + ui_cleanup(); + die("too many wrong password attempts"); + } + } else { + ui_cleanup(); + die("failed to load database"); + } + } + } else { + /* New database: create */ + char *pass2; + + pass = readpass("New Master Password: "); + if (!pass) { + ui_cleanup(); + die("failed to read password"); + } + + pass2 = readpass("Confirm Master Password: "); + if (!pass2) { + sodium_memzero(pass, strlen(pass)); + free(pass); + ui_cleanup(); + die("failed to read password confirmation"); + } + + if (strcmp(pass, pass2) != 0) { + sodium_memzero(pass, strlen(pass)); + sodium_memzero(pass2, strlen(pass2)); + free(pass); + free(pass2); + ui_cleanup(); + die("passwords do not match"); + } + + sodium_memzero(pass2, strlen(pass2)); + free(pass2); + + if (createdb(db.dbpath, pass) != 0) { + sodium_memzero(pass, strlen(pass)); + free(pass); + ui_cleanup(); + die("failed to create database"); + } + + sodium_memzero(pass, strlen(pass)); + free(pass); + } + + /* Show initial status */ + clear(); + refresh(); + ui_status("Press '?' for help, 'q' to quit"); + ui_draw(); + wrefresh(mainwin); + wrefresh(statuswin); + doupdate(); + + /* Main loop */ + while ((ch = getch()) != 'q') { + if (ch == ERR) { + if (errno == EINTR) + continue; + /* Only die on serious errors, not just ERR from getch */ + if (errno != 0) { + ui_cleanup(); + die("input error: %s", strerror(errno)); + } + continue; + } + + switch (ch) { + + case KEY_RESIZE: + handle_resize(); + break; + + case '?': + werase(mainwin); + for (int i = 0; i < sizeof(help_text) / sizeof(help_text[0]); i++) { + mvwprintw(mainwin, i + 2, 2, "%s", help_text[i]); + } + + wrefresh(mainwin); + ui_status("Press any key to continue"); + getch(); + break; + + case 27: /* ESC */ + if (db.view == VIEW_SEARCH) { + db.search[0] = '\0'; + db.view = VIEW_LIST; + + werase(statuswin); + box(statuswin, 0, 0); + wrefresh(statuswin); + nodelay(stdscr, TRUE); /* Don't wait for second ESC byte */ + getch(); /* Clear any pending ESC sequence */ + nodelay(stdscr, FALSE); + } + break; + +case 'j': + if (db.view == VIEW_SEARCH) { + Entry **matches = NULL; + int match_count = 0; + + /* Count and collect matches */ + for (e = db.entries; e; e = e->next) { + if (strstr(e->path, db.search) || + strstr(e->title, db.search) || + (e->user && strstr(e->user, db.search))) { + match_count++; + } + } + + if (match_count > 0) { + matches = malloc(match_count * sizeof(Entry*)); + if (matches) { + int idx = 0; + for (e = db.entries; e; e = e->next) { + if (strstr(e->path, db.search) || + strstr(e->title, db.search) || + (e->user && strstr(e->user, db.search))) { + matches[idx++] = e; + } + } + + /* Find current selection and move to next */ + for (int i = 0; i < match_count; i++) { + if (matches[i] == db.selected && i < match_count - 1) { + db.selected = matches[i + 1]; + break; + } + } + free(matches); + } + } + } else if (db.selected && db.selected->next) { + db.selected = db.selected->next; + } else if (!db.selected && db.entries) { + db.selected = db.entries; + } + break; + +case 'k': + if (db.view == VIEW_SEARCH) { + Entry **matches = NULL; + int match_count = 0; + + /* Count and collect matches */ + for (e = db.entries; e; e = e->next) { + if (strstr(e->path, db.search) || + strstr(e->title, db.search) || + (e->user && strstr(e->user, db.search))) { + match_count++; + } + } + + if (match_count > 0) { + matches = malloc(match_count * sizeof(Entry*)); + if (matches) { + int idx = 0; + for (e = db.entries; e; e = e->next) { + if (strstr(e->path, db.search) || + strstr(e->title, db.search) || + (e->user && strstr(e->user, db.search))) { + matches[idx++] = e; + } + } + + /* Find current selection and move to previous */ + for (int i = 0; i < match_count; i++) { + if (matches[i] == db.selected && i > 0) { + db.selected = matches[i - 1]; + break; + } + } + free(matches); + } + } + } else if (db.selected) { + Entry *prev; + for (prev = db.entries; prev && prev->next != db.selected; prev = prev->next) + ; + db.selected = prev; + } + break; + + case 'a': /* add entry */ + cmd_add(); + break; + + case 'd': /* delete entry */ + if (db.selected) { + char c; + ui_status("Delete entry? (y/n)"); + c = getch(); + if (c == 'y' || c == 'Y') { + Entry *next = db.selected->next; + delentry(db.selected); + db.selected = next; + savedb(); + ui_status("Entry deleted"); + } else { + ui_status("Deletion cancelled"); + } + } + break; + + case 'e': /* edit entry */ + if (db.selected) { + cmd_edit(); + savedb(); + } + break; + + case 'y': /* copy password */ + cmd_copy(); + break; + + case 'g': /* generate password */ + if (db.selected) { + cmd_generate(); + savedb(); + } + break; + + case '/': /* search */ + cmd_search(); + break; + + case '\t': /* toggle view */ + if (db.view == VIEW_LIST && db.selected) + db.view = VIEW_ENTRY; + else + db.view = VIEW_LIST; + break; + + case ERR: + /* Handle input error */ + if (errno == EINTR) + continue; + ui_cleanup(); + die("input error: %s", strerror(errno)); + break; + } + + /* Update display */ + ui_draw(); + } + + /* Cleanup and exit */ + ui_cleanup(); + cleanup(); + + /* Zero out sensitive memory */ + sodium_memzero(&db, sizeof(db)); + + return 0; +} +\ No newline at end of file