/* 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 "common.h"
#include "console.h"
#include "flash.h"
#include "nvmem.h"
#include "task.h"
#include "timer.h"
#include "util.h"

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

#define NVMEM_NOT_INITIALIZED (-1)

/*
 * The NVMEM contents are stored in flash memory. At run time there is an SRAM
 * cache and two instances of the contents in the flash in two partitions.
 *
 * Each instance is protected by a 16 bytes hash and has a 'generation' value
 * associated with it. When NVMEM module is initialized it checks the flash
 * stored instances. If both of them are valid, it considers the newer one
 * (younger generation) to be the proper NVMEM contents and copies it to the
 * SRAM cache. If only one instance is valid, it is used, and if no instances
 * are valid - a new valid partition is created and copied into the SRAM
 * cache.
 *
 * When stored in flash, the contents are encrypted, the hash value is used as
 * the IV for the encryption routine.
 *
 * There is a mutex controlling access to the NVMEM. There are two levels
 * of protection - for read only accesses and for write accesses. When the
 * module is initialized the mutex is opened.
 *
 * If there are no pending writes, each read access locks the mutex, reads out
 * the data and unlocks the mutex, thus multiple tasks could be reading NVMEM,
 * blocking access momentarily.
 *
 * If a write access ever occurs things get more complicated. The write access
 * leaves the mutex locked and stores the flag, indicating that the
 * contents have changed and need to be saved, and stores the task id of the
 * task performing the write access.
 *
 * The mutex remains locked in this case. Next time a read access happens,
 * if it comes from the same task, the unlock in the end of the read is
 * bypassed because the 'write in progress' flag is set. If a read or write
 * request comes from another task, they  will be blocked until the first
 * task to write commits.
 *
 * nvmem_commit() calls the nvmem_save() function which checks if the cache
 * contents indeed changed (by calculating the hash again). If there is no
 * change - the mutex is released and the function exits. If there is a
 * change, the new generation value is set, the new hash is calculated
 * and the copy is saved in the least recently used flash partition, and
 * then the lock is released.
 */

/* Table of start addresses for each partition */
static const uintptr_t nvmem_base_addr[NVMEM_NUM_PARTITIONS] = {
		CONFIG_FLASH_NVMEM_BASE_A,
		CONFIG_FLASH_NVMEM_BASE_B
	};

/* NvMem user buffer start offset table */
static uint32_t nvmem_user_start_offset[NVMEM_NUM_USERS];

/* A/B partion that is most up to date */
static int nvmem_act_partition;

/* NvMem cache memory structure */
struct nvmem_mutex_ {
	task_id_t task;
	int write_in_progress;
	struct mutex mtx;
};

static struct nvmem_mutex_ nvmem_mutex = { .task = TASK_ID_COUNT };
static uint8_t nvmem_cache[NVMEM_PARTITION_SIZE] __aligned(4);

static uint8_t commits_enabled;

/* NvMem error state */
static int nvmem_error_state;
/* Flag to track if an Nv write/move is not completed */
static int nvmem_write_error;

static void nvmem_release_cache(void);

/*
 * Given the nvmem tag address calculate the sha value of the nvmem buffer and
 * save it in the provided space. The caller is expected to provide enough
 * space to store CIPHER_SALT_SIZE bytes.
 */
static void nvmem_compute_sha(struct nvmem_tag *tag, void *sha_buf)
{
	app_compute_hash(tag->padding, NVMEM_PARTITION_SIZE - NVMEM_SHA_SIZE,
			 sha_buf, sizeof(tag->sha));
}

