/**
 * Copyright (C) 2006 International Business Machines Corp.
 *   Author(s): Michael A. Halcrow <mahalcro@us.ibm.com>
 *		Tyler Hicks <tyhicks@ou.edu>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

#include <errno.h>
#include <gcrypt.h>
#include <mntent.h>
#ifndef S_SPLINT_S
#include <stdio.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>
#include <gcrypt.h>
#include <getopt.h>
#include <keyutils.h>
#include "../include/ecryptfs.h"

void ecryptfs_get_versions(int *major, int *minor, int *file_version)
{
	*major = ECRYPTFS_VERSION_MAJOR;
	*minor = ECRYPTFS_VERSION_MINOR;
	*file_version = ECRYPTFS_SUPPORTED_FILE_VERSION;
}

inline void to_hex(char *dst, char *src, int src_size)
{
	int x;

	for (x = 0; x < src_size; x++)
		sprintf(&dst[x*2], "%.2x", (unsigned char)src[x] );
	dst[src_size*2] = '\0';
}

void from_hex(char *dst, char *src, int dst_size)
{
        int x;
        char tmp[3] = { 0, };

        for (x = 0; x < dst_size; x++) {
                tmp[0] = src[x * 2];
                tmp[1] = src[x * 2 + 1];
                dst[x] = (char)strtol(tmp, NULL, 16);
        }
}

int do_hash(char * src, int src_size, char * dst, int algo)
{
	gcry_md_hd_t hd;
	gcry_error_t err = 0;
	unsigned char * hash;
	unsigned int mdlen;

	err = gcry_md_open(&hd, algo, 0);
	mdlen = gcry_md_get_algo_dlen(algo);
	if (err) {
		syslog(LOG_ERR, "Failed to open hash algo [%d]: "
		       "[%d]\n", algo, err);
		goto out;
	}
	gcry_md_write(hd, src, src_size);
	hash = gcry_md_read(hd, algo);
	memcpy(dst, hash, mdlen);
	gcry_md_close(hd);
 out:
	return (int) err;
}

/**
 * TODO: We need to support more hash algs
 *
 * @passphrase A NULL-terminated char array
 *
 * @salt A salt
 *
 * @passphrase_sig An allocated char array into which the generated
 * signature is written; PASSWORD_SIG_SIZE bytes should be allocated
 *
 */
int
generate_passphrase_sig(char *passphrase_sig, char *passphrase, char *salt,
			char *session_key_encryption_key)
{
	char salt_and_passphrase[ECRYPTFS_MAX_PASSPHRASE_BYTES
					  + ECRYPTFS_SALT_SIZE];
	int passphrase_size;
	int alg = GCRY_MD_SHA512;
	int dig_len = SHA512_DIGEST_LENGTH;
	char buf[SHA512_DIGEST_LENGTH];
	int hash_iterations = ECRYPTFS_DEFAULT_NUM_HASH_ITERATIONS;
	int rc = 0;

	passphrase_size = strlen(passphrase);
	if (passphrase_size > ECRYPTFS_MAX_PASSPHRASE_BYTES) {
		passphrase_sig = NULL;
		syslog(LOG_ERR, "Passphrase too large (%d bytes)\n",
		       passphrase_size);
		return -EINVAL;
	}
	memcpy(salt_and_passphrase, salt, ECRYPTFS_SALT_SIZE);
	memcpy((salt_and_passphrase + ECRYPTFS_SALT_SIZE), passphrase,
		passphrase_size);
	if ((rc = do_hash(salt_and_passphrase,
			  (ECRYPTFS_SALT_SIZE + passphrase_size), buf, alg))) {
		return rc;
	}
	hash_iterations--;
	while (hash_iterations--) {
		if ((rc = do_hash(buf, dig_len, buf, alg))) {
			return rc;
		}
	}
	memcpy(session_key_encryption_key, buf, ECRYPTFS_MAX_KEY_BYTES);
	if ((rc = do_hash(buf, dig_len, buf, alg))) {
		return rc;
	}
	to_hex(passphrase_sig, buf, ECRYPTFS_SIG_SIZE);
	return 0;
}

