/*
 * Copyright (C) 2008 Tobias Brunner
 * HSR Hochschule fuer Technik Rapperswil
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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 "openssl_crypter.h"

#include <openssl/evp.h>

typedef struct private_openssl_crypter_t private_openssl_crypter_t;

/**
 * Private data of openssl_crypter_t
 */
struct private_openssl_crypter_t {

	/**
	 * Public part of this class.
	 */
	openssl_crypter_t public;

	/*
	 * the key
	 */
	chunk_t	key;

	/*
	 * the cipher to use
	 */
	const EVP_CIPHER *cipher;
};

/**
 * Look up an OpenSSL algorithm name and validate its key size
 */
static char* lookup_algorithm(uint16_t ikev2_algo, size_t *key_size)
{
	struct {
		/* identifier specified in IKEv2 */
		int ikev2_id;
		/* name of the algorithm, as used in OpenSSL */
		char *name;
		/* default key size in bytes */
		size_t key_def;
		/* minimum key size */
		size_t key_min;
		/* maximum key size */
		size_t key_max;
	} mappings[] = {
		{ENCR_DES, 			"des-cbc",		 8,		 8,		  8},
		{ENCR_3DES, 		"des-ede3-cbc",	24,		24,		 24},
		{ENCR_RC5, 			"rc5-cbc",		16,		 5,		255},
		{ENCR_IDEA, 		"idea-cbc",		16,		16,		 16},
		{ENCR_CAST, 		"cast5-cbc",	16,		 5,		 16},
		{ENCR_BLOWFISH, 	"bf-cbc",		16,		 5,		 56},
	};
	int i;

	for (i = 0; i < countof(mappings); i++)
	{
		if (ikev2_algo == mappings[i].ikev2_id)
		{
			/* set the key size if it is not set */
			if (*key_size == 0)
			{
				*key_size = mappings[i].key_def;
			}
			/* validate key size */
			if (*key_size < mappings[i].key_min ||
				*key_size > mappings[i].key_max)
			{
				return NULL;
			}
			return mappings[i].name;
		}
	}
	return NULL;
}

/**
 * Do the actual en/decryption in an EVP context
 */
static bool crypt(private_openssl_crypter_t *this, chunk_t data, chunk_t iv,
				  chunk_t *dst, int enc)
{
	EVP_CIPHER_CTX *ctx;
	int len;
	u_char *out;
	bool success = FALSE;

	out = data.ptr;
	if (dst)
	{
		*dst = chunk_alloc(data.len);
		out = dst->ptr;
	}
	ctx = EVP_CIPHER_CTX_new();
	if (EVP_CipherInit_ex(ctx, this->cipher, NULL, NULL, NULL, enc) &&
		EVP_CIPHER_CTX_set_padding(ctx, 0) /* disable padding */ &&
		EVP_CIPHER_CTX_set_key_length(ctx, this->key.len) &&
		EVP_CipherInit_ex(ctx, NULL, NULL, this->key.ptr, iv.ptr, enc) &&
		EVP_CipherUpdate(ctx, out, &len, data.ptr, data.len) &&
		/* since padding is disabled this does nothing */
		EVP_CipherFinal_ex(ctx, out + len, &len))
	{
		success = TRUE;
	}
	EVP_CIPHER_CTX_free(ctx);
	return success;
}

METHOD(crypter_t, decrypt, bool,
	private_openssl_crypter_t *this, chunk_t data, chunk_t iv, chunk_t *dst)
{
	return crypt(this, data, iv, dst, 0);
}

METHOD(crypter_t, encrypt, bool,
	private_openssl_crypter_t *this, chunk_t data, chunk_t iv, chunk_t *dst)
{
	return crypt(this, data, iv, dst, 1);
}

METHOD(crypter_t, get_block_size, size_t,
	private_openssl_crypter_t *this)
{
	return EVP_CIPHER_block_size(this->cipher);
}

METHOD(crypter_t, get_iv_size, size_t,
	private_openssl_crypter_t *this)
{
	return EVP_CIPHER_iv_length(this->cipher);
}

METHOD(crypter_t, get_key_size, size_t,
	private_openssl_crypter_t *this)
{
	return this->key.len;
}

METHOD(crypter_t, set_key, bool,
	private_openssl_crypter_t *this, chunk_t key)
{
	memcpy(this->key.ptr, key.ptr, min(key.len, this->key.len));
	return TRUE;
}

METHOD(crypter_t, destroy, void,
	private_openssl_crypter_t *this)
{
	chunk_clear(&this->key);
	free(this);
}

/*
 * Described in header
 */
openssl_crypter_t *openssl_crypter_create(encryption_algorithm_t algo,
												  size_t key_size)
{
	private_openssl_crypter_t *this;

	INIT(this,
		.public = {
			.crypter = {
				.encrypt = _encrypt,
				.decrypt = _decrypt,
				.get_block_size = _get_block_size,
				.get_iv_size = _get_iv_size,
				.get_key_size = _get_key_size,
				.set_key = _set_key,
				.destroy = _destroy,
			},
		},
	);

	switch (algo)
	{
		case ENCR_NULL:
			this->cipher = EVP_enc_null();
			key_size = 0;
			break;
		case ENCR_AES_CBC:
			switch (key_size)
			{
				case 0:
					key_size = 16;
					/* FALL */
				case 16:        /* AES 128 */
					this->cipher = EVP_get_cipherbyname("aes-128-cbc");
					break;
				case 24:        /* AES-192 */
					this->cipher = EVP_get_cipherbyname("aes-192-cbc");
					break;
				case 32:        /* AES-256 */
					this->cipher = EVP_get_cipherbyname("aes-256-cbc");
					break;
				default:
					free(this);
					return NULL;
			}
			break;
		case ENCR_AES_ECB:
			switch (key_size)
			{
				case 0:
					key_size = 16;
					/* FALL */
				case 16:        /* AES 128 */
					this->cipher = EVP_get_cipherbyname("aes-128-ecb");
					break;
				case 24:        /* AES-192 */
					this->cipher = EVP_get_cipherbyname("aes-192-ecb");
					break;
				case 32:        /* AES-256 */
					this->cipher = EVP_get_cipherbyname("aes-256-ecb");
					break;
				default:
					free(this);
					return NULL;
			}
			break;
		case ENCR_CAMELLIA_CBC:
			switch (key_size)
			{
				case 0:
					key_size = 16;
					/* FALL */
				case 16:        /* CAMELLIA 128 */
					this->cipher = EVP_get_cipherbyname("camellia-128-cbc");
					break;
				case 24:        /* CAMELLIA 192 */
					this->cipher = EVP_get_cipherbyname("camellia-192-cbc");
					break;
				case 32:        /* CAMELLIA 256 */
					this->cipher = EVP_get_cipherbyname("camellia-256-cbc");
					break;
				default:
					free(this);
					return NULL;
			}
			break;
#ifndef OPENSSL_NO_DES
		case ENCR_DES_ECB:
			key_size = 8;
			this->cipher = EVP_des_ecb();
			break;
#endif
		default:
		{
			char* name;

			name = lookup_algorithm(algo, &key_size);
			if (!name)
			{
				/* algo unavailable or key_size invalid */
				free(this);
				return NULL;
			}
			this->cipher = EVP_get_cipherbyname(name);
			break;
		}
	}

	if (!this->cipher)
	{
		/* OpenSSL does not support the requested algo */
		free(this);
		return NULL;
	}

	this->key = chunk_alloc(key_size);

	return &this->public;
}
