/*
 *
 *			   IPSEC for Linux
 *		         Preliminary Release
 * 
 *	 Copyright (C) 1996, 1997, John Ioannidis <ji@hol.gr>
 *
 * Changes by Angelos D. Keromytis and Niels Provos
 * ported from OpenBSD 2.2 by Petr Novak, <pn@i.cz>
 * 
 * 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
 *
 */

/* AH processing (hopefully) compliant with draft-ietf-ipsec-auth-header-03.txt */

#include <linux/config.h>
#include <asm/segment.h>
#include <asm/system.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/string.h>
#include <linux/errno.h>
#include <linux/config.h>

#include <linux/socket.h>
#include <linux/sockios.h>
#include <linux/in.h>
#include <linux/inet.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/icmp.h>
#include <linux/udp.h>
#include <net/ip.h>
#include <net/protocol.h>
#include <net/route.h>
#include <net/tcp.h>
#include <net/udp.h>
#include <net/sock.h>
#include <net/icmp.h>

#include <net/checksum.h>

#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/miscdevice.h>

#include <linux/skbuff.h>
#include <linux/proc_fs.h>
#include <linux/stat.h>

#include <net/netlink.h>
#include <unistd.h>
#include "radij.h"
#include "ipsec_encap.h"
#include "ipsec_radij.h"
#include "ipsec_netlink.h"
#include "ipsec_xform.h"
#include "ipsec_ah.h"

struct ah_hash ah_new_hash[] = {
     { ALG_AUTH_MD5, "HMAC-MD5-96",
       AH_MD5_ALEN,
       AH_HMAC_HASHLEN,
       sizeof(MD5_CTX),
       (void (*)(void *)) MD5Init,
       (void (*)(void *, __u8 *, __u16)) MD5Update,
       (void (*)(__u8 *, void *)) MD5Final
     },
     { ALG_AUTH_SHA1, "HMAC-SHA1-96",
       AH_SHA1_ALEN,
       AH_HMAC_HASHLEN,
       sizeof(SHA1_CTX),
       (void (*)(void *)) SHA1Init,
       (void (*)(void *, __u8 *, __u16)) SHA1Update,
       (void (*)(__u8 *, void *)) SHA1Final
     },
     { ALG_AUTH_RMD160, "HMAC-RIPEMD-160-96",
       AH_RMD160_ALEN,
       AH_HMAC_HASHLEN,
       sizeof(RMD160_CTX),
       (void (*)(void *)) RMD160Init,
       (void (*)(void *, __u8 *, __u16)) RMD160Update,
       (void (*)(__u8 *, void *)) RMD160Final
     }
};


#ifdef DEBUG_IPSEC_AH
static void dmp(char *s, caddr_t bb, int len)
{
	int i;
	unsigned char *b = bb;
  
	if (debug_ah & DB_AH_DMP)
	{
		printk("at %s, len=%d:", s, len);
		for (i=0; i < len; i++)
			printk(" %02x", *b++);
		printk("\n");
	}
}
#else
#define dmp(_x, _y, _z) 
#endif

int
ah_new_attach(void)
{
	printk("ah_new_attach: called.\n");
	return 0;
}

/*
 * ah_new_init() is called when an SPI is being set up. It interprets the
 * encap_msghdr present in m, and sets up the transformation data.
 */

