/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2016-2017 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */

#include "config.h"

#include <string.h>
#include <appstream-glib.h>

#include "fu-nitrokey-common.h"
#include "fu-nitrokey-device.h"

G_DEFINE_TYPE (FuNitrokeyDevice, fu_nitrokey_device, FU_TYPE_USB_DEVICE)

#define NITROKEY_TRANSACTION_TIMEOUT		100 /* ms */
#define NITROKEY_NR_RETRIES			5

#define NITROKEY_REQUEST_DATA_LENGTH		59
#define NITROKEY_REPLY_DATA_LENGTH		53

#define NITROKEY_CMD_GET_DEVICE_STATUS		(0x20 + 14)

typedef struct __attribute__((packed)) {
	guint8		command;
	guint8		payload[NITROKEY_REQUEST_DATA_LENGTH];
	guint32		crc;
} NitrokeyHidRequest;

typedef struct __attribute__((packed)) {
	guint8		_padding; /* always zero */
	guint8		device_status;
	guint32		last_command_crc;
	guint8		last_command_status;
	guint8		payload[NITROKEY_REPLY_DATA_LENGTH];
	guint32		crc;
} NitrokeyHidResponse;

/* based from libnitrokey/stick20_commands.h */
typedef struct __attribute__((packed)) {
	guint8		_padding[24];
	guint8		SendCounter;
	guint8		SendDataType;
	guint8		FollowBytesFlag;
	guint8		SendSize;
	guint16		MagicNumber_StickConfig;
	guint8		ReadWriteFlagUncryptedVolume;
	guint8		ReadWriteFlagCryptedVolume;
	guint8		VersionReserved1;
	guint8		VersionMinor;
	guint8		VersionReserved2;
	guint8		VersionMajor;
	guint8		ReadWriteFlagHiddenVolume;
	guint8		FirmwareLocked;
	guint8		NewSDCardFound;
	guint8		SDFillWithRandomChars;
	guint32		ActiveSD_CardID;
	guint8		VolumeActiceFlag;
	guint8		NewSmartCardFound;
	guint8		UserPwRetryCount;
	guint8		AdminPwRetryCount;
	guint32		ActiveSmartCardID;
	guint8		StickKeysNotInitiated;
} NitrokeyGetDeviceStatusPayload;

static void
_dump_to_console (const gchar *title, const guint8 *buf, gsize buf_sz)
{
	if (g_getenv ("FWUPD_NITROKEY_VERBOSE") == NULL)
		return;
	g_debug ("%s", title);
	for (gsize i = 0; i < buf_sz; i++)
		g_debug ("%" G_GSIZE_FORMAT "=0x%02x", i, buf[i]);
}

static gboolean
nitrokey_execute_cmd (GUsbDevice *usb_device, guint8 command,
		      const guint8 *buf_in, gsize buf_in_sz,
		      guint8 *buf_out, gsize buf_out_sz,
		      GCancellable *cancellable, GError **error)
{
	NitrokeyHidResponse res;
	gboolean ret;
	gsize actual_len = 0;
	guint32 crc_tmp;
	guint8 buf[64];

	g_return_val_if_fail (buf_in_sz <= NITROKEY_REQUEST_DATA_LENGTH, FALSE);
	g_return_val_if_fail (buf_out_sz <= NITROKEY_REPLY_DATA_LENGTH, FALSE);

	/* create the request */
	memset (buf, 0x00, sizeof(buf));
	buf[0] = command;
	if (buf_in != NULL)
		memcpy (&buf[1], buf_in, buf_in_sz);
	crc_tmp = fu_nitrokey_perform_crc32 (buf, sizeof(buf) - 4);
	fu_common_write_uint32 (&buf[NITROKEY_REQUEST_DATA_LENGTH + 1], crc_tmp, G_LITTLE_ENDIAN);

	/* send request */
	_dump_to_console ("request", buf, sizeof(buf));
	ret = g_usb_device_control_transfer (usb_device,
					     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					     G_USB_DEVICE_REQUEST_TYPE_CLASS,
					     G_USB_DEVICE_RECIPIENT_INTERFACE,
					     0x09, 0x0300, 0x0002,
					     buf, sizeof(buf),
					     &actual_len,
					     NITROKEY_TRANSACTION_TIMEOUT,
					     NULL, error);
	if (!ret) {
		g_prefix_error (error, "failed to do HOST_TO_DEVICE: ");
		return FALSE;
	}
	if (actual_len != sizeof(buf)) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "only wrote %" G_GSIZE_FORMAT "bytes", actual_len);
		return FALSE;
	}

	/* get response */
	memset (buf, 0x00, sizeof(buf));
	ret = g_usb_device_control_transfer (usb_device,
					     G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
					     G_USB_DEVICE_REQUEST_TYPE_CLASS,
					     G_USB_DEVICE_RECIPIENT_INTERFACE,
					     0x01, 0x0300, 0x0002,
					     buf, sizeof(buf),
					     &actual_len,
					     NITROKEY_TRANSACTION_TIMEOUT,
					     NULL,
					     error);
	if (!ret) {
		g_prefix_error (error, "failed to do DEVICE_TO_HOST: ");
		return FALSE;
	}
	if (actual_len != sizeof(res)) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "only wrote %" G_GSIZE_FORMAT "bytes", actual_len);
		return FALSE;
	}
	_dump_to_console ("response", buf, sizeof(buf));

	/* verify this is the answer to the question we asked */
	memcpy (&res, buf, sizeof(buf));
	if (GUINT32_FROM_LE (res.last_command_crc) != crc_tmp) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "got response CRC %x, expected %x",
			     GUINT32_FROM_LE (res.last_command_crc), crc_tmp);
		return FALSE;
	}

	/* verify the response checksum */
	crc_tmp = fu_nitrokey_perform_crc32 (buf, sizeof(res) - 4);
	if (GUINT32_FROM_LE (res.crc) != crc_tmp) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "got packet CRC %x, expected %x",
			     GUINT32_FROM_LE (res.crc), crc_tmp);
		return FALSE;
	}

	/* copy out the payload */
	if (buf_out != NULL)
		memcpy (buf_out, &res.payload, buf_out_sz);

	/* success */
	return TRUE;
}

