/*
 * Copyright (c) 2008-2010 Jean-Christophe PLAGNIOL-VILLARD <plagnioj@jcrosoft.com>
 *
 * See file CREDITS for list of people who contributed to this
 * project.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <common.h>
#include <password.h>
#include <errno.h>
#include <readkey.h>
#include <fs.h>
#include <fcntl.h>
#include <digest.h>
#include <malloc.h>
#include <xfuncs.h>
#include <clock.h>
#include <stdlib.h>
#include <generated/passwd.h>
#include <crypto/pbkdf2.h>

#if defined(CONFIG_PASSWD_SUM_MD5)
#define PASSWD_SUM "md5"
#elif defined(CONFIG_PASSWD_SUM_SHA1)
#define PASSWD_SUM "sha1"
#elif defined(CONFIG_PASSWD_SUM_SHA256)
#define PASSWD_SUM "sha256"
#elif defined(CONFIG_PASSWD_SUM_SHA512)
#define PASSWD_SUM "sha512"
#else
#define PASSWD_SUM	NULL
#endif

#define PBKDF2_SALT_LEN	32
#define PBKDF2_LENGTH	64
#define PBKDF2_COUNT	10000

int password(unsigned char *passwd, size_t length, int flags, int timeout)
{
	unsigned char *buf = passwd;
	int pos = 0;
	unsigned char ch;
	uint64_t start;

	if (!passwd)
		return -EINVAL;

	start = get_time_ns();

	do {
		if (tstc()) {
			ch = getc();

			switch (ch) {
			case '\r':
			case '\n':
				*buf = '\0';
				puts("\r\n");
				return pos;
			case '\0':
			case '\t':
				continue;
			case CTL_CH('c'):
				passwd[0] = '\0';
				puts("\r\n");
				return 0;
			case CTL_CH('h'):
			case BB_KEY_DEL7:
			case BB_KEY_DEL:
				if (pos > 0) {
					if (flags & STAR)
						puts("\b \b");

					*buf = '\0';
					buf--;
					pos--;
				}
				continue;
			default:
				if (pos < length - 1) {
					if (flags & STAR)
						putchar('*');
					else if (flags & CLEAR)
						putchar(ch);

					*buf = ch;
					buf++;
					pos++;
				} else {
					if (flags & STAR)
						putchar('\a');
				}
			}
		}
	} while (!is_timeout(start, timeout * SECOND) || timeout == 0);

	return -1;
}
EXPORT_SYMBOL(password);

int is_passwd_default_enable(void)
{
	return strlen(default_passwd) > 0;
}
EXPORT_SYMBOL(is_passwd_default_enable);

int is_passwd_env_enable(void)
{
	int fd;

	fd = open(PASSWD_FILE, O_RDONLY);

	if (fd < 0)
		return 0;

	close(fd);

	return 1;
}
EXPORT_SYMBOL(is_passwd_env_enable);

int passwd_env_disable(void)
{
	return unlink(PASSWD_FILE);
}
EXPORT_SYMBOL(passwd_env_disable);

static unsigned char to_digit(unsigned char c)
{
	if (c >= '0' && c <= '9')
		c -= '0';
	else
		c -= 'a' - 10;

	return c;
}

static unsigned char to_hexa(unsigned char c)
{
	if (c < 10)
		c += '0';
	else
		c += 'a' - 10;

	return c;
}

int read_passwd(unsigned char *sum, size_t length)
{
	if (is_passwd_env_enable())
		return read_env_passwd(sum, length);
	else if (is_passwd_default_enable())
		return read_default_passwd(sum, length);
	else
		return -EINVAL;
}

int read_default_passwd(unsigned char *sum, size_t length)
{
	int i = 0;
	int len = strlen(default_passwd);
	unsigned char *buf = (unsigned char *)default_passwd;
	unsigned char c;

	if (!sum || length < 1)
		return -EINVAL;

	for (i = 0; i < len && length > 0; i++) {
		c = buf[i];
		i++;

		*sum = to_digit(c) << 4;

		c = buf[i];

		*sum |= to_digit(c);
		sum++;
		length--;
	}

	return 0;
}
EXPORT_SYMBOL(read_default_passwd);

int read_env_passwd(unsigned char *sum, size_t length)
{
	int fd;
	int ret = 0;
	unsigned char c;

	if (!sum && length < 1)
		return -EINVAL;

	fd = open(PASSWD_FILE, O_RDONLY);

	if (fd < 0)
		return fd;

	do {
		ret = read(fd, &c, sizeof(char));

		if (ret < 0)
			goto exit;

		*sum = to_digit(c) << 4;

		ret = read(fd, &c, sizeof(char));

		if (ret < 0)
			goto exit;

		*sum |= to_digit(c);
		sum++;
		length--;
	} while(length > 0);

exit:

	ret = 0;

	close(fd);

	return ret;
}
EXPORT_SYMBOL(read_env_passwd);

int write_env_passwd(unsigned char *sum, size_t length)
{
	int fd;
	unsigned char c;
	int ret = 0;

	if (!sum && length < 1)
		return -EINVAL;

	fd = open(PASSWD_DIR, O_RDONLY);

	if (fd < 0)
		mkdir(PASSWD_DIR, 644);

	close(fd);

	fd = open(PASSWD_FILE, O_WRONLY | O_CREAT, 600);

	if (fd < 0)
		return fd;

	do {
		c = to_hexa(*sum >> 4 & 0xf);

		ret = write(fd, &c, sizeof(unsigned char));

		if (ret < 0)
			goto exit;

		c = to_hexa(*sum & 0xf);

		ret = write(fd, &c, sizeof(unsigned char));

		if (ret < 0)
			goto exit;

		sum++;
		length--;
	} while(length > 0);

	ret = 0;

exit:
	close(fd);

	return ret;
}
EXPORT_SYMBOL(write_env_passwd);

static int __check_passwd(unsigned char* passwd, size_t length, int std)
{
	struct digest *d = NULL;
	unsigned char *passwd1_sum;
	unsigned char *passwd2_sum;
	int ret = 0;
	int hash_len;

	if (IS_ENABLED(CONFIG_PASSWD_CRYPTO_PBKDF2)) {
		hash_len = PBKDF2_LENGTH;
	} else {
		d = digest_alloc(PASSWD_SUM);

		hash_len = digest_length(d);
	}

	passwd1_sum = calloc(hash_len * 2, sizeof(unsigned char));
	if (!passwd1_sum)
		return -ENOMEM;

	passwd2_sum = passwd1_sum + hash_len;

	if (std)
		ret = read_env_passwd(passwd2_sum, hash_len);
	else
		ret = read_default_passwd(passwd2_sum, hash_len);

	if (ret < 0)
		goto err;

	if (IS_ENABLED(CONFIG_PASSWD_CRYPTO_PBKDF2)) {
		char *key = passwd2_sum + PBKDF2_SALT_LEN;
		char *salt = passwd2_sum;
		int keylen = PBKDF2_LENGTH - PBKDF2_SALT_LEN;

		ret = pkcs5_pbkdf2_hmac_sha1(passwd, length, salt,
			PBKDF2_SALT_LEN, PBKDF2_COUNT, keylen, passwd1_sum);
		if (ret)
			goto err;

		if (strncmp(passwd1_sum, key, keylen) == 0)
			ret = 1;
	} else {
		ret = digest_digest(d, passwd, length, passwd1_sum);

		if (ret)
			goto err;

		if (strncmp(passwd1_sum, passwd2_sum, hash_len) == 0)
			ret = 1;
	}

err:
	free(passwd1_sum);
	digest_free(d);

	return ret;
}

int check_default_passwd(unsigned char* passwd, size_t length)
{
	return __check_passwd(passwd, length, 0);
}
EXPORT_SYMBOL(check_default_passwd);

int check_env_passwd(unsigned char* passwd, size_t length)
{
	return __check_passwd(passwd, length, 1);
}
EXPORT_SYMBOL(check_env_passwd);

int check_passwd(unsigned char* passwd, size_t length)
{
	if (is_passwd_env_enable())
		return check_env_passwd(passwd, length);
	else if (is_passwd_default_enable())
		return check_default_passwd(passwd, length);
	else
		return -EINVAL;
}

int set_env_passwd(unsigned char* passwd, size_t length)
{
	struct digest *d = NULL;
	unsigned char *passwd_sum;
	int ret, hash_len;

	if (IS_ENABLED(CONFIG_PASSWD_CRYPTO_PBKDF2)) {
		hash_len = PBKDF2_LENGTH;
	} else {
		d = digest_alloc(PASSWD_SUM);

		hash_len = digest_length(d);
	}

	passwd_sum = calloc(hash_len, sizeof(unsigned char));
	if (!passwd_sum)
		return -ENOMEM;

	if (IS_ENABLED(CONFIG_PASSWD_CRYPTO_PBKDF2)) {
		char *key = passwd_sum + PBKDF2_SALT_LEN;
		char *salt = passwd_sum;
		int keylen = PBKDF2_LENGTH - PBKDF2_SALT_LEN;

		get_random_bytes(passwd_sum, PBKDF2_SALT_LEN);

		ret = pkcs5_pbkdf2_hmac_sha1(passwd, length, salt,
				PBKDF2_SALT_LEN, PBKDF2_COUNT, keylen, key);
	} else {
		ret = digest_digest(d, passwd, length, passwd_sum);
	}
	if (ret)
		goto err;

	ret = write_env_passwd(passwd_sum, hash_len);

err:
	digest_free(d);
	free(passwd_sum);

	return ret;
}
EXPORT_SYMBOL(set_env_passwd);
