/*
 * Copyright (C) 2020 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include "fu-uefi-dbx-common.h"
#include "fu-uefi-dbx-device.h"

struct _FuUefiDbxDevice {
	FuDevice parent_instance;
};

G_DEFINE_TYPE(FuUefiDbxDevice, fu_uefi_dbx_device, FU_TYPE_DEVICE)

#define FU_UEFI_DBX_DEVICE_INHIBIT_ID_NO_EFIVARS_SPACE "no-efivars-space"

#define FU_UEFI_DBX_DEVICE_MIN_EFIVARS_SPACE (30 * 1024)

static gboolean
fu_uefi_dbx_device_write_firmware(FuDevice *device,
				  FuFirmware *firmware,
				  FuProgress *progress,
				  FwupdInstallFlags install_flags,
				  GError **error)
{
	const guint8 *buf;
	gsize bufsz = 0;
	g_autoptr(GBytes) fw = NULL;

	/* get default image */
	fw = fu_firmware_get_bytes(firmware, error);
	if (fw == NULL)
		return FALSE;

	/* write entire chunk to efivarfs */
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
	buf = g_bytes_get_data(fw, &bufsz);
	if (!fu_efivar_set_data(FU_EFIVAR_GUID_SECURITY_DATABASE,
				"dbx",
				buf,
				bufsz,
				FU_EFIVAR_ATTR_APPEND_WRITE |
				    FU_EFIVAR_ATTR_TIME_BASED_AUTHENTICATED_WRITE_ACCESS |
				    FU_EFIVAR_ATTR_RUNTIME_ACCESS |
				    FU_EFIVAR_ATTR_BOOTSERVICE_ACCESS | FU_EFIVAR_ATTR_NON_VOLATILE,
				error)) {
		return FALSE;
	}

	/* success! */
	return TRUE;
}

