/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2008 Nils Wallménius
 *
 * 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 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include "plugin.h"
#include "lib/playback_control.h"
#include "lib/md5.h"


#define KEYBOX_FILE PLUGIN_APPS_DATA_DIR "/keybox.dat"
#define BLOCK_SIZE 8
#define MAX_ENTRIES 12*BLOCK_SIZE /* keep this a multiple of BLOCK_SIZE */
#define FIELD_LEN 32 /* should be enough for anyone ;) */

/* The header begins with the unencrypted salt (4 bytes) padded with 4 bytes of
   zeroes. After that comes the encrypted hash of the master password (16 bytes) */

#define HEADER_LEN 24

enum
{
    FILE_OPEN_ERROR = -1
};

struct pw_entry
{
    bool used;
    char title[FIELD_LEN];
    char name[FIELD_LEN];
    char password[FIELD_LEN];
    struct pw_entry *next;
};

struct pw_list
{
    struct pw_entry first; /* always points to the first element in the list */
    struct pw_entry entries[MAX_ENTRIES];
    int num_entries;
} pw_list;

/* use this to access hashes in different ways, not byte order
   independent but does it matter? */
union hash
{
    uint8_t bytes[16];
    uint32_t words[4];
};

static char buffer[sizeof(struct pw_entry)*MAX_ENTRIES];
static int bytes_read = 0; /* bytes read into the buffer */
static struct gui_synclist kb_list;
static union hash key;
static char master_pw[FIELD_LEN];
static uint32_t salt;
static union hash pwhash;
static bool data_changed = false;

static void encrypt_buffer(char *buf, size_t size, uint32_t *key);
static void decrypt_buffer(char *buf, size_t size, uint32_t *key);

/* the following two functions are the reference TEA implementation by
   David Wheeler and Roger Needham taken from
   http://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm */

static void do_encrypt(uint32_t* v, uint32_t* k)
{
    uint32_t v0=v[0], v1=v[1], sum=0, i;           /* set up */
    static const uint32_t delta=0x9e3779b9;        /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i < 32; i++) {                            /* basic cycle start */
        sum += delta;
        v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);  /* end cycle */
    }
    v[0]=v0; v[1]=v1;
}

static void do_decrypt(uint32_t* v, uint32_t* k)
{
    uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;  /* set up */
    static const uint32_t delta=0x9e3779b9;        /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i<32; i++) {                              /* basic cycle start */
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        sum -= delta;                                   /* end cycle */
    }
    v[0]=v0; v[1]=v1;
}

static int context_item_cb(int action,
                           const struct menu_item_ex *this_item,
                           struct gui_synclist *this_list)
{
    int i = (intptr_t)this_item;
    (void)this_list;

    if (action == ACTION_REQUEST_MENUITEM
        && pw_list.num_entries == 0
        && (i != 0 && i != 5))
    {
        return ACTION_EXIT_MENUITEM;
    }
    return action;
}

MENUITEM_STRINGLIST(context_m, "Context menu", context_item_cb,
                    "Add entry",
                    "Edit title", "Edit user name", "Edit password",
                    "Delete entry",
                    "Playback Control");

static const char* kb_list_cb(int selected_item, void *data,
                              char *buffer, size_t buffer_len)
{
    (void)data;
    int i;
    struct pw_entry *entry = pw_list.first.next;
    for (i = 0; i < selected_item; i++)
    {
        if (entry)
            entry = entry->next;
    }
    if (!entry)
        return NULL;

    rb->snprintf(buffer, buffer_len, "%s", entry->title);

    return buffer;
}

static void init_ll(void)
{
    pw_list.first.next = &pw_list.entries[0];
    pw_list.entries[0].next = NULL;
    pw_list.num_entries = 0;
}

static void delete_entry(int selected_item)
{
    int i;
    struct pw_entry *entry = &pw_list.first;
    struct pw_entry *entry2;

    /* find the entry before the one to delete */
    for (i = 0; i < selected_item; i++)
    {
        if (entry->next)
            entry = entry->next;
    }
    entry2 = entry->next;
    if (!entry2)
        return;

    entry->next = entry2->next;

    entry2->used = false;
    entry2->name[0] = '\0';
    entry2->password[0] = '\0';
    entry2->next = NULL;

    rb->gui_synclist_set_nb_items(&kb_list, --pw_list.num_entries);
    if(!pw_list.num_entries)
        init_ll();
    data_changed = true;
}