static int nvmem_save(void)
{
	struct nvmem_partition *part;
	size_t nvmem_offset;
	int dest_partition;
	uint8_t sha_comp[NVMEM_SHA_SIZE];
	int rv = EC_SUCCESS;

	part = (struct nvmem_partition *)nvmem_cache;

	/* Has anything changed in the cache? */
	nvmem_compute_sha(&part->tag, sha_comp);

	if (!memcmp(part->tag.sha, sha_comp, sizeof(part->tag.sha))) {
		CPRINTF("%s: Nothing changed, skipping flash write\n",
			__func__);
		goto release_cache;
	}

	/* Get flash offset of the partition to save to. */
	dest_partition = (nvmem_act_partition + 1) % NVMEM_NUM_PARTITIONS;
	nvmem_offset = nvmem_base_addr[dest_partition] -
		CONFIG_PROGRAM_MEMORY_BASE;

	/* Erase partition */
	rv = flash_physical_erase(nvmem_offset, NVMEM_PARTITION_SIZE);
	if (rv != EC_SUCCESS) {
		CPRINTF("%s flash erase failed\n", __func__);
		goto release_cache;
	}

	part->tag.layout_version = NVMEM_LAYOUT_VERSION;
	part->tag.generation++;

	/* Calculate sha of the whole thing. */
	nvmem_compute_sha(&part->tag, part->tag.sha);

	/* Encrypt actual payload. */
	if (!app_cipher(part->tag.sha, part->buffer, part->buffer,
			sizeof(part->buffer))) {
		CPRINTF("%s encryption failed\n", __func__);
		rv = EC_ERROR_UNKNOWN;
		goto release_cache;
	}

	rv = flash_physical_write(nvmem_offset,
				  NVMEM_PARTITION_SIZE,
				  nvmem_cache);
	if (rv != EC_SUCCESS) {
		CPRINTF("%s flash write failed\n", __func__);
		goto release_cache;
	}

	/* Restore payload. */
	if (!app_cipher(part->tag.sha, part->buffer, part->buffer,
			sizeof(part->buffer))) {
		CPRINTF("%s decryption failed\n", __func__);
		rv = EC_ERROR_UNKNOWN;
		goto release_cache;
	}

	nvmem_act_partition = dest_partition;

 release_cache:
	nvmem_mutex.write_in_progress = 0;
	nvmem_release_cache();
	return rv;
}

/*
 * Read from flash and verify partition.
 *
 * @param index - index of the partition to verify
 *
 * Returns EC_SUCCESS on verification success
 *         EC_ERROR_BUSY in case of malloc failure
 *         EC_ERROR_UNKNOWN on failure to decrypt of verify.
 */
static int nvmem_partition_read_verify(int index)
{
	uint8_t sha_comp[NVMEM_SHA_SIZE];
	struct nvmem_partition *p_part;
	struct nvmem_partition *p_copy;
	int ret;

	p_part = (struct nvmem_partition *)nvmem_base_addr[index];
	p_copy = (struct nvmem_partition *)nvmem_cache;
	memcpy(p_copy, p_part, NVMEM_PARTITION_SIZE);

	/* Then decrypt it. */
	if (!app_cipher(p_copy->tag.sha, &p_copy->tag + 1,
			&p_copy->tag + 1,
			NVMEM_PARTITION_SIZE - sizeof(struct nvmem_tag))) {
		CPRINTF("%s: decryption failure\n", __func__);
		return EC_ERROR_UNKNOWN;
	}

	/*
	 * Check if computed value matches stored value. Nonzero 'ret' value
	 * means there was a match.
	 */
	nvmem_compute_sha(&p_copy->tag, sha_comp);
	ret = !memcmp(p_copy->tag.sha, sha_comp, NVMEM_SHA_SIZE);

	return ret ? EC_SUCCESS : EC_ERROR_UNKNOWN;
}

static void nvmem_lock_cache(void)
{
	/*
	 * Need to protect the cache contents value from other tasks
	 * attempting to do nvmem write operations. However, since this
	 * function may be called mutliple times prior to the mutex lock being
	 * released, there is a check first to see if the current task holds
	 * the lock. If it does then the task number will equal the value in
	 * cache.task, no need to wait.
	 *
	 * If the lock is held by a different task then mutex_lock function
	 * will operate as normal.
	 */
	if (nvmem_mutex.task == task_get_current())
		return;

	mutex_lock(&nvmem_mutex.mtx);
	nvmem_mutex.task = task_get_current();
}