/**
 * @return Zero on success
 */
int
generate_payload(struct ecryptfs_auth_tok *auth_tok, char *passphrase_sig,
		 char *salt, char *session_key_encryption_key)
{
	int rc = 0;
	unsigned char major, minor;

	memset(auth_tok, 0, sizeof(struct ecryptfs_auth_tok));
	rc = generate_version_from_string(&major, &minor, VERSION);
	auth_tok->version = (((uint16_t)(major << 8) & 0xFF00)
			     | ((uint16_t)minor & 0x00FF));
	auth_tok->token_type = ECRYPTFS_PASSWORD;
	strncpy(auth_tok->token.password.signature, passphrase_sig,
		ECRYPTFS_PASSWORD_SIG_SIZE);
	memcpy(auth_tok->token.password.salt, salt, ECRYPTFS_SALT_SIZE);
	memcpy(auth_tok->token.password.session_key_encryption_key,
	       session_key_encryption_key, ECRYPTFS_MAX_KEY_BYTES);
	/* TODO: Make the hash parameterizable via policy */
	auth_tok->token.password.session_key_encryption_key_bytes =
		ECRYPTFS_MAX_KEY_BYTES;
	auth_tok->token.password.flags |=
		ECRYPTFS_SESSION_KEY_ENCRYPTION_KEY_SET;
	/* The kernel code will encrypt the session key. */
	auth_tok->session_key.encrypted_key[0] = 0;
	auth_tok->session_key.encrypted_key_size = 0;
	/* Default; subject to change by kernel eCryptfs */
	auth_tok->token.password.hash_algo = PGP_DIGEST_ALGO_SHA512;
	auth_tok->token.password.flags &= ~(ECRYPTFS_PERSISTENT_PASSWORD);
	return rc;
}

/**
 * @return Zero on success
 */
int
generate_private_payload(struct ecryptfs_auth_tok *auth_tok,
			 struct ecryptfs_pki_elem *selected_pki,
			 char *passphrase_sig, int pki_data_len)
{
	size_t key_size;
	unsigned char major, minor;
	int rc = 0;

	memset(auth_tok, 0, sizeof(struct ecryptfs_auth_tok) + pki_data_len);
	rc = generate_version_from_string(&major, &minor, VERSION);
	auth_tok->version = (((uint16_t)(major << 8) & 0xFF00)
			     | ((uint16_t)minor & 0x00FF));
	auth_tok->token_type = ECRYPTFS_PRIVATE_KEY;
	rc = (selected_pki->ops.set_pki_data)(&selected_pki->nvp_head,
					      auth_tok->token.private_key.data);
	if (rc)
		goto out;
	rc = (selected_pki->ops.set_key_data)(auth_tok->token.private_key.data,
					      passphrase_sig, &key_size);
	if (rc)
		goto out;
	/* The pki_type should be set based on the selected pki.
	 * Currently the only offered pki is openssl */
	/* TODO: ECRYPTFS_DEFAULT_PKI (see ecryptfs.h TODO) */
	strncpy(auth_tok->token.private_key.pki_type, ECRYPTFS_PKI_OPENSSL,
		ECRYPTFS_MAX_PKI_NAME_BYTES);
	auth_tok->token.private_key.key_size = key_size;
	auth_tok->token.private_key.data_len = pki_data_len;
	strncpy(auth_tok->token.private_key.signature, passphrase_sig,
		ECRYPTFS_PASSWORD_SIG_SIZE);
out:
	return rc;
}