int
ah_new_init(struct tdb *tdbp, struct xformsw *xsp, struct encap_msghdr *em)
{
	struct ah_new_xdata *xd;
	struct ah_new_xencap txd;
	struct ah_hash *thash;
	caddr_t buffer = NULL;
	int blocklen, i;

	if (em->em_msglen - EMT_SETSPI_FLEN <= AH_NEW_XENCAP_LEN)
	{
		printk("ah_new_init(): initialization failed\n");
		return EINVAL;
	}

	/* Just copy the standard fields */
	memcpy(&txd, em->em_dat, AH_NEW_XENCAP_LEN);

	/* Check whether the hash algorithm is supported */
	for (i = sizeof(ah_new_hash) / sizeof(struct ah_hash) - 1; i >= 0; i--)
		if (txd.amx_hash_algorithm == ah_new_hash[i].type)
			break;
	if (i < 0)
	{
#ifdef	DEBUG_IPSEC_AH
		if (debug_ah)
			printk("ah_new_init(): unsupported authentication algorithm %d specified\n", txd.amx_hash_algorithm);
#endif
		return EINVAL;
	}

#ifdef DEBUG_IPSEC_AH
    if (debug_ah)
      printk("ah_new_init(): initalized TDB with hash algorithm %d: %s\n",
             txd.amx_hash_algorithm, ah_new_hash[i].name);
#endif /* DEBUG_IPSEC_AH */

	thash = &ah_new_hash[i];
	blocklen = HMAC_BLOCK_LEN;

	if (txd.amx_keylen + EMT_SETSPI_FLEN + AH_NEW_XENCAP_LEN != em->em_msglen)
	{
#ifdef DEBUG_IPSEC_AH
		if (debug_ah)
			printk("ah_new_init(): message length (%d) doesn't match\n", em->em_msglen);
#endif
		return EINVAL;
	}

	tdbp->tdb_xdata = (caddr_t)kmalloc(sizeof (struct ah_new_xdata), GFP_ATOMIC);

	if (tdbp->tdb_xdata == NULL)
	{
	  printk("ah_new_init(): malloc xdata failed\n");
	  return ENOBUFS;
	}

	buffer = (caddr_t)kmalloc((txd.amx_keylen < blocklen ? blocklen : txd.amx_keylen), GFP_ATOMIC);
	if (buffer == NULL)
	{
		kfree(tdbp->tdb_xdata);
		printk("ah_new_init(): malloc buffer failed\n");
		return ENOBUFS;
	}

	memset(buffer, 0,  (txd.amx_keylen < blocklen ? blocklen : txd.amx_keylen));
	memset(tdbp->tdb_xdata, 0, sizeof (struct ah_new_xdata));
	
	xd = (struct ah_new_xdata *)tdbp->tdb_xdata;

	/* Copy the key to the buffer */
	memcpy(buffer, em->em_dat + AH_NEW_XENCAP_LEN, txd.amx_keylen);

	xd->amx_hash = thash;

	/* Shorten the key if necessary */
	if (txd.amx_keylen > blocklen)
	{
		xd->amx_hash->Init(&(xd->amx_ictx));
		xd->amx_hash->Update(&(xd->amx_ictx), buffer, txd.amx_keylen);
		memset(buffer, 0, 
			(txd.amx_keylen < blocklen ? blocklen : txd.amx_keylen));
		xd->amx_hash->Final(buffer, &(xd->amx_ictx));
	}

	/* Pointer to the transform */
	tdbp->tdb_xform = xsp;

	/* Pass name of auth algorithm for procfs */
	tdbp->tdb_authname = xd->amx_hash->name;

	xd->amx_hash_algorithm = txd.amx_hash_algorithm;
	xd->amx_rpl = AH_HMAC_INITIAL_RPL;
	xd->amx_wnd = txd.amx_wnd;
	xd->amx_bitmap = 0;

	/* Precompute the I and O pads of the HMAC */
	for (i = 0; i < blocklen; i++)
		buffer[i] ^= HMAC_IPAD_VAL;

	xd->amx_hash->Init(&(xd->amx_ictx));
	xd->amx_hash->Update(&(xd->amx_ictx), buffer, blocklen);

	for (i = 0; i < blocklen; i++)
		buffer[i] ^= (HMAC_IPAD_VAL ^ HMAC_OPAD_VAL);

	xd->amx_hash->Init(&(xd->amx_octx));
	xd->amx_hash->Update(&(xd->amx_octx), buffer, blocklen);

	memset(buffer, 0, blocklen);                    /* paranoid */
	kfree(buffer);

	memset(ipseczeroes, 0, IPSEC_ZEROES_SIZE);      /* paranoid */

	return 0;
}

