/*
* ckpass.c
*
* Main program and support functions.
*
* This file is part of the ckpass project.
*
* Copyright (C) 2009 Heath N. Caldwell <hncaldwell@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <ncurses.h>
#include <locale.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <kpass.h>
#include "ckpass.h"
#include "bindings.h"
#include "forms.h"
int main(int argc, char **argv)
{
kpass_db *db;
kpass_db *new_db;
int press;
int active_win;
int first_group = 0;
int highlighted_group = 0;
int selected_group = 0;
int entries_in_selected_group = 0;
int first_entry = 0;
int highlighted_entry = 0;
int max_x, max_y;
bool reveal = FALSE;
char db_filename[MAX_FILENAME_LENGTH];
char new_db_filename[MAX_FILENAME_LENGTH];
struct binding *b;
struct kpass_entry *entry;
bool file_from_arg = FALSE; /* True if using database filename from command line argument. */
int result;
setlocale(LC_ALL, "C");
initscr();
start_color();
cbreak();
noecho();
//nonl();
//intrflush(stdscr, FALSE);
curs_set(0);
keypad(stdscr, TRUE);
active_win = WIN_GROUPS;
init_bindings();
db = malloc(sizeof(kpass_db));
if(argc > 1) {
strncpy(db_filename, argv[1], MAX_FILENAME_LENGTH);
file_from_arg = TRUE;
}
do {
/* If it was set by command line argument, try it the first time. If
* the user cancels password prompt, read a new filename with form. */
if(!file_from_arg) {
if(open_database_form(db_filename, MAX_FILENAME_LENGTH)) goto quit;
}
file_from_arg = FALSE;
result = open_db(db, db_filename);
if(result < 0) kpass_free_db(db); /* It was initialized, but password cancelled. */
} while(result);
init_windows(db);
draw_groups(db, first_group, highlighted_group, TRUE);
draw_entries(db, highlighted_group, first_entry, highlighted_entry, FALSE, FALSE);
draw_top_bar(db_filename);
draw_bottom_bar(active_win);
while(1) {
press = wgetch(active_win == WIN_GROUPS ? _groups_win : _entries_win);
if(press == KEY_RESIZE) {
init_windows(db);
draw_groups(db, first_group, highlighted_group, active_win == WIN_GROUPS ? TRUE : FALSE);
draw_entries(db, highlighted_group, first_entry, highlighted_entry, active_win == WIN_ENTRIES ? TRUE : FALSE, FALSE);
draw_bottom_bar(active_win);
continue;
}
if(active_win == WIN_GROUPS) {
for(b = _groups_bindings; b->key && b->key != press; b++);
if(!b->key) continue;
if(b->command == C_PREV) {
if(highlighted_group > 0) {
if(highlighted_group == first_group) first_group--;
highlighted_group--;
}
draw_groups(db, first_group, highlighted_group, TRUE);
draw_entries(db, highlighted_group, first_entry, highlighted_entry, FALSE, FALSE);
} else if(b->command == C_NEXT) {
if(highlighted_group < db->groups_len - 1) {
getmaxyx(_groups_win, max_y, max_x);
if(highlighted_group == first_group + max_y - 1) first_group++;
highlighted_group++;
}
draw_groups(db, first_group, highlighted_group, TRUE);
draw_entries(db, highlighted_group, first_entry, highlighted_entry, FALSE, FALSE);
} else if(b->command == C_SELECT) {
selected_group = highlighted_group;
first_entry = 0;
highlighted_entry = 0;
entries_in_selected_group = entries_in_group(db, selected_group);
active_win = WIN_ENTRIES;
draw_groups(db, first_group, highlighted_group, FALSE);
draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
draw_bottom_bar(active_win);
} else if(b->command == C_OPEN) {
if(!open_database_form(new_db_filename, MAX_FILENAME_LENGTH)) {
new_db = malloc(sizeof(kpass_db));
result = open_db(new_db, new_db_filename);
if(!result) {
kpass_free_db(db);
free(db);
db = new_db;
strncpy(db_filename, new_db_filename, MAX_FILENAME_LENGTH);
first_group = 0;
selected_group = 0;
highlighted_group = 0;
first_entry = 0;
highlighted_entry = 0;
} else if(result < 0) {
kpass_free_db(new_db);
free(new_db);
} else {
free(new_db);
}
}
init_windows(db);
draw_groups(db, first_group, highlighted_group, TRUE);
draw_entries(db, highlighted_group, first_entry, highlighted_entry, FALSE, FALSE);
draw_top_bar(db_filename);
draw_bottom_bar(active_win);
} else if(b->command == C_QUIT) {
goto quit;
}
} else if(active_win == WIN_ENTRIES) {
for(b = _entries_bindings; b->key && b->key != press; b++);
if(!b->key) continue;
if(b->command == C_REVEAL) {
reveal = !reveal;
draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, reveal);
continue;
} else {
reveal = FALSE;
}
if(b->command == C_PREV) {
if(highlighted_entry > 0) {
if(highlighted_entry == first_entry) first_entry--;
highlighted_entry--;
}
draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
reveal = FALSE;
} else if(b->command == C_NEXT) {
if(highlighted_entry < entries_in_selected_group - 1) {
getmaxyx(_entries_win, max_y, max_x);
if(highlighted_entry == first_entry + max_y - 1) first_entry++;
highlighted_entry++;
}
draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
reveal = FALSE;
} else if(b->command == C_GROUPS) {
active_win = WIN_GROUPS;
first_entry = 0;
highlighted_entry = 0;
draw_entries(db, selected_group, first_entry, highlighted_entry, FALSE, FALSE);
draw_groups(db, first_group, highlighted_group, TRUE);
draw_bottom_bar(active_win);
} else if(b->command == C_EDIT) {
entry_form(db, nth_entry_in_group(db, selected_group, highlighted_entry));
/* This redraw needs to be revisited. */
draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
draw_groups(db, first_group, highlighted_group, FALSE);
} else if(b->command == C_XCLIP) {
entry = nth_entry_in_group(db, selected_group, highlighted_entry);
pipeout("/usr/bin/xclip", entry->password);
} else if(b->command == C_OPEN) {
if(!open_database_form(new_db_filename, MAX_FILENAME_LENGTH)) {
new_db = malloc(sizeof(kpass_db));
result = open_db(new_db, new_db_filename);
if(!result) {
kpass_free_db(db);
free(db);
db = new_db;
strncpy(db_filename, new_db_filename, MAX_FILENAME_LENGTH);
active_win = WIN_GROUPS;
first_group = 0;
selected_group = 0;
highlighted_group = 0;
first_entry = 0;
highlighted_entry = 0;
} else if(result < 0) {
kpass_free_db(new_db);
free(new_db);
} else {
free(new_db);
}
}
init_windows(db);
if(new_db == db) {
draw_entries(db, selected_group, first_entry, highlighted_entry, FALSE, FALSE);
draw_groups(db, first_group, highlighted_group, TRUE);
} else {
draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
draw_groups(db, first_group, highlighted_group, FALSE);
}
draw_top_bar(db_filename);
draw_bottom_bar(active_win);
} else if(b->command == C_QUIT) {
goto quit;
}
}
}
quit:
endwin();
free(_groups_bindings);
free(_entries_bindings);
kpass_free_db(db);
free(db);
return 0;
}
void draw_top_bar(const char *s)
{
int max_x, max_y;
wmove(_top_bar, 0, 0);
waddstr(_top_bar, s);
getmaxyx(_top_bar, max_y, max_x);
whline(_top_bar, ' ', max_x);
wrefresh(_top_bar);
}
void draw_bottom_bar(int mode)
{
int max_x, max_y;
struct binding *b;
wmove(_bottom_bar, 0, 0);
if(mode == WIN_GROUPS || mode == WIN_ENTRIES) {
b = mode == WIN_GROUPS ? _groups_bindings : _entries_bindings;
for(; b->key; b++) {
switch(b->key) {
case '\t': waddstr(_bottom_bar, "[tab]"); break;
case '\n': waddstr(_bottom_bar, "[return]"); break;
case ' ': waddstr(_bottom_bar, "[space]"); break;
case KEY_UP: continue;
case KEY_DOWN: continue;
default: waddch(_bottom_bar, b->key); break;
}
if((b+1)->key && (b+1)->command == b->command) {
waddch(_bottom_bar, ',');
continue;
}
waddch(_bottom_bar, ':');
waddstr(_bottom_bar, b->command);
waddstr(_bottom_bar, " ");
}
}
getmaxyx(_bottom_bar, max_y, max_x);
whline(_bottom_bar, ' ', max_x);
wrefresh(_bottom_bar);
}
void fatal(const char *message)
{
endwin();
fprintf(stderr, "%s\n", message);
exit(1);
}
int open_db(kpass_db *db, const char *filename)
{
char password[MAX_PASSWORD_LENGTH];
bool decrypt_success = FALSE;
int retval = 0;
clear();
refresh();
if(read_db(db, filename)) {
error_dialog("Couldn't read.");
clear();
refresh();
return 1;
}
while(!decrypt_success) {
if(password_form(password, MAX_PASSWORD_LENGTH)) {
retval = -1;
clear();
refresh();
break;
}
if(decrypt_db(db, password)) {
error_dialog("Password does not match.");
} else {
decrypt_success = TRUE;
}
clear();
refresh();
}
// Clear password since we don't need it anymore.
memset(password, 0, MAX_PASSWORD_LENGTH);
return retval;
}
int read_db(kpass_db *db, const char *filename)
{
uint8_t *data;
kpass_retval retval;
struct stat sb;
int fd;
fd = open(filename, O_RDONLY);
if(fd < 0) return errno;
if(fstat(fd, &sb) < 0) return errno;
data = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
if(data == MAP_FAILED) return errno;
retval = kpass_init_db(db, data, sb.st_size);
if(retval) return -1;
munmap(data, sb.st_size);
return 0;
}
int decrypt_db(kpass_db *db, const char *password)
{
kpass_retval retval;
unsigned char pw_hash[32];
kpass_hash_pw(password, pw_hash);
retval = kpass_decrypt_db(db, pw_hash);
if(retval) return -1;
return 0;
}
void draw_groups(const kpass_db *db, int first, int highlighted, bool foreground)
{
int max_x, max_y;
int i, j;
int y;
int highlight_attrib;
getmaxyx(_groups_win, max_y, max_x);
highlight_attrib = foreground ? A_REVERSE : A_NORMAL;
for(i = first, y = 0; i < db->groups_len; i++, y++) {
wattrset(_groups_win, i == highlighted ? highlight_attrib : A_NORMAL);
if(db->groups[i]->level > 0) {
if(y < max_y) {
wmove(_groups_win, y, 0);
whline(_groups_win, ' ', 3*(db->groups[i]->level - 1));
wmove(_groups_win, y, 3*(db->groups[i]->level - 1));
waddch(_groups_win, ACS_LLCORNER);
waddch(_groups_win, '-');
waddch(_groups_win, '>');
}
for(j = i-1; j >= 0 && j >= first && db->groups[j]->level >= db->groups[i]->level; j--) {
if(y - (i-j) >= max_y) continue;
wmove(_groups_win, y - (i-j), 3*(db->groups[i]->level - 1));
wattrset(_groups_win, j == highlighted ? highlight_attrib : A_NORMAL);
if(db->groups[j]->level == db->groups[i]->level) {
waddch(_groups_win, ACS_LTEE);
} else {
waddch(_groups_win, ACS_VLINE);
}
}
if(y < max_y) {
wattrset(_groups_win, i == highlighted ? highlight_attrib : A_NORMAL);
wmove(_groups_win, y, 3*(db->groups[i]->level));
}
} else {
if(y < max_y) wmove(_groups_win, y, 0);
}
if(y < max_y) {
waddstr(_groups_win, db->groups[i]->name);
whline(_groups_win, ' ', max_x);
if(i == highlighted) mvwaddch(_groups_win, y, max_x - 2, '*');
}
}
wattrset(_groups_win, A_NORMAL);
/* Blank the rest of the window. */
for(; y < max_y; y++) {
mvwhline(_groups_win, y, 0, ' ', max_x);
}
wrefresh(_groups_win);
}
void draw_entries(const kpass_db *db, int group, int first, int highlighted, bool foreground, bool reveal)
{
int max_x;
int max_y;
int i;
int c; /* Accumulator for count of entries in group. */
int x, y;
getmaxyx(_entries_win, max_y, max_x);
for(i = 0, y = 0, c = 0; i < db->entries_len && y < max_y; i++) {
if(db->entries[i]->group_id != db->groups[group]->id) continue;
if(c < first) {
/* Increase count since we did encounter one, we just aren't showing it. */
c++;
continue;
}
if(foreground) wattrset(_entries_win, c == highlighted ? A_REVERSE : A_NORMAL);
mvwhline(_entries_win, y, 0, ' ', max_x); /* clear line first */
x = 0;
mvwaddnstr(_entries_win, y, x, db->entries[i]->title, _entry_widths[0] - 1);
if(strlen(db->entries[i]->title) > _entry_widths[0] - 1 ||
x + MIN(strlen(db->entries[i]->title), _entry_widths[0]) >= max_x)
mvwaddch(_entries_win,
y, MIN(x + _entry_widths[0] - 2, max_x - 1),
'+' | A_REVERSE);
x += _entry_widths[0];
mvwaddnstr(_entries_win, y, x, db->entries[i]->username, _entry_widths[1] - 1);
if(strlen(db->entries[i]->username) > _entry_widths[1] - 1 ||
x + MIN(strlen(db->entries[i]->username), _entry_widths[1]) >= max_x)
mvwaddch(_entries_win,
y, MIN(x + _entry_widths[1] - 2, max_x - 1),
'+' | A_REVERSE);
x += _entry_widths[1];
if(reveal && c == highlighted) {
mvwaddnstr(_entries_win, y, x, db->entries[i]->password, _entry_widths[2] - 1);
if(strlen(db->entries[i]->password) > _entry_widths[2] - 1 ||
x + MIN(strlen(db->entries[i]->password), _entry_widths[2]) >= max_x)
mvwaddch(_entries_win,
y, MIN(x + _entry_widths[2] - 2, max_x - 1),
'+' | A_REVERSE);
} else {
mvwaddstr(_entries_win, y, x, "********");
}
x += _entry_widths[2];
mvwaddnstr(_entries_win, y, x, db->entries[i]->url, _entry_widths[3]);
if(strlen(db->entries[i]->url) > _entry_widths[3] ||
x + MIN(strlen(db->entries[i]->url), _entry_widths[3]) >= max_x)
mvwaddch(_entries_win,
y, MIN(x + _entry_widths[3] - 1, max_x - 1),
'+' | A_REVERSE);
y++;
c++;
}
wattrset(_entries_win, A_NORMAL);
/* Blank the rest of the window. */
for(; y < max_y; y++) {
mvwhline(_entries_win, y, 0, ' ', max_x);
}
wrefresh(_entries_win);
}
int entries_in_group(const kpass_db *db, int group)
{
int i;
int n = 0;
for(i=0; i < db->entries_len; i++) {
if(db->entries[i]->group_id == db->groups[group]->id) n++;
}
return n;
}
struct kpass_entry *nth_entry_in_group(const kpass_db *db, int group, int n)
{
int i;
/* Add 1 since n is expected to start at 0
* (first listed entry for the group is called 0). */
n++;
for(i=0; i < db->entries_len; i++) {
if(db->entries[i]->group_id == db->groups[group]->id) n--;
if(!n) return db->entries[i];
}
return 0;
}
int find_groups_win_width(const kpass_db *db)
{
int i;
int n = 0;
for(i=0; i < db->groups_len; i++) {
if(strlen(db->groups[i]->name) + 3*(db->groups[i]->level) > n)
n = strlen(db->groups[i]->name) + 3*(db->groups[i]->level);
}
/* Longest line plus borders plus " * ". */
return n + 5;
}
void init_windows(const kpass_db *db)
{
int max_y, max_x;
int x;
int groups_win_width;
if(_groups_super_win) delwin(_groups_super_win);
if(_groups_win) delwin(_groups_super_win);
if(_entries_super_win) delwin(_entries_super_win);
if(_entries_win) delwin(_entries_super_win);
if(_top_bar) delwin(_top_bar);
if(_bottom_bar) delwin(_bottom_bar);
getmaxyx(stdscr, max_y, max_x);
groups_win_width = find_groups_win_width(db);
_groups_super_win = newwin(max_y - 2, groups_win_width, 1, 0);
_groups_win = derwin(_groups_super_win, max_y - 6, groups_win_width - 2, 3, 1);
_entries_super_win = newwin(max_y - 2, max_x - groups_win_width, 1, groups_win_width);
_entries_win = derwin(_entries_super_win, max_y - 6, max_x - groups_win_width - 2, 3, 1);
_top_bar = newwin(1, max_x, 0, 0);
_bottom_bar = newwin(1, max_x, max_y - 1, 0);
keypad(_groups_win, TRUE);
box(_groups_super_win, 0, 0);
mvwaddch(_groups_super_win, 2, 0, ACS_LTEE);
mvwhline(_groups_super_win, 2, 1, ACS_HLINE, groups_win_width - 2);
mvwaddch(_groups_super_win, 2, groups_win_width - 1, ACS_RTEE);
mvwaddstr(_groups_super_win, 1, 1, "Groups");
keypad(_entries_win, TRUE);
box(_entries_super_win, 0, 0);
mvwaddch(_entries_super_win, 2, 0, ACS_LTEE);
mvwhline(_entries_super_win, 2, 1, ACS_HLINE, max_x - groups_win_width - 2);
mvwaddch(_entries_super_win, 2, max_x - groups_win_width - 1, ACS_RTEE);
x = 1;
mvwaddstr(_entries_super_win, 1, 1, "Title");
x += _entry_widths[0];
mvwaddstr(_entries_super_win, 1, x, "Username");
x += _entry_widths[1];
mvwaddstr(_entries_super_win, 1, x, "Password");
x += _entry_widths[2];
mvwaddstr(_entries_super_win, 1, x, "URL");
wrefresh(_groups_super_win);
wrefresh(_entries_super_win);
}
void error_dialog(char *s) {
WINDOW *win;
int press;
int max_x, max_y;
int rows, cols;
getmaxyx(stdscr, max_y, max_x);
rows = 7;
cols = strlen(s) + 4;
win = newwin(rows, cols, (max_y - rows) / 2, (max_x - cols) / 2);
keypad(win, TRUE);
box(win, 0, 0);
mvwaddstr(win, 2, 2, s);
wattrset(win, A_REVERSE);
mvwaddstr(win, 4, cols / 2 - 2, "[OK]");
wattrset(win, A_NORMAL);
wrefresh(win);
while(wgetch(win) != '\n');
delwin(win);
}
int pipeout(const char *command, const char *s)
{
int pipefd[2];
pid_t pid;
if(pipe(pipefd) < 0) return -1;
pid = fork();
if(pid < 0) return -1;
if(pid) {
/* parent */
close(pipefd[0]); /* Close read end. */
write(pipefd[1], s, strlen(s));
close(pipefd[1]);
wait(0); /* Wait for child. */
} else {
/* child */
close(pipefd[1]); /* Close write end. */
dup2(pipefd[0], 0); /* Dup read end to stdin. */
execl(command, command, 0);
/* I don't really think these are necessary, but just in case. */
close(pipefd[0]);
exit(0);
}
return 0;
}
void init_bindings()
{
_groups_bindings = new_binding_set();
add_binding(&_groups_bindings, 'o', C_OPEN);
add_binding(&_groups_bindings, 's', C_SAVE);
add_binding(&_groups_bindings, 'a', C_ADD_GROUP);
add_binding(&_groups_bindings, KEY_UP, C_PREV);
add_binding(&_groups_bindings, KEY_DOWN, C_NEXT);
add_binding(&_groups_bindings, '\t', C_SELECT);
add_binding(&_groups_bindings, '\n', C_SELECT);
add_binding(&_groups_bindings, 'e', C_EDIT);
add_binding(&_groups_bindings, 'q', C_QUIT);
_entries_bindings = new_binding_set();
add_binding(&_entries_bindings, 'o', C_OPEN);
add_binding(&_entries_bindings, 's', C_SAVE);
add_binding(&_entries_bindings, 'a', C_ADD_ENTRY);
add_binding(&_entries_bindings, '\t', C_GROUPS);
add_binding(&_entries_bindings, KEY_UP, C_PREV);
add_binding(&_entries_bindings, KEY_DOWN, C_NEXT);
add_binding(&_entries_bindings, ' ', C_REVEAL);
add_binding(&_entries_bindings, 'r', C_REVEAL);
add_binding(&_entries_bindings, 'e', C_EDIT);
add_binding(&_entries_bindings, '\n', C_EDIT);
add_binding(&_entries_bindings, 'q', C_QUIT);
/* This should be optional. */
add_binding(&_entries_bindings, 'x', C_XCLIP);
}