#include <crypto/cryptodev.h>
#include <stdio.h>
#include <string.h>
#include <openssl/aes.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/opensslv.h>

//#define DEBUG

#ifdef DEBUG
#  define dbgp(...) { \
	fprintf(stderr, "%s:%d: ", __FILE__, __LINE__); \
	fprintf(stderr, __VA_ARGS__); \
	fprintf(stderr, "\n"); \
}
#else
#  define dbgp(...) /* nothing */
#endif

enum ctx_type {
	ctx_type_none = 0,
	ctx_type_hmac,
	ctx_type_md,
};

#if OPENSSL_VERSION_NUMBER >= 0x10100000L
union openssl_ctx {
	HMAC_CTX *hmac;
	EVP_MD_CTX *md;
};
#else
union openssl_ctx {
	HMAC_CTX hmac;
	EVP_MD_CTX md;
};
#endif

struct ctx_mapping {
	__u32 ses;
	enum ctx_type type;
	union openssl_ctx ctx;
};

static struct ctx_mapping ctx_map[512];

static struct ctx_mapping *find_mapping(__u32 ses)
{
	int i;

	for (i = 0; i < 512; i++) {
		if (ctx_map[i].ses == ses)
			return &ctx_map[i];
	}
	return NULL;
}

static struct ctx_mapping *new_mapping(void)
{
	return find_mapping(0);
}

static void remove_mapping(__u32 ses)
{
	struct ctx_mapping *mapping;

	if (!(mapping = find_mapping(ses))) {
		printf("%s: failed to find mapping for session %d\n", __func__, ses);
		return;
	}
	switch (mapping->type) {
	case ctx_type_none:
		break;
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
	case ctx_type_hmac:
		dbgp("%s: calling HMAC_CTX_free\n", __func__);
		HMAC_CTX_free(mapping->ctx.hmac);
		break;
	case ctx_type_md:
		dbgp("%s: calling EVP_MD_CTX_free\n", __func__);
		EVP_MD_CTX_free(mapping->ctx.md);
		break;
#else
	case ctx_type_hmac:
		dbgp("%s: calling HMAC_CTX_cleanup\n", __func__);
		HMAC_CTX_cleanup(&mapping->ctx.hmac);
		break;
	case ctx_type_md:
		dbgp("%s: calling EVP_MD_CTX_cleanup\n", __func__);
		EVP_MD_CTX_cleanup(&mapping->ctx.md);
		break;
#endif
	}
	memset(mapping, 0, sizeof(*mapping));
}

static union openssl_ctx *__ses_to_ctx(__u32 ses)
{
	struct ctx_mapping *mapping;

	if (!(mapping = find_mapping(ses)))
		return NULL;
	return &mapping->ctx;
}

static HMAC_CTX *ses_to_hmac(__u32 ses) { return (HMAC_CTX *)__ses_to_ctx(ses); }
static EVP_MD_CTX *ses_to_md(__u32 ses) { return (EVP_MD_CTX *)__ses_to_ctx(ses); }

static const EVP_MD *sess_to_evp_md(struct session_op *sess)
{
	switch (sess->mac) {
#ifndef OPENSSL_NO_MD5
	case CRYPTO_MD5_HMAC: return EVP_md5();
#endif
#ifndef OPENSSL_NO_SHA
	case CRYPTO_SHA1_HMAC:
	case CRYPTO_SHA1:
		return EVP_sha1();
#endif
#ifndef OPENSSL_NO_RIPEMD
	case CRYPTO_RIPEMD160_HMAC: return EVP_ripemd160();
#endif
#ifndef OPENSSL_NO_SHA256
	case CRYPTO_SHA2_256_HMAC: return EVP_sha256();
#endif
#ifndef OPENSSL_NO_SHA512
	case CRYPTO_SHA2_384_HMAC: return EVP_sha384();
	case CRYPTO_SHA2_512_HMAC: return EVP_sha512();
#endif
	default:
		printf("%s: failed to get an EVP, things will be broken!\n", __func__);
		return NULL;
	}
}

