/**
 * Copyright (C) 2006 International Business Machines Corp.
 * Author(s): Mike Halcrow <mhalcrow@us.ibm.com>
 *            Kent Yoder <kyoder@users.sf.net>
 *
 * 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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <trousers/tss.h>
#include "../include/ecryptfs.h"
#include "../include/decision_graph.h"

#define RSA_KEY_SIZE_BYTES 128
#define SHA_DIGEST_LENGTH  20

#undef DEBUG

#ifdef DEBUG
#define LOG()			fprintf(stderr, "%s\n", __FUNCTION__)
#define DBGSYSLOG(x, ...)	syslog(LOG_DEBUG, x, ##__VA_ARGS__)
#define DBG_print_hex(a,b)	print_hex(a,b)

void
print_hex( BYTE *buf, uint32_t len )
{
	uint32_t i = 0, j;

	while (i < len) {
		for (j=0; (j < 15) && (i < len); j++, i++)
			printf("%02x ", buf[i] & 0xff);
		printf("\n");
	}
}

#else
#define LOG()
#define DBGSYSLOG(x, ...)
#define DBG_print_hex(a,b)
#endif

TSS_UUID srk_uuid = TSS_UUID_SRK;

struct key_mapper {
	TSS_UUID uuid;
	TSS_HKEY hKey;
	struct key_mapper *next;
} *mapper = NULL;

struct pki_tpm {
	TSS_UUID uuid;
	char *signature;
};

inline void tpm_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]);
}

static void get_pki_data(unsigned char *data, struct pki_tpm *pki_data)
{
	LOG();
	memcpy(&pki_data->uuid, data, sizeof(TSS_UUID));
	pki_data->signature = data + sizeof(TSS_UUID);
}

static int generate_signature(BYTE *n, uint32_t nbytes, char *sig)
{
	int len, i;
	char hash[SHA_DIGEST_LENGTH];
	char *data = NULL;
	int rc = 0;
	BYTE e[] = { 1, 0, 1 }; // The e for all TPM RSA keys

	LOG();
	len = 10 + nbytes + sizeof(e);
	data = malloc(3 + len);
	if (!data) {
		syslog(LOG_ERR, "Out of memory\n");
		rc = -ENOMEM;
		goto out;
	}
	i = 0;
	data[i++] = '\x99';
	data[i++] = (char)(len >> 8);
	data[i++] = (char)len;
	data[i++] = '\x04';
	data[i++] = '\00';
	data[i++] = '\00';
	data[i++] = '\00';
	data[i++] = '\00';
	data[i++] = '\02';
	data[i++] = (char)((nbytes * 8) >> 8);
	data[i++] = (char)(nbytes * 8);
	memcpy(&data[i], n, nbytes);
	i += nbytes;
	data[i++] = (char)((sizeof(e) * 8) >> 8);
	data[i++] = (char)(sizeof(e) * 8);
	memcpy(&data[i], e, sizeof(e));
	i += sizeof(e);
	SHA1(data, len + 3, hash);
	tpm_to_hex(sig, hash, ECRYPTFS_SIG_SIZE);
	sig[ECRYPTFS_SIG_SIZE_HEX] = '\0';
 out:
	free(data);
	return rc;
}

/* TODO: If sig == NULL, just return *length; the caller can use this
 * information to allocate memory and call again. There should also be
 * a max_length parameter. -Halcrow */
int
ecryptfs_set_key_data(unsigned char *private_key_data, char *sig, int *length)
{
	struct pki_tpm pki_data;
	BYTE *n;
	uint32_t size_n;
	TSS_RESULT result;
	TSS_HCONTEXT hContext;
	TSS_HKEY hKey;
	int rc = 0;

	LOG();
	get_pki_data(private_key_data, &pki_data);

	result = Tspi_Context_Create(&hContext);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_Context_Create failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	DBG_print_hex((BYTE *)&pki_data.uuid, sizeof(TSS_UUID));
	result = Tspi_Context_GetKeyByUUID(hContext, TSS_PS_TYPE_USER,
					   pki_data.uuid, &hKey);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_Context_GetKeyByUUID failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	result = Tspi_GetAttribData(hKey, TSS_TSPATTRIB_RSAKEY_INFO,
				    TSS_TSPATTRIB_KEYINFO_RSA_MODULUS,
				    &size_n, &n);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_GetAttribUint32 failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	*length = size_n;
	rc = generate_signature(n, size_n, pki_data.signature);
	memcpy(sig, pki_data.signature, ECRYPTFS_SIG_SIZE_HEX + 1);
 out:
	return rc;
}

