/*
 * nxp_aes.c
 *
 * (C) Copyright 2008 Chris Steel, NXP Semiconductors
 *
 * 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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/crypto.h>
#include <linux/spinlock.h>
#include <crypto/algapi.h>

#include "nxp-aes.h"

#define AES_HW_KEY 0x01
#define MAX_AES_BUFFER_SIZE 2048

/* Static structures */

static struct dma_chan *dma_chan = NULL;
static struct mutex nxp_aes_mutex;

static bool nxp_aes_dma_wait_complete(dma_cookie_t cookie)
{
	enum dma_status status = DMA_ERROR;

	if ((cookie >= 0) && dma_chan)
	{
		dma_async_issue_pending(dma_chan);
		do
		{
			status = dma_sync_wait(dma_chan, cookie);
		} while (status == DMA_IN_PROGRESS);
	}
	return (status == DMA_SUCCESS);
}

static unsigned int
nxp_aes_crypt(struct nxp_aes_op *op)
{
	dma_cookie_t cookie = -EFAULT;
	int len = op->len;

	if (len)
	{
		mutex_lock (&nxp_aes_mutex);
		if (len > MAX_AES_BUFFER_SIZE)
		{
			len = MAX_AES_BUFFER_SIZE;
		}
		if ((op->mode == nxp_aes_mode_cbc) && (!op->encrypt))
		{
			memcpy(op->next_iv, op->src + (len - AES_IV_LENGTH), AES_IV_LENGTH);
		}
		nxp_adma_aes_setup(op->encrypt, op->mode, op->hw_key ? NULL : op->key, op->iv, len);
		cookie = dma_async_memcpy_buf_to_buf_crypt(dma_chan, op->dst, op->src, len);
		if (false == nxp_aes_dma_wait_complete(cookie))
		{
			BUG();
		}
		mutex_unlock (&nxp_aes_mutex);
	}
	return len;
}

static int
nxp_setkey(struct crypto_tfm *tfm, const u8 *key, unsigned int len)
{
	struct nxp_aes_op *op = crypto_tfm_ctx(tfm);

	if (len != AES_KEY_LENGTH) {
		tfm->crt_flags |= CRYPTO_TFM_RES_BAD_KEY_LEN;
		return -EINVAL;
	}

	memcpy(op->key, key, len);
	return 0;
}

static void
nxp_encrypt(struct crypto_tfm *tfm, u8 *out, const u8 *in)
{
	struct nxp_aes_op *op = crypto_tfm_ctx(tfm);

	if ((out == NULL) || (in == NULL))
		return;

	op->src     = (void *) in;
	op->dst     = (void *) out;
	op->mode    = nxp_aes_mode_ecb;
	op->len     = AES_MIN_BLOCK_SIZE;
	op->encrypt = true;
	op->hw_key  = false;

	nxp_aes_crypt(op);
}


static void
nxp_decrypt(struct crypto_tfm *tfm, u8 *out, const u8 *in)
{
	struct nxp_aes_op *op = crypto_tfm_ctx(tfm);

	if ((out == NULL) || (in == NULL))
		return;

	op->src     = (void *) in;
	op->dst     = (void *) out;
	op->mode    = nxp_aes_mode_ecb;
	op->len     = AES_MIN_BLOCK_SIZE;
	op->encrypt = false;
	op->hw_key  = false;

	nxp_aes_crypt(op);
}


static struct crypto_alg nxp_alg = {
	.cra_name               =       "aes",
	.cra_driver_name	=       "nxp-aes-128",
	.cra_priority           =       300,
	.cra_alignmask          =       15,
	.cra_flags		=	CRYPTO_ALG_TYPE_CIPHER,
	.cra_blocksize		=	AES_MIN_BLOCK_SIZE,
	.cra_ctxsize		=	sizeof(struct nxp_aes_op),
	.cra_module		=	THIS_MODULE,
	.cra_list		=	LIST_HEAD_INIT(nxp_alg.cra_list),
	.cra_u			=	{
		.cipher = {
			.cia_min_keysize	=  AES_KEY_LENGTH,
			.cia_max_keysize	=  AES_KEY_LENGTH,
			.cia_setkey		=  nxp_setkey,
			.cia_encrypt		=  nxp_encrypt,
			.cia_decrypt		=  nxp_decrypt
		}
	}
};