static void nvmem_release_cache(void)
{
	if (nvmem_mutex.write_in_progress || !commits_enabled)
		return;		/* It will have to be saved first. */

	/* Reset task number to max value */
	nvmem_mutex.task = TASK_ID_COUNT;
	/* Release mutex lock here */
	mutex_unlock(&nvmem_mutex.mtx);
}

static int nvmem_reinitialize(void)
{
	nvmem_lock_cache();  /* Unlocked by nvmem_save() below. */
	/*
	 * NvMem is not properly initialized. Let's just erase everything and
	 * start over, so that at least 1 partition is ready to be used.
	 */
	nvmem_act_partition = 0;

	memset(nvmem_cache, 0xff, NVMEM_PARTITION_SIZE);

	/* Start with generation zero in the current active partition. */
	return nvmem_save();
}

static int nvmem_compare_generation(void)
{
	struct nvmem_partition *p_part;
	uint16_t ver0, ver1;
	uint32_t delta;

	p_part = (struct nvmem_partition *)nvmem_base_addr[0];
	ver0 = p_part->tag.generation;
	p_part = (struct nvmem_partition *)nvmem_base_addr[1];
	ver1 = p_part->tag.generation;

	/* Compute generation difference accounting for wrap condition */
	delta = (ver0 - ver1 + (1<<NVMEM_GENERATION_BITS)) &
		NVMEM_GENERATION_MASK;
	/*
	 * If generation number delta is positive in a circular sense then
	 * partition 0 has the newest generation number. Otherwise, it's
	 * partition 1.
	 */
	return delta < (1<<(NVMEM_GENERATION_BITS-1)) ? 0 : 1;
}

static int nvmem_find_partition(void)
{
	int n;
	int newest;

	/* Don't know which partition to use yet */
	nvmem_act_partition = NVMEM_NOT_INITIALIZED;

	/* Find the newest partition available in flash. */
	newest = nvmem_compare_generation();

	/*
	 * Find a partition with a valid sha, starting with the newest one.
	 */
	for (n = 0; n < NVMEM_NUM_PARTITIONS; n++) {
		int check_part = (n + newest) % NVMEM_NUM_PARTITIONS;

		if (nvmem_partition_read_verify(check_part) == EC_SUCCESS) {
			nvmem_act_partition = check_part;
			return EC_SUCCESS;
		}
		ccprintf("%s:%d partiton %d verification FAILED\n",
			 __func__, __LINE__, check_part);
	}

	/*
	 * If active_partition is still not selected, then neither partition
	 * is valid. Let's reinitialize the NVMEM - there is nothing else we
	 * can do.
	 */
	CPRINTS("%s: No Valid Partition found, will reinitialize!", __func__);

	if (nvmem_reinitialize() != EC_SUCCESS) {
		CPRINTS("%s: Reinitialization failed!!");
		return EC_ERROR_UNKNOWN;
	}

	return EC_SUCCESS;
}

