/*
 *
 *			   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
 *
 */

#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"
#ifdef CONFIG_IPSEC_AH
#include "ipsec_ah.h"
#endif

/*
 * Authentication Header Processing
 * Per RFCs 1828/1852 (Metzger & Simpson)
 */

struct ah_hash ah_old_hash[] = {
     { ALG_AUTH_MD5, "Keyed MD5",
       AH_MD5_ALEN,
       AH_MD5_ALEN,
       sizeof(MD5_CTX),
       (void (*)(void *))MD5Init,
       (void (*)(void *, __u8 *, __u16))MD5Update,
       (void (*)(__u8 *, void *))MD5Final
     },
     { ALG_AUTH_SHA1, "Keyed SHA1",
       AH_SHA1_ALEN,
       AH_SHA1_ALEN,
       sizeof(SHA1_CTX),
       (void (*)(void *))SHA1Init,
       (void (*)(void *, __u8 *, __u16))SHA1Update,
       (void (*)(__u8 *, void *))SHA1Final
     }
};

#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_old_attach(void)
{
	printk("ah_old_attach: called.\n");
	return 0;
}

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

int
ah_old_init(struct tdb *tdbp, struct xformsw *xsp, struct encap_msghdr *em)
{
	struct ah_old_xencap xenc;
	struct ah_old_xdata *xd;
	struct ah_hash *thash;
	int i;
	
	if (em->em_msglen - EMT_SETSPI_FLEN <= AH_OLD_XENCAP_LEN)
	{
		printk("ah_old_init(): initialization failed\n");
		return EINVAL;
	}

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

	/* Check wheather the hash algorithm is supported */
	for (i=sizeof(ah_old_hash)/sizeof(struct ah_hash)-1; i >= 0; i--)
		if (xenc.amx_hash_algorithm == ah_old_hash[i].type)
			break;
	if (i < 0)
	{
		printk("ah_old_init(): unsupported authentication algoritm %d specified\n", xenc.amx_hash_algorithm);
		return EINVAL;
	}

#if	DEBUG_IPSEC_AH
	if (debug_ah)
		printk("ah_old_init(): initialized TDB with hash algorithm %d: %s\n",
			xenc.amx_hash_algorithm, ah_old_hash[i].name);
#endif	/* DEBUG_IPSEC_AH */

	thash = &ah_old_hash[i];

	if (xenc.amx_keylen + EMT_SETSPI_FLEN + AH_OLD_XENCAP_LEN != em->em_msglen)
	{
		printk("ah_old_init(): message length %d doesn't match\n", em->em_msglen);
		return EINVAL;
	}

	tdbp->tdb_xdata = (caddr_t)kmalloc(sizeof (struct ah_old_xdata) + xenc.amx_keylen, GFP_ATOMIC);

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

	memset(tdbp->tdb_xdata, 0, sizeof (struct ah_old_xdata) + xenc.amx_keylen);
	xd = (struct ah_old_xdata *)tdbp->tdb_xdata;

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

	xd->amx_keylen = xenc.amx_keylen;
	xd->amx_hash_algorithm = xenc.amx_hash_algorithm;
	xd->amx_hash = thash;

	tdbp->tdb_authname = xd->amx_hash->name;

	/* Copy the key material */
	memcpy((caddr_t)xd->amx_key, (caddr_t)(em->em_dat)+AH_OLD_XENCAP_LEN, xd->amx_keylen);

	xd->amx_hash->Init(&(xd->amx_ctx));
	xd->amx_hash->Update(&(xd->amx_ctx), xd->amx_key, xd->amx_keylen);
	xd->amx_hash->Final(NULL, &(xd->amx_ctx));
	memset(ipseczeroes, 0, IPSEC_ZEROES_SIZE);		/* paranoid */

	return 0;
}

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

int ah_old_print(void *xdat, char *buf)
{
	int size;
#ifdef AHPRINTKEYS
	int ds, i;
#endif
	struct ah_old_xdata *xd = (struct ah_old_xdata *)xdat;
	size = sprintf(buf, "algorithm = %d (%s), keylen = %d", xd->amx_hash_algorithm, xd->amx_hash->name, xd->amx_keylen);
	buf += size;
#ifdef AHPRINTKEYS
	/* XXX - print the key in /proc/net/ipsec-spi !!! */
	ds = sprintf(buf, ", key = ");
	buf += ds;
	size += ds;
	for (i = 0; i < xd->amx_keylen; i++){
		ds = sprintf(buf, " %02x", xd->amx_key[i]);
		buf += ds;
		size += ds;
	}
#endif
	return size;
}

int
ah_old_room(struct tdb *tp, int iphlen, int tlen, int *hr, int *tr)
{
	struct ah_old_xdata *xd = (struct ah_old_xdata *)(tp->tdb_xdata) ;
	
	*hr += xd->amx_hash->hashsize + AH_OLD_FLENGTH;
	*tr += 0;
	return 0;
}