static int
nxp_cbc_decrypt(struct blkcipher_desc *desc,
		  struct scatterlist *dst, struct scatterlist *src,
		  unsigned int nbytes)
{
	struct nxp_aes_op *op = crypto_blkcipher_ctx(desc->tfm);
	struct blkcipher_walk walk;
	int err, ret;

	blkcipher_walk_init(&walk, dst, src, nbytes);
	err = blkcipher_walk_virt(desc, &walk);

	while((nbytes = walk.nbytes)) {
		op->src     = walk.src.virt.addr,
		op->dst     = walk.dst.virt.addr;
		op->mode    = nxp_aes_mode_cbc;
		op->len     = nbytes - (nbytes % AES_MIN_BLOCK_SIZE);
		op->encrypt = false;
		op->hw_key  = (desc->flags & AES_HW_KEY) ? true : false;
		op->next_iv = walk.iv;

		memcpy(op->iv, walk.iv, AES_IV_LENGTH);

		ret = nxp_aes_crypt(op);

		nbytes -= ret;

		err = blkcipher_walk_done(desc, &walk, nbytes);
	}

	return err;
}

static int
nxp_cbc_encrypt(struct blkcipher_desc *desc,
		  struct scatterlist *dst, struct scatterlist *src,
		  unsigned int nbytes)
{
	struct nxp_aes_op *op = crypto_blkcipher_ctx(desc->tfm);
	struct blkcipher_walk walk;
	int err, ret;

	blkcipher_walk_init(&walk, dst, src, nbytes);
	err = blkcipher_walk_virt(desc, &walk);

	while((nbytes = walk.nbytes)) {
		op->src     = walk.src.virt.addr,
		op->dst     = walk.dst.virt.addr;
		op->mode    = nxp_aes_mode_cbc;
		op->len     = nbytes - (nbytes % AES_MIN_BLOCK_SIZE);
		op->encrypt = true;
		op->hw_key  = (desc->flags & AES_HW_KEY) ? true : false;

		memcpy(op->iv, walk.iv, AES_IV_LENGTH);

		ret = nxp_aes_crypt(op);

		memcpy(walk.iv, op->dst + (ret - AES_IV_LENGTH), AES_IV_LENGTH);
		nbytes -= ret;
		err = blkcipher_walk_done(desc, &walk, nbytes);
	}

	return err;
}

static struct crypto_alg nxp_cbc_alg = {
	.cra_name		=	"cbc(aes)",
	.cra_driver_name	=	"cbc-aes-nxp-128",
	.cra_priority		=	400,
	.cra_flags		=	CRYPTO_ALG_TYPE_BLKCIPHER,
	.cra_blocksize		=	AES_MIN_BLOCK_SIZE,
	.cra_ctxsize		=	sizeof(struct nxp_aes_op),
	.cra_alignmask		=	15,
	.cra_type		=	&crypto_blkcipher_type,
	.cra_module		=	THIS_MODULE,
	.cra_list		=	LIST_HEAD_INIT(nxp_cbc_alg.cra_list),
	.cra_u			=	{
		.blkcipher = {
			.min_keysize		=	AES_KEY_LENGTH,
			.max_keysize		=	AES_KEY_LENGTH,
			.setkey			=	nxp_setkey,
			.encrypt		=	nxp_cbc_encrypt,
			.decrypt		=	nxp_cbc_decrypt,
			.ivsize			=	AES_IV_LENGTH,
		}
	}
};

static int
nxp_ecb_decrypt(struct blkcipher_desc *desc,
		  struct scatterlist *dst, struct scatterlist *src,
		  unsigned int nbytes)
{
	struct nxp_aes_op *op = crypto_blkcipher_ctx(desc->tfm);
	struct blkcipher_walk walk;
	int err, ret;

	blkcipher_walk_init(&walk, dst, src, nbytes);
	err = blkcipher_walk_virt(desc, &walk);

	while((nbytes = walk.nbytes)) {
		op->src     = walk.src.virt.addr,
		op->dst     = walk.dst.virt.addr;
		op->mode    = nxp_aes_mode_ecb;
		op->len     = nbytes - (nbytes % AES_MIN_BLOCK_SIZE);
		op->encrypt = false;
		op->hw_key  = (desc->flags & AES_HW_KEY) ? true : false;

		ret = nxp_aes_crypt(op);
		nbytes -= ret;
		err = blkcipher_walk_done(desc, &walk, nbytes);
	}

	return err;
}

