/* Copyright 2016 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "ccd_config.h"
#include "console.h"
#include "crc8.h"
#include "extension.h"
#include "gpio.h"
#include "hooks.h"
#include "registers.h"
#include "scratch_reg1.h"
#include "system.h"
#include "system_chip.h"
#include "tpm_nvmem_read.h"
#include "tpm_registers.h"
#include "util.h"

#define CPRINTS(format, args...) cprints(CC_RBOX, format, ## args)
#define CPRINTF(format, args...) cprintf(CC_RBOX, format, ## args)

/**
 * Return non-zero if battery is present
 */
int board_battery_is_present(void)
{
	/* Invert because battery-present signal is active low */
	return !gpio_get_level(GPIO_BATT_PRES_L);
}

/**
 * Set the current write protect state in RBOX and long life scratch register.
 *
 * @param asserted: 0 to disable write protect, otherwise enable write protect.
 */
static void set_wp_state(int asserted)
{
	/* Enable writing to the long life register */
	GWRITE_FIELD(PMU, LONG_LIFE_SCRATCH_WR_EN, REG1, 1);

	if (asserted) {
		GREG32(PMU, LONG_LIFE_SCRATCH1) |= BOARD_WP_ASSERTED;
		GREG32(RBOX, EC_WP_L) = 0;
	} else {
		GREG32(PMU, LONG_LIFE_SCRATCH1) &= ~BOARD_WP_ASSERTED;
		GREG32(RBOX, EC_WP_L) = 1;
	}

	/* Disable writing to the long life register */
	GWRITE_FIELD(PMU, LONG_LIFE_SCRATCH_WR_EN, REG1, 0);
}

/**
 * Return the current WP state
 *
 * @return 0 if WP deasserted, 1 if WP asserted
 */
static int get_wp_state(void)
{
	/* Signal is active low, so invert */
	return !GREG32(RBOX, EC_WP_L);
}

static void check_wp_battery_presence(void)
{
	int bp = board_battery_is_present();

	/* If we're forcing WP, ignore battery detect */
	if (GREG32(PMU, LONG_LIFE_SCRATCH1) & BOARD_FORCING_WP)
		return;

	/* Otherwise, mirror battery */
	if (bp != get_wp_state()) {
		CPRINTS("WP %d", bp);
		set_wp_state(bp);
	}
}
DECLARE_HOOK(HOOK_SECOND, check_wp_battery_presence, HOOK_PRIO_DEFAULT);

/**
 * Force write protect state or follow battery presence.
 *
 * @param force: Force write protect to wp_en if non-zero, otherwise use battery
 *               presence as the source.
 * @param wp_en: 0: Deassert WP. 1: Assert WP.
 */
static void force_write_protect(int force, int wp_en)
{
	/* Enable writing to the long life register */
	GWRITE_FIELD(PMU, LONG_LIFE_SCRATCH_WR_EN, REG1, 1);

	if (force) {
		/* Force WP regardless of battery presence. */
		GREG32(PMU, LONG_LIFE_SCRATCH1) |= BOARD_FORCING_WP;
	} else {
		/* Stop forcing write protect. */
		GREG32(PMU, LONG_LIFE_SCRATCH1) &= ~BOARD_FORCING_WP;
		/* Use battery presence as the value for write protect. */
		wp_en = board_battery_is_present();
	}

	/* Disable writing to the long life register */
	GWRITE_FIELD(PMU, LONG_LIFE_SCRATCH_WR_EN, REG1, 0);

	/* Update the WP state. */
	set_wp_state(wp_en);
}

