#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#include "setup.h"
#include "blockdev.h"

static int memory_unsafe = 0;

static int setup_enter(struct setup_backend *backend)
{
	int r;

	/*
	 * from here we could have sensible data in memory
	 * so protect it from being swapped out
	 */
	r = mlockall(MCL_CURRENT | MCL_FUTURE);
	if (r < 0) {
		perror("mlockall failed");
		fprintf(stderr, "WARNING!!! Possibly insecure memory. Are you root?\n");
		memory_unsafe = 1;
	}

	set_error(NULL);

	if (backend) {
		r = backend->init();
		if (r < 0)
			return r;
		if (r > 0)
			memory_unsafe = 1;
	}

	return 0;
}

static int setup_leave(struct setup_backend *backend)
{
	const char *error;

	if (backend)
		backend->exit();

	/* dangerous, we can't wipe all the memory */
	if (!memory_unsafe)
		munlockall();
}

static char *xgetpass(const char *prompt, int fd)
{
	char *pass = NULL;
	int buflen, i;

	if (isatty(fd)) {
		char *pass2 = getpass(prompt);	/* FIXME */
		if (!pass2)
			return NULL;

		pass = safe_strdup(pass2);

		memset(pass2, 0, strlen(pass2));

		return pass;
	}

	buflen = 0;
	for (i = 0; ; i++) {
		if (i >= buflen - 1) {
			buflen += 128;
			pass = safe_realloc(pass, buflen);
			if (!pass) {
				set_error("Not enough memory while "
				          "reading passphrase");
				break;
			}
		}
		if (read(fd, pass + i, 1) != 1 || pass[i] == '\n')
			break;
	}

	if (pass)
		pass[i] = '\0';
	return pass;
}

static char *get_key(struct crypt_options *options)
{
	char *key = safe_alloc(options->key_size);
	char *pass = NULL, *pass2 = NULL;

	if (!key) {
		set_error("Not enough memory to allocate key");
		goto out_err;
	}

	if (options->flags & CRYPT_FLAG_PASSPHRASE) {
		pass = xgetpass("Enter passphrase: ", options->passphrase_fd);
		if (!pass) {
			set_error("Error reading passphrase");
			goto out_err;
		}

		if (options->flags & CRYPT_FLAG_VERIFY) {
			char *pass2 = xgetpass("Verify passphrase: ",
			                       options->passphrase_fd);
			if (!pass2 || strcmp(pass, pass2) != 0) {
				set_error("Passphrases do not match");
				goto out_err;
			}
		}

		if (options->hash) {
			if (hash(NULL, options->hash, key,
			         options->key_size, pass) < 0)
				goto out_err;
		} else {
			int len = strlen(pass);

			if (len > options->key_size)
				len = options->key_size;

			memcpy(key, pass, len);

			if (len < options->key_size)
				memset(&key[len], 0, options->key_size - len);
		}
	} else {
		FILE *f;
		int r;

		if (options->key_file)
			f = fopen(options->key_file, "r");
		else
			f = fdopen(options->passphrase_fd, "r");
		if (!f) {
			char buf[128];
			set_error("Error opening key file: %s",
			          strerror_r(errno, buf, 128));
			goto out_err;
		}

		r = fread(key, 1, options->key_size, f);
		if (r < 0) {
			char buf[128];
			set_error("Could not read from key file: %s",
			          strerror_r(errno, buf, 128));
		}
		else if (r != options->key_size)
			set_error("Could not read %d bytes from key file",
			          options->key_size);

		fclose(f);

		if (r != options->key_size)
			goto out_err;
	}

out:
	if (pass)
		safe_free(pass);
	if (pass2)
		safe_free(pass2);

	return key;

out_err:
	if (key)
		safe_free(key);
	key = NULL;
	goto out;
}

uint64_t get_device_size(const char *device)
{
	char buf[128];
	uint64_t size;
	unsigned long size_small;
	int fd;

	fd = open(device, O_RDONLY);
	if (fd < 0) {
		set_error("Error opening device: %s",
		          strerror_r(errno, buf, 128));
		return 0;
	}

#ifdef BLKGETSIZE64
	if (ioctl(fd, BLKGETSIZE64, &size) >= 0) {
		size >>= SECTOR_SHIFT;
		goto out;
	}
#endif

#ifdef BLKGETSIZE
	if (ioctl(fd, BLKGETSIZE, &size_small) >= 0) {
		size = (uint64_t)size_small;
		goto out;
	}
#else
#	error "Need at least the BLKGETSIZE ioctl!"
#endif

	set_error("BLKGETSIZE ioctl failed on device: %s",
	          strerror_r(errno, buf, 128));
	size = 0;

out:
	close(fd);
	return size;
}