static int
nxp_ecb_encrypt(struct blkcipher_desc *desc,
		  struct scatterlist *dst, struct scatterlist *src,
		  unsigned int nbytes)
{
	struct nxp_aes_op *op = crypto_blkcipher_ctx(desc->tfm);
	struct blkcipher_walk walk;
	int err, ret;

	blkcipher_walk_init(&walk, dst, src, nbytes);
	err = blkcipher_walk_virt(desc, &walk);

	while((nbytes = walk.nbytes)) {
		op->src     = walk.src.virt.addr,
		op->dst     = walk.dst.virt.addr;
		op->mode    = nxp_aes_mode_ecb;
		op->len     = nbytes - (nbytes % AES_MIN_BLOCK_SIZE);
		op->encrypt = true;
		op->hw_key  = (desc->flags & AES_HW_KEY) ? true : false;

		ret = nxp_aes_crypt(op);
		nbytes -= ret;
		ret =  blkcipher_walk_done(desc, &walk, nbytes);
	}

	return err;
}

static struct crypto_alg nxp_ecb_alg = {
	.cra_name		=	"ecb(aes)",
	.cra_driver_name	=	"ecb-aes-nxp-128",
	.cra_priority		=	400,
	.cra_flags		=	CRYPTO_ALG_TYPE_BLKCIPHER,
	.cra_blocksize		=	AES_MIN_BLOCK_SIZE,
	.cra_ctxsize		=	sizeof(struct nxp_aes_op),
	.cra_alignmask		=	15,
	.cra_type		=	&crypto_blkcipher_type,
	.cra_module		=	THIS_MODULE,
	.cra_list		=	LIST_HEAD_INIT(nxp_ecb_alg.cra_list),
	.cra_u			=	{
		.blkcipher = {
			.min_keysize		=	AES_KEY_LENGTH,
			.max_keysize		=	AES_KEY_LENGTH,
			.setkey			=	nxp_setkey,
			.encrypt		=	nxp_ecb_encrypt,
			.decrypt		=	nxp_ecb_decrypt,
		}
	}
};

static inline void nxp_ctr_increment(u8 *ctr, unsigned int bytes)
{
	unsigned int blocks = bytes / AES_MIN_BLOCK_SIZE;
	int i = AES_IV_LENGTH;

	do
	{
		unsigned int tmp;
		i--;
		tmp = ((unsigned int)ctr[i]) + (blocks & 0xff);
		ctr[i] = tmp & 0xff;
		tmp >>= 8;
		blocks >>= 8;
		blocks += tmp;
	} while (blocks && i);
}

static int
nxp_ctr_decrypt(struct blkcipher_desc *desc,
		  struct scatterlist *dst, struct scatterlist *src,
		  unsigned int nbytes)
{
	struct nxp_aes_op *op = crypto_blkcipher_ctx(desc->tfm);
	struct blkcipher_walk walk;
	int err, ret;

	blkcipher_walk_init(&walk, dst, src, nbytes);
	err = blkcipher_walk_virt(desc, &walk);

	while((nbytes = walk.nbytes)) {
		op->src     = walk.src.virt.addr,
		op->dst     = walk.dst.virt.addr;
		op->mode    = nxp_aes_mode_ctr;
		op->len     = nbytes - (nbytes % AES_MIN_BLOCK_SIZE);
		op->encrypt = false;
		op->hw_key  = (desc->flags & AES_HW_KEY) ? true : false;

		memcpy(op->iv, walk.iv, AES_IV_LENGTH);

		ret = nxp_aes_crypt(op);

		nxp_ctr_increment(walk.iv, ret);
		nbytes -= ret;

		err = blkcipher_walk_done(desc, &walk, nbytes);
	}

	return err;
}

static int
nxp_ctr_encrypt(struct blkcipher_desc *desc,
		  struct scatterlist *dst, struct scatterlist *src,
		  unsigned int nbytes)
{
	struct nxp_aes_op *op = crypto_blkcipher_ctx(desc->tfm);
	struct blkcipher_walk walk;
	int err, ret;

	blkcipher_walk_init(&walk, dst, src, nbytes);
	err = blkcipher_walk_virt(desc, &walk);

	while((nbytes = walk.nbytes)) {
		op->src     = walk.src.virt.addr,
		op->dst     = walk.dst.virt.addr;
		op->mode    = nxp_aes_mode_ctr;
		op->len     = nbytes - (nbytes % AES_MIN_BLOCK_SIZE);
		op->encrypt = true;
		op->hw_key  = (desc->flags & AES_HW_KEY) ? true : false;

		memcpy(op->iv, walk.iv, AES_IV_LENGTH);
		ret = nxp_aes_crypt(op);

		nxp_ctr_increment(walk.iv, ret);
		nbytes -= ret;
		err = blkcipher_walk_done(desc, &walk, nbytes);
	}

	return err;
}