static int command_wp(int argc, char **argv)
{
	int val = 1;
	int forced = 1;

	if (argc > 1) {
		/* Make sure we're allowed to override WP settings */
		if (!ccd_is_cap_enabled(CCD_CAP_OVERRIDE_WP))
			return EC_ERROR_ACCESS_DENIED;

		/* Update WP */
		if (strncasecmp(argv[1], "follow_batt_pres", 16) == 0)
			forced = 0;
		else if (parse_bool(argv[1], &val))
			forced = 1;
		else
			return EC_ERROR_PARAM1;

		force_write_protect(forced, val);

		if (argc > 2 && !strcasecmp(argv[2], "atboot")) {
			/* Change override at boot to match */
			ccd_set_flag(CCD_FLAG_OVERRIDE_WP_AT_BOOT, forced);
			ccd_set_flag(CCD_FLAG_OVERRIDE_WP_STATE_ENABLED, val);
		}
	}

	forced = GREG32(PMU, LONG_LIFE_SCRATCH1) & BOARD_FORCING_WP;
	ccprintf("Flash WP: %s%s\n", forced ? "forced " : "",
		 get_wp_state() ? "enabled" : "disabled");

	ccprintf(" at boot: ");
	if (ccd_get_flag(CCD_FLAG_OVERRIDE_WP_AT_BOOT))
		ccprintf("forced %s\n",
			 ccd_get_flag(CCD_FLAG_OVERRIDE_WP_STATE_ENABLED)
			 ? "enabled" : "disabled");
	else
		ccprintf("follow_batt_pres\n");

	return EC_SUCCESS;
}
DECLARE_SAFE_CONSOLE_COMMAND(wp, command_wp,
			     "[<BOOLEAN>/follow_batt_pres [atboot]]",
			     "Get/set the flash HW write-protect signal");

void set_wp_follow_ccd_config(void)
{
	if (ccd_get_flag(CCD_FLAG_OVERRIDE_WP_AT_BOOT)) {
		/* Reset to at-boot state specified by CCD */
		force_write_protect(1, ccd_get_flag
				    (CCD_FLAG_OVERRIDE_WP_STATE_ENABLED));
	} else {
		/* Reset to WP based on battery-present (val is ignored) */
		force_write_protect(0, 1);
	}
}

void init_wp_state(void)
{
	/* Check system reset flags after CCD config is initially loaded */
	if ((system_get_reset_flags() & RESET_FLAG_HIBERNATE) &&
	    !system_rollback_detected()) {
		/*
		 * Deep sleep resume without rollback, so reload the WP state
		 * that was saved to the long-life registers before the deep
		 * sleep instead of going back to the at-boot default.
		 */
		if (GREG32(PMU, LONG_LIFE_SCRATCH1) & BOARD_FORCING_WP) {
			/* Temporarily forcing WP */
			set_wp_state(GREG32(PMU, LONG_LIFE_SCRATCH1) &
				     BOARD_WP_ASSERTED);
		} else {
			/* Write protected if battery is present */
			set_wp_state(board_battery_is_present());
		}
	} else {
		set_wp_follow_ccd_config();
	}
}

/**
 * Wipe the TPM
 *
 * @return EC_SUCCESS, or non-zero if error.
 */
int board_wipe_tpm(void)
{
	int rc;

	/*
	 * Blindly zapping the TPM space while the AP is awake and poking at
	 * it will bork the TPM task and the AP itself, so force the whole
	 * system off by holding the EC in reset.
	 */
	CPRINTS("%s: force EC off", __func__);
	assert_ec_rst();

	/* Wipe the TPM's memory and reset the TPM task. */
	rc = tpm_reset_request(1, 1);
	if (rc != EC_SUCCESS) {
		/*
		 * If anything goes wrong (which is unlikely), we REALLY don't
		 * want to unlock the console. It's possible to fail without
		 * the TPM task ever running, so rebooting is probably our best
		 * bet for fixing the problem.
		 */
		CPRINTS("%s: Couldn't wipe nvmem! (rc %d)", __func__, rc);
		cflush();
		system_reset(SYSTEM_RESET_MANUALLY_TRIGGERED |
			     SYSTEM_RESET_HARD);

		/*
		 * That should never return, but if it did, release EC reset
		 * and pass through the error we got.
		 */
		deassert_ec_rst();
		return rc;
	}

	CPRINTS("TPM is erased");

	/* Tell the TPM task to re-enable NvMem commits. */
	tpm_reinstate_nvmem_commits();

	/* Let the rest of the system boot. */
	CPRINTS("%s: release EC reset", __func__);
	deassert_ec_rst();

	return EC_SUCCESS;
}