static void add_entry(int selected_item)
{
    int i, j;
    struct pw_entry *entry = pw_list.first.next;
    for (i = 0; i < MAX_ENTRIES && pw_list.entries[i].used; i++)
        ;

    if (MAX_ENTRIES == i)
    {
        rb->splash(HZ, "Password list full");
        return;
    }

    rb->splash(HZ, "Enter title");
    pw_list.entries[i].title[0] = '\0';
    if (rb->kbd_input(pw_list.entries[i].title, FIELD_LEN, NULL) < 0)
        return;

    rb->splash(HZ, "Enter name");
    pw_list.entries[i].name[0] = '\0';
    if (rb->kbd_input(pw_list.entries[i].name, FIELD_LEN, NULL) < 0)
    {
        pw_list.entries[i].title[0] = '\0';
        return;
    }

    rb->splash(HZ, "Enter password");
    pw_list.entries[i].password[0] = '\0';
    if (rb->kbd_input(pw_list.entries[i].password, FIELD_LEN, NULL) < 0)
    {
        pw_list.entries[i].title[0] = '\0';
        pw_list.entries[i].name[0] = '\0';
        return;
    }

    for (j = 0; j < selected_item; j++)
    {
        if (entry->next)
            entry = entry->next;
    }

    rb->gui_synclist_set_nb_items(&kb_list, ++pw_list.num_entries);

    pw_list.entries[i].used = true;
    pw_list.entries[i].next = entry->next;

    entry->next = &pw_list.entries[i];

    if (entry->next == entry)
        entry->next = NULL;

    data_changed = true;
}

static void edit_title(int selected_item)
{
    int i;
    struct pw_entry *entry = pw_list.first.next;
    for (i = 0; i < selected_item; i++)
    {
        if (entry->next)
            entry = entry->next;
    }
    if (rb->kbd_input(entry->title, FIELD_LEN, NULL) == 0)
        data_changed = true;
}

static void edit_name(int selected_item)
{
    int i;
    struct pw_entry *entry = pw_list.first.next;
    for (i = 0; i < selected_item; i++)
    {
        if (entry->next)
            entry = entry->next;
    }
    if (rb->kbd_input(entry->name, FIELD_LEN, NULL) == 0)
        data_changed = true;
}

static void edit_pw(int selected_item)
{
    int i;
    struct pw_entry *entry = pw_list.first.next;
    for (i = 0; i < selected_item; i++)
    {
        if (entry->next)
            entry = entry->next;
    }
    if (rb->kbd_input(entry->password, FIELD_LEN, NULL) == 0)
        data_changed = true;
}

static void context_menu(int selected_item)
{
    int selection = 0, result;
    bool exit = false;

    do {
        result = rb->do_menu(&context_m, &selection, NULL, false);
        switch (result) {
        case 0:
            add_entry(selected_item);
            return;
        case 1:
            edit_title(selected_item);
            return;
        case 2:
            edit_name(selected_item);
            return;
        case 3:
            edit_pw(selected_item);
            return;
        case 4:
            delete_entry(selected_item);
            return;
        case 5:
            playback_control(NULL);
            return;
        default:
            exit = true;
            break;
        }
        rb->yield();
    } while (!exit);
}

static void splash_pw(int selected_item)
{
    int i;
    struct pw_entry *entry = pw_list.first.next;

    for (i = 0; i < selected_item; i++)
    {
        if (entry->next)
            entry = entry->next;
    }
    if (entry->name[0] != '\0')
        rb->splashf(0, "%s  %s", entry->name, entry->password);
    else
        rb->splashf(0, "%s", entry->password);
    rb->get_action(CONTEXT_STD, TIMEOUT_BLOCK);
}

static void hash_pw(union hash *out)
{
    int i;
    struct md5_s pw_md5;

    InitMD5(&pw_md5);
    AddMD5(&pw_md5, master_pw, rb->strlen(master_pw));
    EndMD5(&pw_md5);

    for (i = 0; i < 4; i++)
        out->words[i] = htole32(pw_md5.p_digest[i]);
}

static void make_key(void)
{
    int i;
    char buf[sizeof(master_pw) + sizeof(salt) + 1] = {0};
    struct md5_s key_md5;
    size_t len = rb->strlen(master_pw);

    rb->strlcpy(buf, master_pw, sizeof(buf));

    rb->memcpy(&buf[len], &salt, sizeof(salt));

    InitMD5(&key_md5);
    AddMD5(&key_md5, buf, rb->strlen(buf));
    EndMD5(&key_md5);

    for (i = 0; i < 4; i++)
        key.words[i] = key_md5.p_digest[i];
}