static struct crypto_alg nxp_ctr_alg = {
	.cra_name		=	"ctr(aes)",
	.cra_driver_name	=	"ctr-aes-nxp-128",
	.cra_priority		=	400,
	.cra_flags		=	CRYPTO_ALG_TYPE_BLKCIPHER,
	.cra_blocksize		=	AES_MIN_BLOCK_SIZE,
	.cra_ctxsize		=	sizeof(struct nxp_aes_op),
	.cra_alignmask		=	15,
	.cra_type		=	&crypto_blkcipher_type,
	.cra_module		=	THIS_MODULE,
	.cra_list		=	LIST_HEAD_INIT(nxp_cbc_alg.cra_list),
	.cra_u			=	{
		.blkcipher = {
			.min_keysize		=	AES_KEY_LENGTH,
			.max_keysize		=	AES_KEY_LENGTH,
			.setkey			=	nxp_setkey,
			.encrypt		=	nxp_ctr_encrypt,
			.decrypt		=	nxp_ctr_decrypt,
			.ivsize			=	AES_IV_LENGTH,
		}
	}
};

static enum dma_state_client nxp_aes_dma_channel_add_remove(struct dma_client *client, struct dma_chan *chan, enum dma_state state)
{
	enum dma_state_client ack = DMA_DUP; /* default: take no action */

	switch (state) {
	case DMA_RESOURCE_AVAILABLE:
		if (dma_chan == NULL)
		{
			if (crypto_register_alg(&nxp_alg))
			{
				printk("error installing nxp_aes driver\n");
			}
			else if (crypto_register_alg(&nxp_ecb_alg))
			{
				printk("error installing nxp_ecb_aes driver\n");
				crypto_unregister_alg(&nxp_alg);
			}
			else if (crypto_register_alg(&nxp_cbc_alg))
			{
				printk("error installing nxp_cbc_aes driver\n");
				crypto_unregister_alg(&nxp_alg);
				crypto_unregister_alg(&nxp_ecb_alg);
			}
			else if (crypto_register_alg(&nxp_ctr_alg))
			{
				printk("error installing nxp_ctr_aes driver\n");
				crypto_unregister_alg(&nxp_alg);
				crypto_unregister_alg(&nxp_ecb_alg);
				crypto_unregister_alg(&nxp_cbc_alg);
			}
			else
			{
				printk("nxp-aes driver : loaded\n");
				dma_chan = chan;
				ack = DMA_ACK;
			}
		}
		break;
	case DMA_RESOURCE_REMOVED:
		if (dma_chan)
		{
			printk("nxp-aes driver : removed\n");
			crypto_unregister_alg(&nxp_alg);
			crypto_unregister_alg(&nxp_ecb_alg);
			crypto_unregister_alg(&nxp_cbc_alg);
			crypto_unregister_alg(&nxp_ctr_alg);
			dma_chan = NULL;
			ack = DMA_ACK;
		}
		break;
	case DMA_RESOURCE_SUSPEND:
	case DMA_RESOURCE_RESUME:
		break;
	default:
		BUG();
	}

	return ack;
}

static int __init
nxp_aes_init(void)
{
	static struct dma_client dma_client;
	int ret = 0;

	mutex_init (&nxp_aes_mutex);
	dma_client.event_callback = nxp_aes_dma_channel_add_remove;
	dma_cap_set(DMA_MEMCPY, dma_client.cap_mask);
	dma_async_client_register(&dma_client);
	dma_async_client_chan_request(&dma_client);

	return ret;
}

static void __exit
nxp_aes_exit(void)
{
	crypto_unregister_alg(&nxp_alg);
	crypto_unregister_alg(&nxp_ecb_alg);
	crypto_unregister_alg(&nxp_cbc_alg);
	dma_chan = NULL;
}

MODULE_AUTHOR("Chris Steel, NXP Semiconductors");
MODULE_DESCRIPTION("NXP PNX833x Hardware AES driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("aes");

module_init(nxp_aes_init);
module_exit(nxp_aes_exit);