static gboolean
nitrokey_execute_cmd_full (GUsbDevice *usb_device, guint8 command,
			   const guint8 *buf_in, gsize buf_in_sz,
			   guint8 *buf_out, gsize buf_out_sz,
			   GCancellable *cancellable, GError **error)
{
	for (guint i = 0; i < NITROKEY_NR_RETRIES; i++) {
		g_autoptr(GError) error_local = NULL;
		gboolean ret;
		ret = nitrokey_execute_cmd (usb_device, command,
					    buf_in, buf_in_sz,
					    buf_out, buf_out_sz,
					    cancellable, &error_local);
		if (ret)
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_FAILED)) {
			if (error != NULL)
				*error = g_steal_pointer (&error_local);
			return FALSE;
		}
		g_warning ("retrying command: %s", error_local->message);
		g_usleep (100 * 1000);
	}

	/* failed */
	g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
		     "failed to issue command after %i retries",
		     NITROKEY_NR_RETRIES);
	return FALSE;
}

static gboolean
fu_nitrokey_device_probe (FuUsbDevice *device, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (device);

	/* not the right kind of device */
	if (g_usb_device_get_vid (usb_device) != 0x20a0 ||
	    g_usb_device_get_pid (usb_device) != 0x4109) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "not supported with this device");
		return FALSE;
	}

	/* harcoded */
	fu_device_set_name (FU_DEVICE (device), "Nitrokey Storage");
	fu_device_set_vendor (FU_DEVICE (device), "Nitrokey");
	fu_device_set_summary (FU_DEVICE (device), "A secure memory stick");
	fu_device_add_icon (FU_DEVICE (device), "media-removable");

	/* also add the USB VID:PID hash of the bootloader */
	fu_device_add_guid (FU_DEVICE (device), "USB\\VID_03EB&PID_2FF1");
	fu_device_set_remove_delay (FU_DEVICE (device), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE);

	/* allowed, but requires manual bootloader step */
	fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER);

	/* success */
	return TRUE;
}

static gboolean
fu_nitrokey_device_open (FuUsbDevice *device, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (device);
	NitrokeyGetDeviceStatusPayload payload;
	guint8 buf_reply[NITROKEY_REPLY_DATA_LENGTH];
	g_autofree gchar *version = NULL;

	/* claim interface */
	if (!g_usb_device_claim_interface (usb_device, 0x02, /* idx */
					   G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
					   error)) {
		g_prefix_error (error, "failed to do claim nitrokey: ");
		return FALSE;
	}

	/* get firmware version */
	if (!nitrokey_execute_cmd_full (usb_device,
					NITROKEY_CMD_GET_DEVICE_STATUS,
					NULL, 0,
					buf_reply, sizeof(buf_reply),
					NULL, error)) {
		g_prefix_error (error, "failed to do get firmware version: ");
		return FALSE;
	}
	_dump_to_console ("payload", buf_reply, sizeof(buf_reply));
	memcpy (&payload, buf_reply, sizeof(buf_reply));
	version = g_strdup_printf ("%u.%u", payload.VersionMinor, payload.VersionMajor);
	fu_device_set_version (FU_DEVICE (device), version);

	/* success */
	return TRUE;
}

static gboolean
fu_nitrokey_device_close (FuUsbDevice *device, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (device);
	g_autoptr(GError) error_local = NULL;

	/* reconnect kernel driver */
	if (!g_usb_device_release_interface (usb_device, 0x02,
					     G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
					     &error_local)) {
		g_warning ("failed to release interface: %s", error_local->message);
	}
	return TRUE;
}

static void
fu_nitrokey_device_init (FuNitrokeyDevice *device)
{
}

static void
fu_nitrokey_device_class_init (FuNitrokeyDeviceClass *klass)
{
	FuUsbDeviceClass *klass_usb_device = FU_USB_DEVICE_CLASS (klass);
	klass_usb_device->open = fu_nitrokey_device_open;
	klass_usb_device->close = fu_nitrokey_device_close;
	klass_usb_device->probe = fu_nitrokey_device_probe;
}

FuNitrokeyDevice *
fu_nitrokey_device_new (GUsbDevice *usb_device)
{
	FuNitrokeyDevice *device;
	device = g_object_new (FU_TYPE_NITROKEY_DEVICE,
			       "usb-device", usb_device,
			       NULL);
	return FU_NITROKEY_DEVICE (device);
}
