/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2010 Amaury Pouly
 *
 * 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 "crypto.h"
#include <stdio.h>
#include <stdbool.h>
#ifdef CRYPTO_LIBUSB
#include "libusb.h"
#endif
#include "misc.h"

static enum crypto_method_t cur_method = CRYPTO_NONE;
static byte key[16];
static uint16_t usb_vid, usb_pid;

void crypto_setup(enum crypto_method_t method, void *param)
{
    cur_method = method;
    switch(method)
    {
        case CRYPTO_KEY:
            memcpy(key, param, sizeof(key));
            break;
        case CRYPTO_USBOTP:
        {
            uint32_t value = *(uint32_t *)param;
            usb_vid = value >> 16;
            usb_pid = value & 0xffff;
            break;
        }
        default:
            break;
    }
}

int crypto_apply(
    byte *in_data, /* Input data */
    byte *out_data, /* Output data (or NULL) */
    int nr_blocks, /* Number of blocks (one block=16 bytes) */
    byte iv[16], /* Key */
    byte (*out_cbc_mac)[16], /* CBC-MAC of the result (or NULL) */
    int encrypt)
{
    if(cur_method == CRYPTO_KEY)
    {
        cbc_mac(in_data, out_data, nr_blocks, key, iv, out_cbc_mac, encrypt);
        return CRYPTO_ERROR_SUCCESS;
    }
    #ifdef CRYPTO_LIBUSB
    else if(cur_method == CRYPTO_USBOTP)
    {
        if(out_cbc_mac && !encrypt)
            memcpy(*out_cbc_mac, in_data + 16 * (nr_blocks - 1), 16);

        libusb_device_handle *handle = NULL;
        libusb_context *ctx;
        /* init library */
        libusb_init(&ctx);
        libusb_set_debug(NULL,3);
        /* open device */
        handle = libusb_open_device_with_vid_pid(ctx, usb_vid, usb_pid);
        if(handle == NULL)
        {
            printf("usbotp: cannot open device %04x:%04x\n", usb_vid, usb_pid);
            return CRYPTO_ERROR_NODEVICE;
        }
        /* get device pointer */
        libusb_device *mydev = libusb_get_device(handle);
        if(g_debug)
            printf("usbotp: device found at %d:%d\n", libusb_get_bus_number(mydev),
                libusb_get_device_address(mydev));
        int config_id;
        /* explore configuration */
        libusb_get_configuration(handle, &config_id);
        struct libusb_config_descriptor *config;
        libusb_get_active_config_descriptor(mydev, &config);

        if(g_debug)
        {
            printf("usbotp: configuration: %d\n", config_id);
            printf("usbotp: interfaces: %d\n", config->bNumInterfaces);
        }

        const struct libusb_endpoint_descriptor *endp = NULL;
        int intf, intf_alt;
        for(intf = 0; intf < config->bNumInterfaces; intf++)
            for(intf_alt = 0; intf_alt < config->interface[intf].num_altsetting; intf_alt++)
                for(int ep = 0; ep < config->interface[intf].altsetting[intf_alt].bNumEndpoints; ep++)
                {
                    endp = &config->interface[intf].altsetting[intf_alt].endpoint[ep];
                    if((endp->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) == LIBUSB_TRANSFER_TYPE_INTERRUPT &&
                            (endp->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK) == LIBUSB_ENDPOINT_IN)
                        goto Lfound;
                }
        libusb_close(handle);
        printf("usbotp: No suitable endpoint found\n");
        return CRYPTO_ERROR_BADENDP;

        if(g_debug)
        {
            printf("usbotp: use interface %d, alt %d\n", intf, intf_alt);
            printf("usbotp: use endpoint %d\n", endp->bEndpointAddress);
        }
        Lfound:
        if(libusb_claim_interface(handle, intf) != 0)
        {
            if(g_debug)
                printf("usbotp: claim error\n");
            return CRYPTO_ERROR_CLAIMFAIL;
        }

        int buffer_size = 16 + 16 * nr_blocks;
        unsigned char *buffer = xmalloc(buffer_size);
        memcpy(buffer, iv, 16);
        memcpy(buffer + 16, in_data, 16 * nr_blocks);
        int  ret = libusb_control_transfer(handle,
            LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_DEVICE,
            0xaa, encrypt ? 0xeeee : 0xdddd, 0, buffer, buffer_size, 1000);
        if(ret < 0)
        {
            if(g_debug)
                printf("usbotp: control transfer failed: %d\n", ret);
            libusb_release_interface(handle, intf);
            libusb_close(handle);
            return CRYPTO_ERROR_DEVREJECT;
        }

        int recv_size;
        ret = libusb_interrupt_transfer(handle, endp->bEndpointAddress, buffer,
            buffer_size, &recv_size, 1000);
        libusb_release_interface(handle, intf);
        libusb_close(handle);

        if(ret < 0)
        {
            if(g_debug)
                printf("usbotp: interrupt transfer failed: %d\n", ret);
            return CRYPTO_ERROR_DEVSILENT;
        }
        if(recv_size != buffer_size)
        {
            if(g_debug)
                printf("usbotp: device returned %d bytes, expected %d\n", recv_size,
                    buffer_size);
            return CRYPTO_ERROR_DEVERR;
        }

        if(out_data)
            memcpy(out_data, buffer + 16, 16 * nr_blocks);
        if(out_cbc_mac && encrypt)
            memcpy(*out_cbc_mac, buffer + buffer_size - 16, 16);

        return CRYPTO_ERROR_SUCCESS;
    }
    #endif
    else
        return CRYPTO_ERROR_BADSETUP;
}

int crypto_cbc(
    byte *in_data, /* Input data */
    byte *out_data, /* Output data (or NULL) */
    int nr_blocks, /* Number of blocks (one block=16 bytes) */
    struct crypto_key_t *key, /* Key */
    byte iv[16], /* IV */
    byte (*out_cbc_mac)[16], /* CBC-MAC of the result (or NULL) */
    int encrypt)
{
    crypto_setup(key->method, (void *)key->u.param);
    return crypto_apply(in_data, out_data, nr_blocks, iv, out_cbc_mac, encrypt);
}