int ah_new_zeroize(struct tdb *tdbp)
{
	if (tdbp->tdb_xdata)
	{
		kfree(tdbp->tdb_xdata);
		tdbp->tdb_xdata = NULL;
	}

	return 0;
}

int ah_new_print(void *xdat, char *buf)
{
	int size, ds;
	struct ah_new_xdata *xd = (struct ah_new_xdata *)xdat;

	size = sprintf(buf, "algorithm = %d (%s), win = %d", xd->amx_hash_algorithm, xd->amx_hash->name, xd->amx_wnd);
	buf += size;

	if (xd->amx_wnd >= 0)
	{
		ds = sprintf(buf, ", seq = %08lx", (unsigned long)xd->amx_rpl);
		buf += ds;
		size += ds;
		ds = sprintf(buf, ", bit = %08lx", (unsigned long)xd->amx_bitmap);
		buf += ds;
		size += ds;
	}
	
	return size;
}

int
ah_new_room(struct tdb *tp, int iphlen, int tlen, int *hr, int *tr)
{
        struct ah_new_xdata *xd = (struct ah_new_xdata *)(tp->tdb_xdata) ;

        *hr += AH_NEW_FLENGTH + AH_HMAC_RPLENGTH + xd->amx_hash->authsize;
        *tr += 0;
        return 0;
}