static gboolean
fu_uefi_dbx_device_set_checksum(FuUefiDbxDevice *self, const gchar *csum, GError **error)
{
	/* used for md-set-version, but only when the device is added to the daemon */
	fu_device_add_checksum(FU_DEVICE(self), csum);

	/* for operating offline, with no /usr/share/fwupd/remotes.d archives */
	fu_device_add_instance_strup(FU_DEVICE(self), "CSUM", csum);
	if (!fu_device_build_instance_id_full(FU_DEVICE(self),
					      FU_DEVICE_INSTANCE_FLAG_QUIRKS,
					      error,
					      "UEFI",
					      "CSUM",
					      NULL))
		return FALSE;

	/* this makes debugging easier */
	if (fu_device_get_version(FU_DEVICE(self)) == NULL) {
		g_autofree gchar *csum_trunc = g_strndup(csum, 8);
		g_autofree gchar *summary =
		    g_strdup_printf("UEFI revocation database %s", csum_trunc);
		fu_device_set_summary(FU_DEVICE(self), summary);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_uefi_dbx_device_ensure_checksum(FuUefiDbxDevice *self, GError **error)
{
	g_autoptr(GBytes) dbx_blob = NULL;
	g_autoptr(FuFirmware) dbx = fu_efi_signature_list_new();
	g_autoptr(GPtrArray) sigs = NULL;

	/* use the number of checksums in the dbx as a version number, ignoring
	 * some owners that do not make sense */
	dbx_blob = fu_efivar_get_data_bytes(FU_EFIVAR_GUID_SECURITY_DATABASE, "dbx", NULL, error);
	if (dbx_blob == NULL)
		return FALSE;
	if (!fu_firmware_parse(dbx, dbx_blob, FWUPD_INSTALL_FLAG_NO_SEARCH, error))
		return FALSE;

	/* add the last checksum to the device */
	sigs = fu_firmware_get_images(dbx);
	if (sigs->len > 0) {
		FuEfiSignature *sig = g_ptr_array_index(sigs, sigs->len - 1);
		g_autofree gchar *csum =
		    fu_firmware_get_checksum(FU_FIRMWARE(sig), G_CHECKSUM_SHA256, NULL);
		if (csum != NULL) {
			if (!fu_uefi_dbx_device_set_checksum(self, csum, error))
				return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static void
fu_uefi_dbx_device_version_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data)
{
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_set_version_lowest(device, fu_device_get_version(device));
}

static FuFirmware *
fu_uefi_dbx_prepare_firmware(FuDevice *device, GBytes *fw, FwupdInstallFlags flags, GError **error)
{
	FuContext *ctx = fu_device_get_context(device);
	g_autoptr(FuFirmware) siglist = fu_efi_signature_list_new();

	/* parse dbx */
	if (!fu_firmware_parse(siglist, fw, flags, error))
		return NULL;

	/* validate this is safe to apply */
	if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
		//		fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_VERIFY);
		if (!fu_uefi_dbx_signature_list_validate(ctx,
							 FU_EFI_SIGNATURE_LIST(siglist),
							 flags,
							 error)) {
			g_prefix_error(error,
				       "Blocked executable in the ESP, "
				       "ensure grub and shim are up to date: ");
			return NULL;
		}
	}

	/* default blob */
	return fu_firmware_new_from_bytes(fw);
}

static gboolean
fu_uefi_dbx_device_probe(FuDevice *device, GError **error)
{
	FuContext *ctx = fu_device_get_context(device);
	FuUefiDbxDevice *self = FU_UEFI_DBX_DEVICE(device);
	g_autoptr(FuFirmware) kek = fu_efi_signature_list_new();
	g_autoptr(GBytes) kek_blob = NULL;
	g_autoptr(GPtrArray) sigs = NULL;
	g_autoptr(GError) error_fde = NULL;
	g_autoptr(GError) error_free = NULL;

	/* check free space */
	if (!fu_context_efivars_check_free_space(ctx,
						 FU_UEFI_DBX_DEVICE_MIN_EFIVARS_SPACE,
						 &error_free)) {
		fu_device_inhibit(FU_DEVICE(self),
				  FU_UEFI_DBX_DEVICE_INHIBIT_ID_NO_EFIVARS_SPACE,
				  error_free->message);
	} else {
		fu_device_uninhibit(FU_DEVICE(self),
				    FU_UEFI_DBX_DEVICE_INHIBIT_ID_NO_EFIVARS_SPACE);
	}

	/* use each of the certificates in the KEK to generate the GUIDs */
	kek_blob = fu_efivar_get_data_bytes(FU_EFIVAR_GUID_EFI_GLOBAL, "KEK", NULL, error);
	if (kek_blob == NULL)
		return FALSE;
	if (!fu_firmware_parse(kek, kek_blob, FWUPD_INSTALL_FLAG_NONE, error))
		return FALSE;
	fu_device_add_instance_strup(device, "ARCH", EFI_MACHINE_TYPE_NAME);

	sigs = fu_firmware_get_images(kek);
	for (guint j = 0; j < sigs->len; j++) {
		FuEfiSignature *sig = g_ptr_array_index(sigs, j);
		g_autofree gchar *checksum = NULL;

		checksum = fu_firmware_get_checksum(FU_FIRMWARE(sig), G_CHECKSUM_SHA256, error);
		if (checksum == NULL)
			return FALSE;
		fu_device_add_instance_strup(device, "CRT", checksum);
		fu_device_build_instance_id_full(device,
						 FU_DEVICE_INSTANCE_FLAG_QUIRKS,
						 NULL,
						 "UEFI",
						 "CRT",
						 NULL);
		fu_device_build_instance_id(device, NULL, "UEFI", "CRT", "ARCH", NULL);
	}

	/* dbx changes are expected to change PCR7, warn the user that BitLocker might ask for
	recovery key after fw update */
	if (!fu_common_check_full_disk_encryption(&error_fde)) {
		g_debug("FDE in use, set flag: %s", error_fde->message);
		fu_device_add_flag(device, FWUPD_DEVICE_FLAG_AFFECTS_FDE);
	}

	return fu_uefi_dbx_device_ensure_checksum(self, error);
}

static void
fu_uefi_dbx_device_report_metadata_pre(FuDevice *device, GHashTable *metadata)
{
	guint64 nvram_total = fu_efivar_space_used(NULL);
	if (nvram_total != G_MAXUINT64) {
		g_hash_table_insert(metadata,
				    g_strdup("EfivarNvramUsed"),
				    g_strdup_printf("%" G_GUINT64_FORMAT, nvram_total));
	}
}

static void
fu_uefi_dbx_device_set_progress(FuDevice *self, FuProgress *progress)
{
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload");
}

static void
fu_uefi_dbx_device_init(FuUefiDbxDevice *self)
{
	fu_device_set_physical_id(FU_DEVICE(self), "dbx");
	fu_device_set_name(FU_DEVICE(self), "UEFI dbx");
	fu_device_set_summary(FU_DEVICE(self), "UEFI revocation database");
	fu_device_add_protocol(FU_DEVICE(self), "org.uefi.dbx2");
	fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER);
	fu_device_set_install_duration(FU_DEVICE(self), 1);
	fu_device_add_icon(FU_DEVICE(self), "computer");
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_MD_ONLY_CHECKSUM);
	fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_MD_SET_VERSION);
	fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_HOST_FIRMWARE_CHILD);
	if (!fu_common_is_live_media())
		fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
	g_signal_connect(FWUPD_DEVICE(self),
			 "notify::version",
			 G_CALLBACK(fu_uefi_dbx_device_version_notify_cb),
			 NULL);
}

static void
fu_uefi_dbx_device_class_init(FuUefiDbxDeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
	klass_device->probe = fu_uefi_dbx_device_probe;
	klass_device->write_firmware = fu_uefi_dbx_device_write_firmware;
	klass_device->prepare_firmware = fu_uefi_dbx_prepare_firmware;
	klass_device->set_progress = fu_uefi_dbx_device_set_progress;
	klass_device->report_metadata_pre = fu_uefi_dbx_device_report_metadata_pre;
}

FuUefiDbxDevice *
fu_uefi_dbx_device_new(FuContext *ctx)
{
	FuUefiDbxDevice *self;
	self = g_object_new(FU_TYPE_UEFI_DBX_DEVICE, "context", ctx, NULL);
	return self;
}