int ecryptfs_encrypt(int size, char *from, char *to,
		     unsigned char *private_key_data)
{
	TSS_RESULT result;
	TSS_HCONTEXT hContext;
	TSS_HKEY hKey;
	TSS_HENCDATA hEncData;
	uint32_t encdata_size;
	BYTE *encdata;
	int rc = 0;
	struct pki_tpm pki_data;

	LOG();
	get_pki_data(private_key_data, &pki_data);

	result = Tspi_Context_Create(&hContext);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_Context_Create failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	result = Tspi_Context_CreateObject(hContext, TSS_OBJECT_TYPE_ENCDATA,
					   TSS_ENCDATA_BIND, &hEncData);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_Context_CreateObject failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	result = Tspi_Context_GetKeyByUUID(hContext, TSS_PS_TYPE_USER,
					   pki_data.uuid, &hKey);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_Context_GetKeyByUUID failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	result = Tspi_Data_Bind(hEncData, hKey, size, from);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_Data_Bind failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	result = Tspi_GetAttribData(hEncData, TSS_TSPATTRIB_ENCDATA_BLOB,
				    TSS_TSPATTRIB_ENCDATABLOB_BLOB,
				    &encdata_size, &encdata);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_GetAttribData failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	memcpy(to, encdata, encdata_size);
	Tspi_Context_FreeMemory(hContext, encdata);

 out:
	return rc;
}

/* currently assumes that a single block is passed in for decryption */
int ecryptfs_decrypt(char *from, char *to, size_t *decrypted_key_size,
		     unsigned char *private_key_data)
{
	TSS_RESULT result;
	static TSS_HCONTEXT hDecryptContext = 0;
	static TSS_HPOLICY hSRKPolicy = 0;
	static TSS_HKEY hDecryptKey = 0, hSRK = 0;
	static uint32_t decryptKeySize;
	static TSS_HENCDATA hEncData;
	uint32_t encdata_size;
	BYTE *encdata;
	int rc = 0;
	struct pki_tpm pki_data;
	struct key_mapper *walker, *new_mapper;

	LOG();
	get_pki_data(private_key_data, &pki_data);

	if (!hDecryptContext) {
		result = Tspi_Context_Create(&hDecryptContext);
		if (result != TSS_SUCCESS) {
			syslog(LOG_ERR, "Tspi_Context_Create failed: %s",
			       Trspi_Error_String(result));
			rc = -1;
			goto out_uninit;
		}
		DBGSYSLOG("New TSP context: 0x%x", hDecryptContext);

		result = Tspi_Context_Connect(hDecryptContext, NULL);
		if (result != TSS_SUCCESS) {
			syslog(LOG_ERR, "Tspi_Context_Connect failed: %s",
			       Trspi_Error_String(result));
			rc = -1;
			goto out_uninit;
		}

		result = Tspi_Context_LoadKeyByUUID(hDecryptContext,
						    TSS_PS_TYPE_SYSTEM,
						    srk_uuid, &hSRK);
		if (result != TSS_SUCCESS) {
			syslog(LOG_ERR, "Tspi_Context_LoadKeyByUUID failed: %s",
			       Trspi_Error_String(result));
			rc = -1;
			goto out_uninit;
		}

		result = Tspi_GetPolicyObject(hSRK, TSS_POLICY_USAGE,
					      &hSRKPolicy);
		if (result != TSS_SUCCESS) {
			syslog(LOG_ERR, "Tspi_GetPolicyObject failed: %s",
			       Trspi_Error_String(result));
			rc = -1;
			goto out_uninit;
		}

		result = Tspi_Policy_SetSecret(hSRKPolicy,
					       TSS_SECRET_MODE_POPUP, 0, NULL);
		if (result != TSS_SUCCESS) {
			syslog(LOG_ERR, "Tspi_Policy_SetSecret failed: %s",
			       Trspi_Error_String(result));
			rc = -1;
			goto out_uninit;
		}

		result = Tspi_Context_CreateObject(hDecryptContext,
						   TSS_OBJECT_TYPE_ENCDATA,
						   TSS_ENCDATA_BIND, &hEncData);
		if (result != TSS_SUCCESS) {
			syslog(LOG_ERR, "Tspi_Context_CreateObject failed: %s",
			       Trspi_Error_String(result));
			rc = -1;
			goto out_uninit;
		}
	}

	for (walker = mapper; walker; walker = walker->next) {
		if (!memcmp(&walker->uuid, &pki_data.uuid, sizeof(TSS_UUID)))
			break;
	}

	if (!walker) {
		new_mapper = calloc(1, sizeof(struct key_mapper));
		if (!new_mapper) {
			syslog(LOG_ERR, "malloc failed: %s", strerror(errno));
			rc = -1;
			goto out;
		}

		result = Tspi_Context_GetKeyByUUID(hDecryptContext,
						   TSS_PS_TYPE_USER,
						   pki_data.uuid,
						   &new_mapper->hKey);
		if (result != TSS_SUCCESS) {
			syslog(LOG_ERR, "Tspi_Context_LoadKeyByUUID failed: %s",
			       Trspi_Error_String(result));
			rc = -1;
			goto out;
		}

		DBGSYSLOG("New key object: 0x%x", new_mapper->hKey);

		result = Tspi_Key_LoadKey(new_mapper->hKey, hSRK);
		if (result != TSS_SUCCESS) {
			syslog(LOG_ERR, "Tspi_Context_LoadKey failed: %s",
			       Trspi_Error_String(result));
			rc = -1;
			goto out;
		}

		memcpy(&new_mapper->uuid, &pki_data.uuid, sizeof(TSS_UUID));
		new_mapper->next = mapper;
		walker = mapper = new_mapper;
	}

	result = Tspi_GetAttribUint32(walker->hKey, TSS_TSPATTRIB_KEY_INFO,
				      TSS_TSPATTRIB_KEYINFO_SIZE,
				      &decryptKeySize);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_GetAttribUint32 failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	encdata_size = decryptKeySize / 8;
	result = Tspi_SetAttribData(hEncData, TSS_TSPATTRIB_ENCDATA_BLOB,
				    TSS_TSPATTRIB_ENCDATABLOB_BLOB,
				    encdata_size, (BYTE *)from);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_SetAttribData failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	result = Tspi_Data_Unbind(hEncData, walker->hKey, &encdata_size,
				  &encdata);
	if (result != TSS_SUCCESS) {
		syslog(LOG_ERR, "Tspi_Data_Unbind failed: %s",
		       Trspi_Error_String(result));
		rc = -1;
		goto out;
	}

	memcpy(to, encdata, encdata_size);
	Tspi_Context_FreeMemory(hDecryptContext, encdata);
	*decrypted_key_size = encdata_size;
	return 0;

 out_uninit:
	Tspi_Context_Close(hDecryptContext);
	hDecryptContext = 0;
	hSRKPolicy = 0;
	hDecryptKey = 0;
	hSRK = 0;
 out:
	return rc;
}