/****************************************************************************/
/* FWMP TPM NVRAM space support */

/*
 * These definitions and the structure layout were manually copied from
 * src/platform/vboot_reference/firmware/lib/include/rollback_index.h. at
 * git sha c7282f6.
 */
#define FWMP_NV_INDEX		    0x100a
#define FWMP_HASH_SIZE		    32
#define FWMP_DEV_DISABLE_CCD_UNLOCK (1 << 6)

/* Firmware management parameters */
struct RollbackSpaceFwmp {
	/* CRC-8 of fields following struct_size */
	uint8_t crc;
	/* Structure size in bytes */
	uint8_t struct_size;
	/* Structure version */
	uint8_t struct_version;
	/* Reserved; ignored by current reader */
	uint8_t reserved0;
	/* Flags; see enum fwmp_flags */
	uint32_t flags;
	/* Hash of developer kernel key */
	uint8_t dev_key_hash[FWMP_HASH_SIZE];
} __packed;

#ifndef CR50_DEV
static int lock_enforced(const struct RollbackSpaceFwmp *fwmp)
{
	uint8_t crc;

	/* Let's verify that the FWMP structure makes sense. */
	if (fwmp->struct_size != sizeof(*fwmp)) {
		CPRINTS("%s: fwmp size mismatch (%d)\n", __func__,
			fwmp->struct_size);
		return 1;
	}

	crc = crc8(&fwmp->struct_version, sizeof(struct RollbackSpaceFwmp) -
		   offsetof(struct RollbackSpaceFwmp, struct_version));
	if (fwmp->crc != crc) {
		CPRINTS("%s: fwmp crc mismatch\n", __func__);
		return 1;
	}

	return !!(fwmp->flags & FWMP_DEV_DISABLE_CCD_UNLOCK);
}
#endif

int board_fwmp_allows_unlock(void)
{
#ifdef CR50_DEV
	return 1;
#else
	/* Let's see if FWMP disables console activation. */
	struct RollbackSpaceFwmp fwmp;
	int allows_unlock;

	switch (read_tpm_nvmem(FWMP_NV_INDEX,
			       sizeof(struct RollbackSpaceFwmp), &fwmp)) {
	default:
		/* Something is messed up, let's not allow console unlock. */
		allows_unlock = 0;
		break;

	case tpm_read_not_found:
		allows_unlock = 1;
		break;

	case tpm_read_success:
		allows_unlock = !lock_enforced(&fwmp);
		break;
	}

	CPRINTS("Console unlock %sallowed", allows_unlock ? "" : "not ");

	return allows_unlock;
#endif
}

/****************************************************************************/
/* TPM vendor-specific commands */

static enum vendor_cmd_rc vc_lock(enum vendor_cmd_cc code,
				  void *buf,
				  size_t input_size,
				  size_t *response_size)
{
	uint8_t *buffer = buf;

	if (code == VENDOR_CC_GET_LOCK) {
		/*
		 * Get the state of the console lock.
		 *
		 *   Args: none
		 *   Returns: one byte; true (locked) or false (unlocked)
		 */
		if (input_size != 0) {
			*response_size = 0;
			return VENDOR_RC_BOGUS_ARGS;
		}

		buffer[0] = console_is_restricted() ? 0x01 : 0x00;
		*response_size = 1;
		return VENDOR_RC_SUCCESS;
	}

	/* I have no idea what you're talking about */
	*response_size = 0;
	return VENDOR_RC_NO_SUCH_COMMAND;
}
DECLARE_VENDOR_COMMAND(VENDOR_CC_GET_LOCK, vc_lock);

/*
 * TODO(rspangler): The old concept of 'lock the console' really meant
 * something closer to 'reset CCD config', not the CCD V1 meaning of 'ccdlock'.
 * This command is no longer supported, so will fail.  It was defined this
 * way:
 *
 * DECLARE_VENDOR_COMMAND(VENDOR_CC_SET_LOCK, vc_lock);
 */
