/*
 * Copyright (c) 2014, 2015, 2016 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * random.cpp
 * Kernel entropy gathering.
 */

#include <sys/mman.h>

#include <errno.h>
#include <stdlib.h>
#include <string.h>

#include <sortix/clock.h>

#include <sortix/kernel/addralloc.h>
#include <sortix/kernel/copy.h>
#include <sortix/kernel/kernel.h>
#include <sortix/kernel/kthread.h>
#include <sortix/kernel/memorymanagement.h>
#include <sortix/kernel/random.h>
#include <sortix/kernel/syscall.h>
#include <sortix/kernel/time.h>

#include "multiboot.h"

// TODO: This implementation does not gather entropy. No claims are being made
//       whatsoever that this implementation is secure. It isn't secure.

namespace Sortix {
namespace Random {

static kthread_mutex_t entropy_lock = KTHREAD_MUTEX_INITIALIZER;
static unsigned char entropy[256];
static size_t entropy_used = 0;
static size_t entropy_available = 0;
static bool any_random_seed = false;
static bool fallback = false;

void Init(multiboot_info_t* bootinfo)
{
	size_t offset = 0;
	// TODO: This assumes the multiboot structures are accessible. That
	//       assumption is wrong in general and we should map them ourselves in
	//       manner that cannot fail.
	struct multiboot_mod_list* modules =
		(struct multiboot_mod_list*) (uintptr_t) bootinfo->mods_addr;
	for ( uint32_t i = 0; i < bootinfo->mods_count; i++ )
	{
		struct multiboot_mod_list* module = &modules[i];
		// TODO: This assumes module is mapped.
		size_t mod_size = module->mod_end - module->mod_start;
		const char* cmdline = (const char*) (uintptr_t) module->cmdline;
		// TODO: This assumes cmdline is mapped.
		if ( strcmp(cmdline, "--random-seed") != 0 )
			continue;
		any_random_seed = true;
		// TODO: Make an early map facility that cannot fail and use it to map
		//       the random seed.
		// TODO: AllocateKernelAddress might invoke randomness in some way in
		//       future.
		addralloc_t addralloc;
		if ( !AllocateKernelAddress(&addralloc, mod_size) )
			PanicF("Random::Init AllocateKernelAddress failed: %m");
		addr_t physfrom = module->mod_start;
		addr_t mapat = addralloc.from;
		for ( size_t i = 0; i < mod_size; i += Page::Size() )
		{
			if ( !Memory::Map(physfrom + i, mapat + i, PROT_KREAD | PROT_KWRITE) )
				PanicF("Random::Init Memory::Map failed: %m");
		}
		Memory::Flush();
		unsigned char* seed = (unsigned char*) addralloc.from;
		for ( size_t i = 0; i < mod_size; i++ )
		{
			entropy[offset++] ^= seed[i];
			if ( entropy_available < offset )
				entropy_available = offset;
			offset %= sizeof(entropy);
		}
		explicit_bzero(seed, mod_size);
		for ( size_t i = 0; i < mod_size; i += Page::Size() )
			Memory::Unmap(mapat + i);
		Memory::Flush();
		FreeKernelAddress(&addralloc);
	}
	fallback = entropy_available < sizeof(entropy);
}

int GetFallbackStatus()
{
	// If in fallback mode, mix in the current time. No particular reason.
	(void) arc4random();

	ScopedLock lock(&entropy_lock);
	if ( !any_random_seed )
		return 1;
	if ( fallback )
		return 2;
	return 0;
}

bool HasEntropy(size_t amount)
{
	ScopedLock lock(&entropy_lock);
	if ( amount <= entropy_available )
		return true;
	// Keep mixing fallback values (current time) in the hope it's better than
	// nothing.
	if ( fallback )
		return true;
	return false;
}

void GetEntropy(void* result, size_t size)
{
	kthread_mutex_lock(&entropy_lock);
	size_t amount = size < entropy_available ? size : entropy_available;
	memcpy(result, entropy + entropy_used, amount);
	explicit_bzero(entropy + entropy_used, amount);
	entropy_used += amount;
	entropy_available -= amount;
	kthread_mutex_unlock(&entropy_lock);
	// If more entropy is needed, mix in the current time and the time since
	// boot in the hope that the unpredictability of exactly when randomness is
	// consumed introduces some entropy.
	struct
	{
		struct timespec realtime;
		struct timespec monotonic;
	} seed;
	size_t sofar = amount;
	while ( sofar < size )
	{
		seed.realtime = Time::Get(CLOCK_REALTIME);
		seed.monotonic = Time::Get(CLOCK_MONOTONIC);
		unsigned char* out = (unsigned char*) result + sofar;
		size_t left = size - sofar;
		amount = left < sizeof(seed) ? left : sizeof(seed);
		memcpy(out, &seed, amount);
		sofar += amount;
	}
	explicit_bzero(&seed, sizeof(seed));
}

} // namespace Random
} // namespace Sortix

namespace Sortix {

int sys_getentropy(void* user_buffer, size_t size)
{
	unsigned char buffer[256];
	if ( sizeof(buffer) < size )
		return errno = EIO, -1;
	arc4random_buf(buffer, size);
	if ( !CopyToUser(user_buffer, buffer, size) )
		return -1;
	return 0;
}

} // namespace Sortix