static void decrypt_buffer(char *buf, size_t size, uint32_t *key)
{
    unsigned int i;
    uint32_t block[2];

    for (i = 0; i < size/BLOCK_SIZE; i++)
    {
        rb->memcpy(&block[0], &buf[i*BLOCK_SIZE], sizeof(block));

        block[0] = letoh32(block[0]);
        block[1] = letoh32(block[1]);

        do_decrypt(&block[0], key);

        /* byte swap one block */
        block[0] = letoh32(block[0]);
        block[1] = letoh32(block[1]);

        rb->memcpy(&buf[i*BLOCK_SIZE], &block[0], sizeof(block));
    }
}

static void encrypt_buffer(char *buf, size_t size, uint32_t *key)
{
    unsigned int i;
    uint32_t block[2];

    for (i = 0; i < size/BLOCK_SIZE; i++)
    {
        rb->memcpy(&block[0], &buf[i*BLOCK_SIZE], sizeof(block));

        /* byte swap one block */
        block[0] = htole32(block[0]);
        block[1] = htole32(block[1]);

        do_encrypt(&block[0], key);

        block[0] = htole32(block[0]);
        block[1] = htole32(block[1]);

        rb->memcpy(&buf[i*BLOCK_SIZE], &block[0], sizeof(block));
    }
}

static int parse_buffer(void)
{
    int i;
    int len;
    struct pw_entry *entry = pw_list.first.next;
    char *start, *end;
    start = &buffer[HEADER_LEN];

    rb->memcpy(&salt, &buffer[0], sizeof(salt));
    make_key();

    decrypt_buffer(&buffer[8], bytes_read - 8, &key.words[0]);

    if (rb->memcmp(&buffer[8], &pwhash, sizeof(union hash)))
    {
        rb->splash(HZ*2, "Wrong password");
        return -1;
    }

    for (i = 0; i < MAX_ENTRIES; i++)
    {
        end = rb->strchr(start, '\0'); /* find eol */
        len = end - &buffer[HEADER_LEN];
        if ((len > bytes_read + HEADER_LEN) || start == end)
        {
            break;
        }

        rb->strlcpy(entry->title, start, FIELD_LEN);
        start = end + 1;

        end = rb->strchr(start, '\0'); /* find eol */
        len = end - &buffer[HEADER_LEN];
        if (len > bytes_read + HEADER_LEN)
        {
            break;
        }

        rb->strlcpy(entry->name, start, FIELD_LEN);
        start = end + 1;

        end = rb->strchr(start, '\0'); /* find eol */
        len = end - &buffer[HEADER_LEN];
        if (len > bytes_read + HEADER_LEN)
        {
            break;
        }
        rb->strlcpy(entry->password, start, FIELD_LEN);
        start = end + 1;
        entry->used = true;
        if (i + 1 < MAX_ENTRIES - 1)
        {
            entry->next = &pw_list.entries[i+1];
            entry = entry->next;
        }
        else
        {
            break;
        }
    }
    entry->next = NULL;
    pw_list.num_entries = i;
    rb->gui_synclist_set_nb_items(&kb_list, pw_list.num_entries);
    return 0;
}