static int nvmem_generate_offset_table(void)
{
	int n;
	uint32_t start_offset;

	/*
	 * Create table of starting offsets within partition for each user
	 * buffer that's been defined.
	 */
	start_offset = sizeof(struct nvmem_tag);
	for (n = 0; n < NVMEM_NUM_USERS; n++) {
		nvmem_user_start_offset[n] = start_offset;
		start_offset += nvmem_user_sizes[n];
	}
	/* Verify that all defined user buffers fit within the partition */
	if (start_offset > NVMEM_PARTITION_SIZE)
		return EC_ERROR_OVERFLOW;

	return EC_SUCCESS;
}
static int nvmem_get_partition_off(int user, uint32_t offset,
				   uint32_t len, uint32_t *p_buf_offset)
{
	uint32_t start_offset;

	/* Sanity check for user */
	if (user >= NVMEM_NUM_USERS)
		return EC_ERROR_OVERFLOW;

	/* Get offset within the partition for the start of user buffer */
	start_offset = nvmem_user_start_offset[user];
	/*
	 * Ensure that read/write operation that is calling this function
	 * doesn't exceed the end of its buffer.
	 */
	if (offset + len > nvmem_user_sizes[user])
		return EC_ERROR_OVERFLOW;
	/* Compute offset within the partition for the rd/wr operation */
	*p_buf_offset = start_offset + offset;

	return EC_SUCCESS;
}

int nvmem_erase_user_data(enum nvmem_users user)
{
	int part;
	int ret;
	uint32_t user_offset, user_size;

	if (user >= NVMEM_NUM_USERS)
		return EC_ERROR_INVAL;

	CPRINTS("Erasing NVMEM Flash Partition user: %d", user);

	ret = EC_SUCCESS;

	/* Find offset within cache. */
	user_offset = nvmem_user_start_offset[user];
	user_size = nvmem_user_sizes[user];

	for (part = 0; part < NVMEM_NUM_PARTITIONS; part++) {
		int rv;

		/* Lock the cache buffer. */
		nvmem_lock_cache();
		/* Erase the user's data. */
		memset(nvmem_cache + user_offset, 0xFF, user_size);

		/*
		 * Make sure the contents change between runs of
		 * nvmem_save() so that all flash partitions are
		 * written with empty contents and different
		 * generation numbers.
		 */
		((struct nvmem_partition *)nvmem_cache)->tag.generation = part;

		/* Make a best effort to clear each partition. */
		rv = nvmem_save();
		if (rv != EC_SUCCESS)
			ret = rv;
	}

	return ret;
}

int nvmem_init(void)
{
	int ret;

	/* Generate start offsets within partiion for user buffers */
	ret = nvmem_generate_offset_table();
	if (ret) {
		CPRINTF("%s:%d\n", __func__, __LINE__);
		return ret;
	}
	/* Initialize error state, assume everything is good */
	nvmem_error_state = EC_SUCCESS;
	nvmem_write_error = 0;

	/*
	 * Default policy is to allow all commits. This ensures reinitialization
	 * succeeds to bootstrap the nvmem area.
	 */
	commits_enabled = 1;
	ret = nvmem_find_partition();

	if (ret != EC_SUCCESS) {
		/* Change error state to non-zero */
		nvmem_error_state = ret;
		CPRINTF("%s:%d\n", __func__, __LINE__);
		return ret;
	}

	CPRINTS("Active Nvmem partition set to %d", nvmem_act_partition);

	return EC_SUCCESS;
}

int nvmem_get_error_state(void)
{
	return nvmem_error_state;
}

int nvmem_is_different(uint32_t offset, uint32_t size, void *data,
		       enum nvmem_users user)
{
	int ret;
	uint32_t src_offset;

	nvmem_lock_cache();

	/* Get partition offset for this read operation */
	ret = nvmem_get_partition_off(user, offset, size, &src_offset);
	if (ret != EC_SUCCESS)
		return ret;

	/* Advance to the correct byte within the data buffer */

	/* Compare NvMem with data */
	ret = memcmp(nvmem_cache + src_offset, data, size);

	nvmem_release_cache();

	return ret;
}

int nvmem_read(uint32_t offset, uint32_t size,
		    void *data, enum nvmem_users user)
{
	int ret;
	uint32_t src_offset;

	nvmem_lock_cache();

	/* Get partition offset for this read operation */
	ret = nvmem_get_partition_off(user, offset, size, &src_offset);

	if (ret == EC_SUCCESS)
		/* Copy from src into the caller's destination buffer */
		memcpy(data, nvmem_cache + src_offset, size);

	nvmem_release_cache();

	return ret;
}