static int __crypt_create_device(int reload, struct setup_backend *backend,
                                 struct crypt_options *options)
{
	struct crypt_options tmp = {
		.name = options->name,
	};
	char *key = NULL;
	int r;

	r = backend->status(0, &tmp, NULL);
	if (reload) {
		if (r < 0)
			return r;
	} else {
		if (r >= 0) {
			set_error("Device already exists");
			return -EEXIST;
		}
		if (r != -ENODEV)
			return r;
	}

	if (options->key_size < 0 || options->key_size > 1024) {
		set_error("Invalid key size");
		return -EINVAL;
	}

	if (!options->size) {
		options->size = get_device_size(options->device);
		if (!options->size) {
			set_error("Not a block device");
			return -ENOTBLK;
		}
		if (options->size <= options->offset) {
			set_error("Invalid offset");
			return -EINVAL;
		}
		options->size -= options->offset;
	}

	key = get_key(options);
	if (!key)
		return -ENOENT;

	r = backend->create(reload, options, key);

	safe_free(key);

	return r;
}

static int __crypt_query_device(int details, struct setup_backend *backend,
                                struct crypt_options *options)
{
	int r = backend->status(details, options, NULL);
	if (r == -ENODEV)
		return 0;
	else if (r >= 0)
		return 1;
	else
		return r;
}

static int __crypt_resize_device(int details, struct setup_backend *backend,
                                struct crypt_options *options)
{
	struct crypt_options tmp = {
		.name = options->name,
	};
	char *key = NULL;
	int r;

	r = backend->status(1, &tmp, &key);
	if (r < 0)
		return r;

	if (!options->size) {
		options->size = get_device_size(tmp.device);
		if (!options->size) {
			set_error("Not a block device");
			return -ENOTBLK;
		}
		if (options->size <= tmp.offset) {
			set_error("Invalid offset");
			return -EINVAL;
		}
		options->size -= tmp.offset;
	}
	tmp.size = options->size;

	r = backend->create(1, &tmp, key);

	safe_free(key);

	return r;
}

static int __crypt_remove_device(int arg, struct setup_backend *backend,
                                 struct crypt_options *options)
{
	int r;

	r = backend->status(0, options, NULL);
	if (r < 0)
		return r;
	if (r > 0) {
		set_error("Device busy");
		return -EBUSY;
	}

	return backend->remove(options);
}

static int crypt_job(int (*job)(int arg, struct setup_backend *backend,
                                struct crypt_options *options),
                     int arg, struct crypt_options *options)
{
	struct setup_backend *backend;
	int r;

	backend = get_setup_backend(NULL);

	setup_enter(backend);

	if (!backend) {
		set_error("No setup backend available");
		r = -ENOSYS;
		goto out;
	}

	r = job(arg, backend, options);
out:
	setup_leave(backend);
	if (backend)
		put_setup_backend(backend);

	if (r >= 0)
		set_error(NULL);

	return r;
}

int crypt_create_device(struct crypt_options *options)
{
	return crypt_job(__crypt_create_device, 0, options);
}

int crypt_update_device(struct crypt_options *options)
{
	return crypt_job(__crypt_create_device, 1, options);
}

int crypt_resize_device(struct crypt_options *options)
{
	return crypt_job(__crypt_resize_device, 0, options);
}

int crypt_query_device(struct crypt_options *options)
{
	return crypt_job(__crypt_query_device, 1, options);
}

int crypt_remove_device(struct crypt_options *options)
{
	return crypt_job(__crypt_remove_device, 0, options);
}

void crypt_get_error(char *buf, size_t size)
{
	const char *error = get_error();

	if (!buf || size < 1)
		set_error(NULL);
	else if (error) {
		strncpy(buf, error, size - 1);
		buf[size - 1] = '\0';
		set_error(NULL);
	} else
		buf[0] = '\0';
}