static void write_output(int fd)
{
    int i;
    size_t len, size;
    char *p = &buffer[HEADER_LEN]; /* reserve space for salt + hash */

    rb->memcpy(&buffer[8], &pwhash, sizeof(union hash));
    struct pw_entry *entry = pw_list.first.next;

    for (i = 0; i < pw_list.num_entries; i++)
    {
        len = rb->strlen(entry->title);
        rb->strlcpy(p, entry->title, len+1);
        p += len+1;
        len = rb->strlen(entry->name);
        rb->strlcpy(p, entry->name, len+1);
        p += len+1;
        len = rb->strlen(entry->password);
        rb->strlcpy(p, entry->password, len+1);
        p += len+1;
        if (entry->next)
            entry = entry->next;
    }
    *p++ = '\0'; /* mark the end of the list */

    /* round up to a number divisible by BLOCK_SIZE */
    size = ((p - buffer + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE;

    salt = rb->rand();
    make_key();

    encrypt_buffer(&buffer[8], size, &key.words[0]);
    rb->memcpy(&buffer[0], &salt, sizeof(salt));

    rb->write(fd, &buffer, size);
}

static int enter_pw(char *pw_buf, size_t buflen, bool new_pw)
{
    char buf[2][sizeof(master_pw)];
    rb->memset(buf, 0, sizeof(buf));
    rb->memset(master_pw, 0, sizeof(master_pw));

    if (new_pw)
    {
        rb->splash(HZ, "Enter new master password");
        if (rb->kbd_input(buf[0], sizeof(buf[0]), NULL) < 0)
            return -1;

        rb->splash(HZ, "Confirm master password");
        if (rb->kbd_input(buf[1], sizeof(buf[1]), NULL) < 0)
            return -1;

        if (rb->strcmp(buf[0], buf[1]))
        {
            rb->splash(HZ, "Password mismatch");
            return -1;
        }
        else
        {
            rb->strlcpy(pw_buf, buf[0], buflen);
            hash_pw(&pwhash);
            return 0;
        }
    }

    rb->splash(HZ, "Enter master password");
    if (rb->kbd_input(pw_buf, buflen, NULL) < 0)
        return -1;
    hash_pw(&pwhash);
    return 0;
}

static int keybox(void)
{
    int button, fd;
    bool new_file = !rb->file_exists(KEYBOX_FILE);
    bool done = false;

    if (enter_pw(master_pw, sizeof (master_pw), new_file))
        return 0;

    /* Read the existing file */
    if (!new_file)
    {
        fd = rb->open(KEYBOX_FILE, O_RDONLY);
        if (fd < 0)
            return FILE_OPEN_ERROR;
        bytes_read = rb->read(fd, &buffer, sizeof(buffer));

        rb->close(fd);

        if (parse_buffer())
            return 0;
    }

    while (!done)
    {
        rb->gui_synclist_draw(&kb_list);
        button = rb->get_action(CONTEXT_LIST, TIMEOUT_BLOCK);
        if (rb->gui_synclist_do_button(&kb_list, &button))
            continue;

        switch (button)
        {
            case ACTION_STD_OK:
                splash_pw(rb->gui_synclist_get_sel_pos(&kb_list));
                break;
            case ACTION_STD_CONTEXT:
                context_menu(rb->gui_synclist_get_sel_pos(&kb_list));
                break;
            case ACTION_STD_CANCEL:
                done = true;
                break;
        }
        rb->yield();
    }

    if (data_changed)
    {
        fd = rb->open(KEYBOX_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd < 0)
            return FILE_OPEN_ERROR;
        write_output(fd);
        rb->close(fd);
    }

    return 0;
}

static void reset(void)
{
    static const char *message_lines[]=
        {"Do you really want", "to reset keybox?"};
    static const char *yes_lines[]=
        {"Keybox reset."};
    static const struct text_message message={message_lines, 2};
    static const struct text_message yes_message={yes_lines, 1};

    if(rb->gui_syncyesno_run(&message, &yes_message, NULL) == YESNO_YES)
    {
        rb->remove(KEYBOX_FILE);
        rb->memset(&buffer, 0, sizeof(buffer));
        rb->memset(&pw_list, 0, sizeof(pw_list));
        rb->gui_synclist_set_nb_items(&kb_list, 0);
        init_ll();
    }
}

static int main_menu(void)
{
    int selection = 0, result, ret;
    bool exit = false;

    MENUITEM_STRINGLIST(menu, "Keybox", NULL,
                        "Enter Keybox", "Reset Keybox",
                        "Playback Control", "Exit");

    do {
        result = rb->do_menu(&menu, &selection, NULL, false);
        switch (result) {
        case 0:
            ret = keybox();
            if (ret)
                return ret;
            break;
        case 1:
            reset();
            break;
        case 2:
            playback_control(NULL);
            break;
        case 3:
            exit = true;
            break;
        }
        rb->yield();
    } while (!exit);

    return 0;
}

enum plugin_status plugin_start(const void *parameter)
{
    (void)parameter;
    int ret;

    rb->gui_synclist_init(&kb_list, &kb_list_cb, NULL, false, 1, NULL);

    rb->gui_synclist_set_title(&kb_list, "Keybox", NOICON);
    rb->gui_synclist_set_nb_items(&kb_list, 0);
    rb->gui_synclist_select_item(&kb_list, 0);

    init_ll();
    ret = main_menu();

    switch (ret)
    {
        case FILE_OPEN_ERROR:
            rb->splash(HZ*2, "Error opening file");
            return PLUGIN_ERROR;
    }

    return PLUGIN_OK;
}