int nvmem_write(uint32_t offset, uint32_t size,
		 void *data, enum nvmem_users user)
{
	int ret;
	uint8_t *p_dest;
	uint32_t dest_offset;

	/* Make sure that the cache buffer is active */
	nvmem_lock_cache();
	nvmem_mutex.write_in_progress = 1;

	/* Compute partition offset for this write operation */
	ret = nvmem_get_partition_off(user, offset, size, &dest_offset);
	if (ret != EC_SUCCESS) {
		nvmem_write_error = 1;
		return ret;
	}

	/* Advance to correct offset within data buffer */
	p_dest = nvmem_cache + dest_offset;

	/* Copy data from caller into destination buffer */
	memcpy(p_dest, data, size);

	return EC_SUCCESS;
}

int nvmem_move(uint32_t src_offset, uint32_t dest_offset, uint32_t size,
		enum nvmem_users user)
{
	int ret;
	uint8_t *p_src, *p_dest;
	uintptr_t base_addr;
	uint32_t s_buff_offset, d_buff_offset;

	/* Make sure that the cache buffer is active */
	nvmem_lock_cache();
	nvmem_mutex.write_in_progress = 1;

	/* Compute partition offset for source */
	ret = nvmem_get_partition_off(user, src_offset, size, &s_buff_offset);
	if (ret != EC_SUCCESS) {
		nvmem_write_error = 1;
		return ret;
	}

	/* Compute partition offset for destination */
	ret = nvmem_get_partition_off(user, dest_offset, size, &d_buff_offset);
	if (ret != EC_SUCCESS) {
		nvmem_write_error = 1;
		return ret;
	}

	base_addr = (uintptr_t)nvmem_cache;
	/* Create pointer to src location within partition */
	p_src = (uint8_t *)(base_addr + s_buff_offset);
	/* Create pointer to dest location within partition */
	p_dest = (uint8_t *)(base_addr + d_buff_offset);
	/* Move the data block in NvMem */
	memmove(p_dest, p_src, size);

	return EC_SUCCESS;
}

int nvmem_enable_commits(void)
{
	if (commits_enabled)
		return EC_SUCCESS;

	if (nvmem_mutex.task != task_get_current()) {
		CPRINTF("%s: locked by task %d, attempt to unlock by task %d\n",
			__func__, nvmem_mutex.task, task_get_current());
		return EC_ERROR_INVAL;
	}

	commits_enabled = 1;
	CPRINTS("Committing NVMEM changes.");
	return nvmem_commit();
}

void nvmem_disable_commits(void)
{
	/* Will be unlocked when nvmem_enable_commits() is called. */
	nvmem_lock_cache();

	commits_enabled = 0;
}

int nvmem_commit(void)
{
	if (nvmem_mutex.task == TASK_ID_COUNT) {
		CPRINTF("%s: attempt to commit in unlocked state\n",
			__func__, nvmem_mutex.task);
		return EC_ERROR_OVERFLOW;  /* Noting to commit. */
	}

	if (nvmem_mutex.task != task_get_current()) {
		CPRINTF("%s: locked by task %d, attempt to unlock by task %d\n",
			__func__, nvmem_mutex.task, task_get_current());
		return EC_ERROR_INVAL;
	}

	/* Ensure that all writes/moves prior to commit call succeeded */
	if (nvmem_write_error) {
		CPRINTS("%s: Write Error, commit abandoned", __func__);
		/* Clear error state */
		nvmem_write_error = 0;
		commits_enabled = 1;
		nvmem_release_cache();
		return EC_ERROR_UNKNOWN;
	}

	if (!commits_enabled) {
		CPRINTS("Skipping commit");
		return EC_SUCCESS;
	}

	/* Write active partition to NvMem */
	return nvmem_save();
}