struct sk_buff *
ah_old_input(struct sk_buff *skb, struct tdb *tdbp)
{
	struct iphdr *ipp;	/* Pointer to the original IP packet header */
	struct iphdr ipo;	/* Local scratch IP header for hash processing */
	struct ah_old_xdata *xd; /* The transform private data */
	struct ah_old *ahp;	/* Pointer to packet AH header */
	int	iphlen, 	/* actual IP packet header length */
		authlen, 	/* length of the authenticator incl. fixed part */
		alen,		/* length of the hash */
		proto, 		/* next protocol field in AH */
		off,		/* offset into IP header */
		len;		/* length of the whole packet */
#ifdef DEBUG_IPSEC_AH
	int i;
#endif
	unsigned char *dat;	/* pointer to raw packet data */
	HASH_CTX ctx;		/* hash context */
	__u8 optval;		/* IP option value */
	__u8 buffer[AH_ALEN_MAX];
	
	len = skb->len; dat = skb->data;

	ipp = (struct iphdr *)dat;
	iphlen = ipp->ihl << 2;
	ahp = (struct ah_old *)(dat + iphlen);
	
	authlen = (ahp->ah_hl << 2) + AH_OLD_FLENGTH;
	proto = ahp->ah_nh;
	
	xd = (struct ah_old_xdata *)tdbp->tdb_xdata;
	alen = xd->amx_hash->hashsize;

	if ((authlen - AH_OLD_FLENGTH) != alen)
	{
#ifdef DEBUG_IPSEC_AH
		if (debug_ah & DB_AH_INAU)
			printk("ah_old_input: bad authenticator length %d, expected %d\n", authlen-AH_OLD_FLENGTH, alen);
#endif
	}

	ipo = *ipp;
	ipo.tos = 0;
	ipo.frag_off = htons(ntohs(ipo.frag_off) & IP_DF);	/* XXX -- and the C bit? */
	ipo.ttl = 0;
	ipo.check = 0;

	/* setup the precomputed hash context */
	memcpy(&ctx, &(xd->amx_ctx), xd->amx_hash->ctxsize);

	/* update the hash with the fixed IP header fields */
	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 fixed part of the AH */
	xd->amx_hash->Update(&ctx, (unsigned char *) ahp, AH_OLD_FLENGTH);

	/* update the hash with the authenticator set to zero */
	xd->amx_hash->Update(&ctx, ipseczeroes, xd->amx_hash->hashsize);

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

	/* update the hash with the key */
	xd->amx_hash->Update(&ctx, (unsigned char *)xd->amx_key, xd->amx_keylen);

	/* Finalize the hash, copy it into the local scratch AH */
	xd->amx_hash->Final(buffer, &ctx);

	/* THE AH CHECK */
	if (memcmp(buffer, ahp->ah_data, xd->amx_hash->hashsize))
	{
#ifdef DEBUG_IPSEC_AH
/* Should log properly as in OpenBSD */
		if (debug_ah & DB_AH_INAU)
			printk("ah_old_input: AH authentication failed for packet from %lx to %lx, spi %08lx\n",
				(unsigned long)ipo.saddr,
				(unsigned long)ipo.daddr,
				ntohl(tdbp->tdb_spi));
#endif
		return NULL;
	}

	/*
	 *	Discard the AH header from the packet
	 */
	 
	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_old_output(struct sk_buff *skb, struct tdb *tdbp)
{
	struct iphdr *ipp, ipo;
	struct ah_old_xdata *xd;
	struct ah_old *ahp;
	int iphlen, authlen, len, off, alen;
	unsigned char *dat;
	HASH_CTX ctx;
	__u8 optval;

	/* setup the xform data */
	xd = (struct ah_old_xdata *) tdbp->tdb_xdata;
	alen = xd->amx_hash->hashsize;

	ipp = (struct iphdr *)(skb->data);
	
	iphlen = ipp->ihl << 2;
	authlen = AH_OLD_FLENGTH + alen;

printk("alen=%d, iphlen=%d, authlen=%d\n", alen, iphlen, authlen);
	/* increase the size of the skb by the full AH size */
	dat = skb_push(skb, authlen);
	len = skb->len;
	
	/* move the header incl. options down to start of skb, making room for AH */
	memmove((void *)skb->data, (void *)(skb->data + authlen), iphlen);

	/* setup a pointer to the AH being made */
	ahp = (struct ah_old *)(dat + iphlen);

	/* keep an updated IP header pointer */
	ipp = (struct iphdr *)dat;

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

	/* setting up the AH fields */
	ahp->ah_nh = ipp->protocol;
	ahp->ah_hl = alen >> 2;
	ahp->ah_rv = 0;
	ahp->ah_spi = tdbp->tdb_spi;
	
	ipp->protocol = IPPROTO_AH;
	ipp->tot_len = htons(skb->len);

	ipo = *ipp;
	ipo.tos = 0;
       	ipo.frag_off = htons(ntohs(ipo.frag_off) & IP_DF);      /* XXX -- and the C bit? */
        ipo.ttl = 0;
        ipo.check = 0;

	/* Setup the precomputed CTX */
	memcpy(&ctx, &(xd->amx_ctx), 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 fixed part of AH */
	xd->amx_hash->Update(&ctx, (unsigned char *) ahp, AH_OLD_FLENGTH);

	/* 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);

	/* Update the hash with the key */
	xd->amx_hash->Update(&ctx, (unsigned char *)xd->amx_key, xd->amx_keylen);

	/* Finalize the hash */
	xd->amx_hash->Final(ahp->ah_data, &ctx);
	
	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;
}