static int openssl_hmac(struct session_op *sess, struct crypt_op *cop)
{
	HMAC_CTX *ctx = ses_to_hmac(sess->ses);

	if (!ctx) {
		struct ctx_mapping *mapping = new_mapping();
		if (!mapping) {
			printf("%s: failed to get new mapping\n", __func__);
			return 1;
		}

		mapping->ses = sess->ses;
		mapping->type = ctx_type_hmac;
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
		ctx = mapping->ctx.hmac;

		dbgp("calling HMAC_CTX_new");
		ctx = HMAC_CTX_new();
#else
		ctx = &mapping->ctx.hmac;

		dbgp("calling HMAC_CTX_init");
		HMAC_CTX_init(ctx);
#endif
		dbgp("calling HMAC_Init_ex");
		if (!HMAC_Init_ex(ctx, sess->mackey, sess->mackeylen,
				sess_to_evp_md(sess), NULL)) {
			printf("%s: HMAC_Init_ex failed\n", __func__);
			return 1;
		}
	}

	if (cop->len) {
		dbgp("calling HMAC_Update");
		if (!HMAC_Update(ctx, cop->src, cop->len)) {
			printf("%s: HMAC_Update failed\n", __func__);
			return 1;
		}
	}
	if (cop->flags & COP_FLAG_FINAL ||
	    (cop->len && !(cop->flags & COP_FLAG_UPDATE))) {
		dbgp("calling HMAC_Final");
		if (!HMAC_Final(ctx, cop->mac, 0)) {
			printf("%s: HMAC_Final failed\n", __func__);
			remove_mapping(sess->ses);
			return 1;
		}
		remove_mapping(sess->ses);
	}
	return 0;
}

static int openssl_md(struct session_op *sess, struct crypt_op *cop)
{
	EVP_MD_CTX *ctx = ses_to_md(sess->ses);

	if (!ctx) {
		struct ctx_mapping *mapping = new_mapping();
		if (!mapping) {
			printf("%s: failed to get new mapping\n", __func__);
			return 1;
		}

		mapping->ses = sess->ses;
		mapping->type = ctx_type_md;
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
		ctx = mapping->ctx.md;

		dbgp("calling EVP_MD_CTX_new");
		ctx = EVP_MD_CTX_new();
#else
		ctx = &mapping->ctx.md;

		dbgp("calling EVP_MD_CTX_init");
		EVP_MD_CTX_init(ctx);
#endif
		dbgp("calling EVP_DigestInit");
		EVP_DigestInit(ctx, sess_to_evp_md(sess));
	}

	if (cop->len) {
		dbgp("calling EVP_DigestUpdate");
		EVP_DigestUpdate(ctx, cop->src, cop->len);
	}
	if (cop->flags & COP_FLAG_FINAL ||
	    (cop->len && !(cop->flags & COP_FLAG_UPDATE))) {
		dbgp("calling EVP_DigestFinal");
		EVP_DigestFinal(ctx, cop->mac, 0);
		remove_mapping(sess->ses);
	}

	return 0;
}

static int openssl_aes(struct session_op *sess, struct crypt_op *cop)
{
	AES_KEY key;
	int i, enc;
	unsigned char ivec[AES_BLOCK_SIZE];

	if (cop->len % AES_BLOCK_SIZE) {
		printf("%s: illegal length passed, "
		       "not a multiple of AES_BLOCK_SIZE\n", __func__);
		return 1;
	}

	switch (cop->op) {
	case COP_ENCRYPT:
		AES_set_encrypt_key(sess->key, sess->keylen * 8, &key);
		enc = 1;
		break;
	case COP_DECRYPT:
		AES_set_decrypt_key(sess->key, sess->keylen * 8, &key);
		enc = 0;
		break;
	default:
		printf("%s: unknown cop->op received!\n", __func__);
		return 1;
	}

	switch (sess->cipher) {
	case CRYPTO_AES_CBC:
		memcpy(ivec, cop->iv, AES_BLOCK_SIZE);
		AES_cbc_encrypt(cop->src, cop->dst, cop->len, &key, ivec, enc);
		if (cop->flags & COP_FLAG_WRITE_IV)
			memcpy(cop->iv, ivec, AES_BLOCK_SIZE);
		break;
#if 0
	/* XXX: TODO: implement this stuff */
	case CRYPTO_AES_CTR:
		AES_ctr128_encrypt(cop->src, cop->dst, &key, cop->iv,
	case CRYPTO_AES_XTS:
#endif
	case CRYPTO_AES_ECB:
		for (i = 0; i < cop->len; i += AES_BLOCK_SIZE)
			AES_ecb_encrypt(cop->src + i, cop->dst + i, &key, enc);
		break;
	}
	return 0;
}

int openssl_cioccrypt(struct session_op *sess, struct crypt_op *cop)
{
	if (sess->mac && sess->mackey && sess->mackeylen)
		openssl_hmac(sess, cop);
	else if (sess->mac)
		openssl_md(sess, cop);

	switch (sess->cipher) {
	case CRYPTO_AES_CBC:
	case CRYPTO_AES_CTR:
	case CRYPTO_AES_XTS:
	case CRYPTO_AES_ECB:
		openssl_aes(sess, cop);
		break;
	case 0:
		/* no encryption wanted, everythings fine */
		break;
	default:
		printf("%s: unknown cipher passed!\n", __func__);
		break;
	}

	return 0;
}