int ecryptfs_get_pki_data_length(struct ecryptfs_name_val_pair *pair)
{
	LOG();
	return sizeof(TSS_UUID) + (2 + (ECRYPTFS_SIG_SIZE_HEX + 1));
}

struct pki_nvp_map_elem {
	char *name;
	uint32_t flags;
};

static struct pki_nvp_map_elem pki_nvp_map[] = {
	{ "uuid", ECRYPTFS_ECHO },
	{ NULL, 0 }
};

int generate_name_val_list(struct ecryptfs_name_val_pair *head)
{
	int i = 0;
	int rc = 0;

	LOG();
	while (pki_nvp_map[i].name) {
		head->next = malloc(sizeof(struct ecryptfs_name_val_pair));
		if (!head->next) {
			rc = -ENOMEM;
			goto out;
		}
		head = head->next;
		head->name = pki_nvp_map[i].name;
		head->flags = pki_nvp_map[i].flags;
		i++;
	}
	head->next = NULL;
 out:
	return rc;
}

void
string_to_uuid(char *str, TSS_UUID *uuid)
{
	BYTE tmp[sizeof(unsigned long) * 2 + 1];
	unsigned long l;
	uint32_t i;

	LOG();
	tmp[sizeof(unsigned long) * 2] = '\0';

	for (i = 0; i < (sizeof(TSS_UUID) * 2); i += sizeof(unsigned long)*2) {
		memcpy(tmp, &str[i], sizeof(unsigned long) * 2);
		l = strtoul(tmp, NULL, 16);
		l = htonl(l);
		memcpy(&((BYTE *)uuid)[i/2], &l, sizeof(unsigned long));
	}
}

/* TODO: In general, we should be passing the head of the lists
 * around; the head's only job is to provide a pointer to the first
 * real element in the list. Please update this function to reflect
 * that. -Halcrow */
int
ecryptfs_set_pki_data(struct ecryptfs_name_val_pair *pair, unsigned char *data)
{
	char *uuid_str = NULL;
	TSS_UUID uuid;
	size_t i = 0;

	LOG();
	while (pair) {
		if (!pair->name)
			;
		else if (!strcmp(pair->name, "uuid"))
			uuid_str = pair->value;
		pair = pair->next;
	}
	if (uuid_str) {
		string_to_uuid(uuid_str, &uuid);
		memcpy(data, &uuid, sizeof(TSS_UUID));
	} else {
		return -1;
	}
	return 0;
}

int ecryptfs_generate_key(char *filename)
{
	return -1;
}

static int
ecryptfs_get_param_subgraph_trans_node(struct transition_node **trans,
				       uint32_t version)
{
	return -1;
}

struct ecryptfs_pki_ops ops = {
	&ecryptfs_set_key_data,
	&ecryptfs_generate_key,
	&ecryptfs_encrypt,
	&ecryptfs_decrypt,
	&ecryptfs_get_pki_data_length,
	&ecryptfs_set_pki_data,
	&ecryptfs_get_param_subgraph_trans_node
};

int init_pki(char **pki_name, struct ecryptfs_pki_elem *pki)
{
	int rc;

	LOG();
	rc = generate_name_val_list(&pki->nvp_head);
	if (rc)
		goto out;
	if (asprintf(pki_name, "tspi") == -1) {
		syslog(LOG_ERR, "Out of memory\n");
		goto out;
	}
	memcpy(&pki->ops, &ops, sizeof(struct ecryptfs_pki_ops));
 out:
	return -EINVAL;
}