int ecryptfs_mount(char *source, char *target, unsigned long flags, char *opts)
{
	FILE *mtab_fd = NULL;
	struct mntent mountent;
	char cwd[MAX_PATH_SIZE];
	char *cwdptr;
	char *fullpath_source = NULL;
	char *fullpath_target = NULL;
	int i;
	int rc;

	mountent.mnt_opts = NULL;
	if (!source) {
		rc = -EINVAL;
		syslog(LOG_ERR, "Invalid source directory\n");
		goto out;
	}
	if (!target) {
		rc = -EINVAL;
		syslog(LOG_ERR, "Invalid target directory\n");
		goto out;
	}
	if (strlen(opts) > 200) {
		rc = -EINVAL;
		syslog(LOG_ERR, "Invalid mount options length\n");
		goto out;
	}
	if (mount(source, target, "ecryptfs", flags, opts)) {
		rc = -errno;
		syslog(LOG_ERR, "Failed to perform eCryptfs mount: "
		       "[%s]\n", strerror(errno));
		goto out;
	}
	mtab_fd = setmntent("/etc/mtab", "a");
	if (!mtab_fd) {
		rc = -EACCES;
		syslog(LOG_ERR, "Failed to update the mount table\n");
		goto out;
	}
	cwdptr = getcwd(cwd, MAX_PATH_SIZE);
	if (!cwdptr) {
		rc = -errno;
		syslog(LOG_ERR, "Failed to get the current working directory; "
		       "errno = [%d]\n", errno);
		goto out;
	}
	if (source[0] != '/') {
		rc = asprintf(&fullpath_source, "%s%s", cwd, source);
	} else
		rc = asprintf(&fullpath_source, "%s", source);
	if (rc == -1) {
		rc = -ENOMEM;
		fullpath_source = NULL;
		syslog(LOG_ERR, "Out of memory\n");
		goto out;
	}
	if (target[0] != '/') {
		rc = asprintf(&fullpath_target, "%s%s", cwd, target);
	} else
		rc = asprintf(&fullpath_target, "%s", target);
	if (rc == -1) {
		rc = -ENOMEM;
		fullpath_target = NULL;
		syslog(LOG_ERR, "Out of memory\n");
		goto out;
	}
	rc = 0;
	i = strlen(fullpath_source);
	if (i > 1 && fullpath_source[i - 1] == '/')
		fullpath_source[i - 1] = '\0';
	i = strlen(fullpath_target);
	if (i > 1 && fullpath_target[i - 1] == '/')
		fullpath_target[i - 1] = '\0';
	mountent.mnt_fsname = fullpath_source;
	mountent.mnt_dir = fullpath_target;
	mountent.mnt_type = "ecryptfs";
	/* we need the following byte count:
	 * 200 max for opts
	 * 23  max for strings below
	 * 1   the final \0
	 */
	mountent.mnt_opts = malloc(224);
	if (!mountent.mnt_opts) {
		rc = -ENOMEM;
		syslog(LOG_ERR, "Failed to allocate memory for mount "
		       "options\n");
		goto out;
	}
	memset(mountent.mnt_opts, 0, 224);
	/* reporting the right mount opts */
	if (flags & MS_RDONLY)
		strcat(mountent.mnt_opts,"ro");
	else
		strcat(mountent.mnt_opts,"rw");
	if (flags & MS_NOEXEC)
		strcat(mountent.mnt_opts,",noexec");
	if (flags & MS_NOSUID)
		strcat(mountent.mnt_opts,",nosuid");
	if (flags & MS_NODEV)
		strcat(mountent.mnt_opts,",nodev");
	if (opts) {
		strcat(mountent.mnt_opts, ",");
		strcat(mountent.mnt_opts, opts);
	}
	mountent.mnt_freq = 0;
	mountent.mnt_passno = 0;
	if (addmntent(mtab_fd, &mountent)) {
		rc = -EIO;
		syslog(LOG_ERR, "Failed to write to the mount "
		       "table\n");
		goto out;
	}
	rc = 0;
out:
	free(fullpath_source);
	free(fullpath_target);
	free(mountent.mnt_opts);
	if (mtab_fd)
		endmntent(mtab_fd);
	return rc;
}

int generate_version_from_string(unsigned char *major, unsigned char *minor,
                                 char *version)
{
        int rc = 0;
        unsigned int tmp_major, tmp_minor;

        rc = sscanf(version, "%d.%d", &tmp_major, &tmp_minor);
        if (tmp_major > 255 || tmp_minor > 255) {
                rc = 1;
                (*major) = 0;
                (*minor) = 0;
                goto out;
        }
        rc = 0;
        (*major) = (unsigned char)tmp_major;
        (*minor) = (unsigned char)tmp_minor;
out:
        return rc;
}