struct sk_buff *
ah_new_input(struct sk_buff *skb, struct tdb *tdbp)
{
	struct iphdr *ipp, ipo;
	struct ah_new_xdata *xd;
	struct ah_new *ahp;
	int iphlen, authlen, alen, proto, len, off, errc;
	
#ifdef DEBUG_IPSEC_AH
	int i;
#endif
	unsigned char *dat;
	HASH_CTX ctx;
	__u8 optval;
	__u32 btsx;
	u_char buffer[AH_ALEN_MAX];

	xd = (struct ah_new_xdata *)tdbp->tdb_xdata;
	alen = xd->amx_hash->authsize;

	len = skb->len; dat = skb->data;

	ipp = (struct iphdr *)dat;
	iphlen = ipp->ihl << 2;
	ahp = (struct ah_new *)(dat + iphlen);
	
	authlen = (ahp->ah_hl << 2) + AH_NEW_FLENGTH;
	proto = ahp->ah_nh;
	
	if (authlen != AH_NEW_FLENGTH + AH_HMAC_RPLENGTH + alen)
	{
#ifdef DEBUG_IPSEC_AH
		if (debug_ah & DB_AH_INAU)
			printk("ah_new_input: bad authenticator length %d, expected %d\n", authlen, AH_NEW_FLENGTH + AH_HMAC_HASHLEN + alen);
#endif
	}

	/* Replay window checking */
	if (xd->amx_wnd >= 0)
	{
		btsx = ntohl(ahp->ah_rpl);
		if ((errc = checkreplaywindow32(btsx, 0, &(xd->amx_rpl),  xd->amx_wnd,
			 &(xd->amx_bitmap))) != 0)
		{
			switch (errc)
			{
				case 1:
					printk("ah_new_input(): replay counter wrapped for packets from %lx to %lx, spi %08lx\n",
						(unsigned long)ipp->saddr,
						(unsigned long)ipp->daddr,
						(unsigned long)ntohl(ahp->ah_spi));
					break;

				case 2:
				case 3:
					printk("ah_new_input(): duplicate packet received, %lx->%lx spi %08lx\n",
						(unsigned long)ipp->saddr,
						(unsigned long)ipp->daddr,
						ntohl(ahp->ah_spi));
					break;
			}
		}
	}

	ipo = *ipp;
	ipo.tos = 0;
	ipo.frag_off = 0;
	ipo.ttl = 0;
	ipo.check = 0;

	/* setup the precomputed context */
	memcpy((caddr_t)&ctx, (caddr_t)&(xd->amx_ictx), xd->amx_hash->ctxsize);
	
	xd->amx_hash->Update(&ctx, (unsigned char *)&ipo, sizeof (struct iphdr));

	/* Process the IP Options */
	if (iphlen > sizeof(struct iphdr))
		for (off = sizeof(struct iphdr); off < iphlen;)
		{
			optval = ((__u8 *) ipp)[off];
			switch (optval)
			{
				case IPOPT_EOL:
					xd->amx_hash->Update(&ctx, ipseczeroes, 1);

					off = iphlen;
					break;

				case IPOPT_NOP:
					xd->amx_hash->Update(&ctx, ipseczeroes, 1);

					off++;
					break;

				case IPOPT_SEC:
				case 133:
				case 134:
					optval = ((__u8 *) ipp)[off + 1];

					xd->amx_hash->Update(&ctx, (__u8 *) ipp + off, optval);

					off += optval;
					break;

				default:
					optval = ((__u8 *) ipp)[off + 1];

					xd->amx_hash->Update(&ctx, ipseczeroes, optval);

					off += optval;
					break;
			}
		}

	xd->amx_hash->Update(&ctx, (unsigned char *)ahp, AH_NEW_FLENGTH + AH_HMAC_RPLENGTH);
	xd->amx_hash->Update(&ctx, (unsigned char *)ipseczeroes, alen);
	xd->amx_hash->Update(&ctx,  dat + iphlen + authlen, len - iphlen - authlen);
	xd->amx_hash->Final(buffer, &ctx);

	/* Setup the precomputed outer ctx */
	memcpy(&ctx, &(xd->amx_octx), xd->amx_hash->ctxsize);
	xd->amx_hash->Update(&ctx, buffer, xd->amx_hash->hashsize);
	xd->amx_hash->Final(buffer, &ctx);

	/* THE AH TEST */
	if (memcmp(buffer, ahp->ah_data, alen))
	{
#ifdef DEBUG_IPSEC_AH
		if (debug_ah & DB_AH_INAU)
		{
			printk("ah_new_input: bad auth\n");
		}
#endif
		return NULL;
	}

	/*
	 *	Discard the AH part
	 */
	 
	ipp->tot_len = htons(ntohs(ipp->tot_len) - authlen);
	ipp->protocol = proto;

	memmove((void *)(skb->data + authlen), (void *)skb->data, iphlen);

	skb_pull(skb, authlen);

	/*
	 *	Adjust pointers
	 */
	 
	len = skb->len; dat = skb->data;
	
#ifdef DEBUG_IPSEC_AH
	if (debug_ah & DB_AH_PKTRX2)
	{
		printk("ipsec_ah: new ip packet is %d bytes:", len);
		for (i = 0; i < len; i++)
			printk(" %02x", dat[i]);
		printk("\n");
	}
#endif

	skb->h.iph=(struct iphdr *)skb->data;
	skb->ip_hdr=(struct iphdr *)skb->data;

	memset(skb->proto_priv, 0, sizeof(struct options));
	
	ipp = (struct iphdr *)dat;
	ipp->check = ip_fast_csum((unsigned char *)dat, iphlen >> 2);

	return skb;
}

int ah_new_output(struct sk_buff *skb, struct tdb *tdbp)
{
	struct iphdr *ipp, ipo;
	struct ah_new_xdata *xd;
	struct ah_new *ahp;
	int iphlen, authlen, alen, len, off;
	unsigned char *dat;
	HASH_CTX ctx;
	__u8 optval;
	__u8 buffer[AH_ALEN_MAX];

	/* Take a pointer to the xform data */
	xd = (struct ah_new_xdata *)tdbp->tdb_xdata;
	alen = xd->amx_hash->authsize;
	authlen = AH_NEW_FLENGTH + AH_HMAC_RPLENGTH + alen;

	/* Setup a pointer to the IP packet header */
	ipp = (struct iphdr *)(skb->data);
	
	/* Extract the IP header length */
	iphlen = ipp->ihl << 2;

	/* Make enoung room to accomodate the AH */
	dat = skb_push(skb, authlen);
	len = skb->len;
	
	/* Copy the IP header to the start of the enlarged SKB */
	memmove((void *)skb->data, (void *)(skb->data + authlen), iphlen);

	/* Take a pointer to the newly formed AH */
	ahp = (struct ah_new *)(dat + iphlen);

	/* Update the pointer to the IP packet header */
	ipp = (struct iphdr *)dat;

	if (ntohs(ipp->frag_off) & IP_OFFSET)
	{
		printk("ah_new_output: attempted to authenticate fragment!\n"); /* XXX */
		return -1;
	}

	/* Fill in the known AH fields */
	ahp->ah_nh = ipp->protocol;
	ahp->ah_hl = (AH_HMAC_RPLENGTH + alen) >> 2;
	ahp->ah_rv = 0;
	ahp->ah_spi = tdbp->tdb_spi;

	if (xd->amx_rpl == 0)
	{
		printk("ah_new_output(): SA %lx/%08lx should have expired\n",
			(unsigned long)tdbp->tdb_dst.s_addr,
			(unsigned long)ntohl(tdbp->tdb_spi));
		return -1;
	}

	ahp->ah_rpl = htonl(xd->amx_rpl++);

	ipp->protocol = IPPROTO_AH;
	ipp->tot_len  = htons(skb->len);

	ipo = *ipp;
	ipo.tos = 0;
	ipo.frag_off = 0;
	ipo.ttl = 0;
	ipo.check = 0;

	/* Setup the hash with precomputed context */
	memcpy((caddr_t)&ctx, (caddr_t)&(xd->amx_ictx), xd->amx_hash->ctxsize);

	/* Update the hash with the sanitized IP header without options */
	xd->amx_hash->Update(&ctx, (unsigned char *)&ipo, sizeof(struct iphdr));

	/* Process the IP Options */
	if (iphlen > sizeof(struct iphdr))
		for (off = sizeof(struct iphdr); off < iphlen;)
		{
			optval = ((__u8 *) ipp)[off];
			switch (optval)
			{
				case IPOPT_EOL:
					xd->amx_hash->Update(&ctx, ipseczeroes, 1);

					off = iphlen;
					break;

				case IPOPT_NOP:
					xd->amx_hash->Update(&ctx, ipseczeroes, 1);

					off++;
					break;

				case IPOPT_SEC:
				case 133:
				case 134:
					optval = ((__u8 *) ipp)[off + 1];

					xd->amx_hash->Update(&ctx, (__u8 *) ipp + off, optval);

					off += optval;
					break;

				default:
					optval = ((__u8 *) ipp)[off + 1];

					xd->amx_hash->Update(&ctx, ipseczeroes, optval);

					off += optval;
					break;
			}
		}
    
	/* Update the hash with the AH without authenticator (but with the RPL field) */
	xd->amx_hash->Update(&ctx, (unsigned char *) ahp, AH_NEW_FLENGTH + AH_HMAC_RPLENGTH);

	/* Update the hash with zeroized authenticator */
	xd->amx_hash->Update(&ctx, ipseczeroes, alen);

	/* Update the hash with the IP payload */
	xd->amx_hash->Update(&ctx,  dat + iphlen + authlen, len - iphlen - authlen);

	/* Finalize the hash */
	xd->amx_hash->Final(buffer, &ctx);

	/* Setup the outer precomputed ctx */
	memcpy((caddr_t)&ctx, &(xd->amx_octx), xd->amx_hash->ctxsize);
	xd->amx_hash->Update(&ctx, buffer, xd->amx_hash->hashsize);
	xd->amx_hash->Final(buffer, &ctx);

	/* Copy the authenticator into place */
	memcpy(ahp->ah_data, buffer, alen);

	skb->ip_hdr = skb->h.iph = (struct iphdr *) skb->data;
	
	ipp->check = 0;
	ipp->check = ip_fast_csum((unsigned char *)ipp, ipp->ihl);

	return 0;
}
