/*
 * Copyright (C) 2009 Martin Willi
 *
 * Copyright (C) secunet Security Networks AG
 *
 * 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 "radius_message.h"

#include <utils/debug.h>
#include <bio/bio_reader.h>
#include <crypto/hashers/hasher.h>

typedef struct private_radius_message_t private_radius_message_t;
typedef struct rmsg_t rmsg_t;
typedef struct rattr_t rattr_t;

/**
 * RADIUS message header
 */
struct rmsg_t {
	/** message code, radius_message_code_t */
	uint8_t code;
	/** message identifier */
	uint8_t identifier;
	/** length of Code, Identifier, Length, Authenticator and Attributes */
	uint16_t length;
	/** message authenticator, MD5 hash */
	uint8_t authenticator[HASH_SIZE_MD5];
	/** variable list of packed attributes */
	uint8_t attributes[];
} __attribute__((packed));

/**
 * RADIUS message attribute.
 */
struct rattr_t {
	/** attribute type, radius_attribute_type_t */
	uint8_t type;
	/** length of the attribute, including the Type, Length and Value fields */
	uint8_t length;
	/** variable length attribute value */
	uint8_t value[];
} __attribute__((packed));

/**
 * Private data of an radius_message_t object.
 */
struct private_radius_message_t {

	/**
	 * Public radius_message_t interface.
	 */
	radius_message_t public;

	/**
	 * message data, allocated
	 */
	rmsg_t *msg;

	/**
	 * User-Password to encrypt and encode, if any
	 */
	chunk_t password;
};

/**
 * Described in header.
 */
void libradius_init(void)
{
	/* empty */
}

ENUM_BEGIN(radius_message_code_names, RMC_ACCESS_REQUEST, RMC_ACCOUNTING_RESPONSE,
	"Access-Request",
	"Access-Accept",
	"Access-Reject",
	"Accounting-Request",
	"Accounting-Response");
ENUM_NEXT(radius_message_code_names, RMC_ACCESS_CHALLENGE, RMC_ACCESS_CHALLENGE, RMC_ACCOUNTING_RESPONSE,
	"Access-Challenge");
ENUM_NEXT(radius_message_code_names, RMC_DISCONNECT_REQUEST, RMC_COA_NAK, RMC_ACCESS_CHALLENGE,
	"Disconnect-Request",
	"Disconnect-ACK",
	"Disconnect-NAK",
	"CoA-Request",
	"CoA-ACK",
	"CoA-NAK");
ENUM_END(radius_message_code_names, RMC_COA_NAK);

ENUM_BEGIN(radius_attribute_type_names, RAT_USER_NAME, RAT_MIP6_HOME_LINK_PREFIX,
	"User-Name",
	"User-Password",
	"CHAP-Password",
	"NAS-IP-Address",
	"NAS-Port",
	"Service-Type",
	"Framed-Protocol",
	"Framed-IP-Address",
	"Framed-IP-Netmask",
	"Framed-Routing",
	"Filter-Id",
	"Framed-MTU",
	"Framed-Compression",
	"Login-IP-Host",
	"Login-Service",
	"Login-TCP-Port",
	"Unassigned",
	"Reply-Message",
	"Callback-Number",
	"Callback-Id",
	"Unassigned",
	"Framed-Route",
	"Framed-IPX-Network",
	"State",
	"Class",
	"Vendor-Specific",
	"Session-Timeout",
	"Idle-Timeout",
	"Termination-Action",
	"Called-Station-Id",
	"Calling-Station-Id",
	"NAS-Identifier",
	"Proxy-State",
	"Login-LAT-Service",
	"Login-LAT-Node",
	"Login-LAT-Group",
	"Framed-AppleTalk-Link",
	"Framed-AppleTalk-Network",
	"Framed-AppleTalk-Zone",
	"Acct-Status-Type",
	"Acct-Delay-Time",
	"Acct-Input-Octets",
	"Acct-Output-Octets",
	"Acct-Session-Id",
	"Acct-Authentic",
	"Acct-Session-Time",
	"Acct-Input-Packets",
	"Acct-Output-Packets",
	"Acct-Terminate-Cause",
	"Acct-Multi-Session-Id",
	"Acct-Link-Count",
	"Acct-Input-Gigawords",
	"Acct-Output-Gigawords",
	"Unassigned",
	"Event-Timestamp",
	"Egress-VLANID",
	"Ingress-Filters",
	"Egress-VLAN-Name",
	"User-Priority-Table",
	"CHAP-Challenge",
	"NAS-Port-Type",
	"Port-Limit",
	"Login-LAT-Port",
	"Tunnel-Type",
	"Tunnel-Medium-Type",
	"Tunnel-Client-Endpoint",
	"Tunnel-Server-Endpoint",
	"Acct-Tunnel-Connection",
	"Tunnel-Password",
	"ARAP-Password",
	"ARAP-Features",
	"ARAP-Zone-Access",
	"ARAP-Security",
	"ARAP-Security-Data",
	"Password-Retry",
	"Prompt",
	"Connect-Info",
	"Configuration-Token",
	"EAP-Message",
	"Message-Authenticator",
	"Tunnel-Private-Group-ID",
	"Tunnel-Assignment-ID",
	"Tunnel-Preference",
	"ARAP-Challenge-Response",
	"Acct-Interim-Interval",
	"Acct-Tunnel-Packets-Lost",
	"NAS-Port-Id",
	"Framed-Pool",
	"CUI",
	"Tunnel-Client-Auth-ID",
	"Tunnel-Server-Auth-ID",
	"NAS-Filter-Rule",
	"Unassigned",
	"Originating-Line-Info",
	"NAS-IPv6-Address",
	"Framed-Interface-Id",
	"Framed-IPv6-Prefix",
	"Login-IPv6-Host",
	"Framed-IPv6-Route",
	"Framed-IPv6-Pool",
	"Error-Cause",
	"EAP-Key-Name",
	"Digest-Response",
	"Digest-Realm",
	"Digest-Nonce",
	"Digest-Response-Auth",
	"Digest-Nextnonce",
	"Digest-Method",
	"Digest-URI",
	"Digest-Qop",
	"Digest-Algorithm",
	"Digest-Entity-Body-Hash",
	"Digest-CNonce",
	"Digest-Nonce-Count",
	"Digest-Username",
	"Digest-Opaque",
	"Digest-Auth-Param",
	"Digest-AKA-Auts",
	"Digest-Domain",
	"Digest-Stale",
	"Digest-HA1",
	"SIP-AOR",
	"Delegated-IPv6-Prefix",
	"MIP6-Feature-Vector",
	"MIP6-Home-Link-Prefix");
ENUM_NEXT(radius_attribute_type_names, RAT_FRAMED_IPV6_ADDRESS, RAT_STATEFUL_IPV6_ADDRESS_POOL, RAT_MIP6_HOME_LINK_PREFIX,
	"Framed-IPv6-Address",
	"DNS-Server-IPv6-Address",
	"Route-IPv6-Information",
	"Delegated-IPv6-Prefix-Pool",
	"Stateful-IPv6-Address-Pool");
ENUM_END(radius_attribute_type_names, RAT_STATEFUL_IPV6_ADDRESS_POOL);

/**
 * Attribute enumerator implementation
 */
typedef struct {
	/** implements enumerator interface */
	enumerator_t public;
	/** currently pointing attribute */
	rattr_t *next;
	/** bytes left */
	int left;
} attribute_enumerator_t;

METHOD(enumerator_t, attribute_enumerate, bool,
	attribute_enumerator_t *this, va_list args)
{
	chunk_t *data;
	int *type;

	VA_ARGS_VGET(args, type, data);
	if (this->left == 0)
	{
		return FALSE;
	}
	if (this->left < sizeof(rattr_t) ||
		this->left < this->next->length)
	{
		DBG1(DBG_IKE, "RADIUS message truncated");
		return FALSE;
	}
	if (this->next->length < sizeof(rattr_t))
	{
		DBG1(DBG_IKE, "RADIUS attribute has invalid length");
		return FALSE;
	}
	*type = this->next->type;
	data->ptr = this->next->value;
	data->len = this->next->length - sizeof(rattr_t);
	this->left -= this->next->length;
	this->next = ((void*)this->next) + this->next->length;
	return TRUE;
}

METHOD(radius_message_t, create_enumerator, enumerator_t*,
	private_radius_message_t *this)
{
	attribute_enumerator_t *e;

	if (ntohs(this->msg->length) < sizeof(rmsg_t) + sizeof(rattr_t))
	{
		return enumerator_create_empty();
	}
	INIT(e,
		.public = {
			.enumerate = enumerator_enumerate_default,
			.venumerate = _attribute_enumerate,
			.destroy = (void*)free,
		},
		.next = (rattr_t*)this->msg->attributes,
		.left = ntohs(this->msg->length) - sizeof(rmsg_t),
	);
	return &e->public;
}

/**
 * Vendor attribute enumerator implementation
 */
typedef struct {
	/** implements enumerator interface */
	enumerator_t public;
	/** inner attribute enumerator */
	enumerator_t *inner;
	/** current vendor ID */
	uint32_t vendor;
	/** reader for current vendor ID */
	bio_reader_t *reader;
} vendor_enumerator_t;

METHOD(enumerator_t, vendor_enumerate, bool,
	vendor_enumerator_t *this, va_list args)
{
	chunk_t inner_data, *data;
	int inner_type, *vendor, *type;
	uint8_t type8, len;

	VA_ARGS_VGET(args, vendor, type, data);

	while (TRUE)
	{
		if (this->reader)
		{
			if (this->reader->remaining(this->reader) >= 2 &&
				this->reader->read_uint8(this->reader, &type8) &&
				this->reader->read_uint8(this->reader, &len) && len >= 2 &&
				this->reader->read_data(this->reader, len - 2, data))
			{
				*vendor = this->vendor;
				*type = type8;
				return TRUE;
			}
			this->reader->destroy(this->reader);
			this->reader = NULL;
		}
		if (this->inner->enumerate(this->inner, &inner_type, &inner_data))
		{
			if (inner_type == RAT_VENDOR_SPECIFIC)
			{
				this->reader = bio_reader_create(inner_data);
				if (!this->reader->read_uint32(this->reader, &this->vendor))
				{
					this->reader->destroy(this->reader);
					this->reader = NULL;
				}
			}
		}
		else
		{
			return FALSE;
		}
	}
}
METHOD(enumerator_t, vendor_destroy, void,
	vendor_enumerator_t *this)
{
	DESTROY_IF(this->reader);
	this->inner->destroy(this->inner);
	free(this);
}

METHOD(radius_message_t, create_vendor_enumerator, enumerator_t*,
	private_radius_message_t *this)
{
	vendor_enumerator_t *e;

	INIT(e,
		.public = {
			.enumerate = enumerator_enumerate_default,
			.venumerate = _vendor_enumerate,
			.destroy = _vendor_destroy,
		},
		.inner = create_enumerator(this),
	);

	return &e->public;
}

METHOD(radius_message_t, add, void,
	private_radius_message_t *this, radius_attribute_type_t type, chunk_t data)
{
	rattr_t *attribute;

	if (type == RAT_USER_PASSWORD && !this->password.len)
	{
		/* store a null-padded password */
		this->password = chunk_alloc(round_up(data.len, HASH_SIZE_MD5));
		memset(this->password.ptr + data.len, 0, this->password.len - data.len);
		memcpy(this->password.ptr, data.ptr, data.len);
		return;
	}

	data.len = min(data.len, MAX_RADIUS_ATTRIBUTE_SIZE);
	this->msg = realloc(this->msg,
						ntohs(this->msg->length) + sizeof(rattr_t) + data.len);
	attribute = ((void*)this->msg) + ntohs(this->msg->length);
	attribute->type = type;
	attribute->length = data.len + sizeof(rattr_t);
	memcpy(attribute->value, data.ptr, data.len);
	this->msg->length = htons(ntohs(this->msg->length) + attribute->length);
}

METHOD(radius_message_t, crypt, bool,
	private_radius_message_t *this, chunk_t salt, chunk_t in, chunk_t out,
	chunk_t secret, hasher_t *hasher)
{
	char b[HASH_SIZE_MD5];

	/**
	 * From RFC2548 (encryption):
	 * b(1) = MD5(S + R + A)    c(1) = p(1) xor b(1)   C = c(1)
	 * b(2) = MD5(S + c(1))     c(2) = p(2) xor b(2)   C = C + c(2)
	 *      . . .
	 * b(i) = MD5(S + c(i-1))   c(i) = p(i) xor b(i)   C = C + c(i)
	 *
	 * P/C = Plain/Crypted => in/out
	 * S = secret
	 * R = authenticator
	 * A = salt
	 */
	if (in.len != out.len)
	{
		return FALSE;
	}
	if (in.len % HASH_SIZE_MD5 || in.len < HASH_SIZE_MD5)
	{
		return FALSE;
	}
	if (out.ptr != in.ptr)
	{
		memcpy(out.ptr, in.ptr, in.len);
	}
	/* Preparse seed for first round:
	 * b(1) = MD5(S + R + A) */
	if (!hasher->get_hash(hasher, secret, NULL) ||
		!hasher->get_hash(hasher,
						  chunk_from_thing(this->msg->authenticator), NULL) ||
		!hasher->get_hash(hasher, salt, b))
	{
		return FALSE;
	}
	while (in.len)
	{
		/* p(i) = b(i) xor c(1) */
		memxor(out.ptr, b, HASH_SIZE_MD5);

		out = chunk_skip(out, HASH_SIZE_MD5);
		if (out.len)
		{
			/* Prepare seed for next round::
			 * b(i) = MD5(S + c(i-1)) */
			if (!hasher->get_hash(hasher, secret, NULL) ||
				!hasher->get_hash(hasher,
								  chunk_create(in.ptr, HASH_SIZE_MD5), b))
			{
				return FALSE;
			}
		}
		in = chunk_skip(in, HASH_SIZE_MD5);
	}
	return TRUE;
}

METHOD(radius_message_t, sign, bool,
	private_radius_message_t *this, uint8_t *req_auth, chunk_t secret,
	hasher_t *hasher, signer_t *signer, rng_t *rng, bool msg_auth)
{
	if (rng)
	{
		/* build Request-Authenticator */
		if (!rng->get_bytes(rng, HASH_SIZE_MD5, this->msg->authenticator))
		{
			return FALSE;
		}
	}
	else
	{
		/* prepare build of Response-Authenticator */
		if (req_auth)
		{
			memcpy(this->msg->authenticator, req_auth, HASH_SIZE_MD5);
		}
		else
		{
			memset(this->msg->authenticator, 0, sizeof(this->msg->authenticator));
		}
	}

	if (this->password.len)
	{
		/* encrypt password inline */
		if (!crypt(this, chunk_empty, this->password, this->password,
				   secret, hasher))
		{
			return FALSE;
		}
		add(this, RAT_USER_PASSWORD, this->password);
		chunk_clear(&this->password);
	}

	if (msg_auth)
	{
		char buf[HASH_SIZE_MD5];

		/* build Message-Authenticator attribute, using 16 null bytes */
		memset(buf, 0, sizeof(buf));
		add(this, RAT_MESSAGE_AUTHENTICATOR, chunk_create(buf, sizeof(buf)));
		if (!signer->get_signature(signer,
				chunk_create((u_char*)this->msg, ntohs(this->msg->length)),
				((u_char*)this->msg) + ntohs(this->msg->length) - HASH_SIZE_MD5))
		{
			return FALSE;
		}
	}

	if (!rng)
	{
		chunk_t msg;

		/* build Response-Authenticator */
		msg = chunk_create((u_char*)this->msg, ntohs(this->msg->length));
		if (!hasher->get_hash(hasher, msg, NULL) ||
			!hasher->get_hash(hasher, secret, this->msg->authenticator))
		{
			return FALSE;
		}
	}
	return TRUE;
}

METHOD(radius_message_t, verify, bool,
	private_radius_message_t *this, uint8_t *req_auth, chunk_t secret,
	hasher_t *hasher, signer_t *signer)
{
	char buf[HASH_SIZE_MD5], res_auth[HASH_SIZE_MD5];
	enumerator_t *enumerator;
	int type;
	chunk_t data, msg;
	bool has_eap = FALSE, has_auth = FALSE;

	msg = chunk_create((u_char*)this->msg, ntohs(this->msg->length));

	if (this->msg->code != RMC_ACCESS_REQUEST)
	{
		/* replace Response by Request Authenticator for verification */
		memcpy(res_auth, this->msg->authenticator, HASH_SIZE_MD5);
		if (req_auth)
		{
			memcpy(this->msg->authenticator, req_auth, HASH_SIZE_MD5);
		}
		else
		{
			memset(this->msg->authenticator, 0, HASH_SIZE_MD5);
		}

		/* verify Response-Authenticator */
		if (!hasher->get_hash(hasher, msg, NULL) ||
			!hasher->get_hash(hasher, secret, buf) ||
			!memeq_const(buf, res_auth, HASH_SIZE_MD5))
		{
			DBG1(DBG_CFG, "RADIUS Response-Authenticator verification failed");
			return FALSE;
		}
	}

	/* verify Message-Authenticator attribute */
	enumerator = create_enumerator(this);
	while (enumerator->enumerate(enumerator, &type, &data))
	{
		if (type == RAT_MESSAGE_AUTHENTICATOR)
		{
			if (data.len != HASH_SIZE_MD5)
			{
				DBG1(DBG_CFG, "RADIUS Message-Authenticator invalid length");
				enumerator->destroy(enumerator);
				return FALSE;
			}
			memcpy(buf, data.ptr, data.len);
			memset(data.ptr, 0, data.len);
			if (signer->verify_signature(signer, msg,
										 chunk_create(buf, sizeof(buf))))
			{
				/* restore Message-Authenticator */
				memcpy(data.ptr, buf, data.len);
				has_auth = TRUE;
				break;
			}
			else
			{
				DBG1(DBG_CFG, "RADIUS Message-Authenticator verification failed");
				enumerator->destroy(enumerator);
				return FALSE;
			}
		}
		else if (type == RAT_EAP_MESSAGE)
		{
			has_eap = TRUE;
		}
	}
	enumerator->destroy(enumerator);

	if (this->msg->code != RMC_ACCESS_REQUEST)
	{
		/* restore Response-Authenticator */
		memcpy(this->msg->authenticator, res_auth, HASH_SIZE_MD5);
	}

	if (has_eap && !has_auth)
	{	/* Message-Authenticator is required if we have an EAP-Message */
		DBG1(DBG_CFG, "RADIUS Message-Authenticator attribute missing");
		return FALSE;
	}
	return TRUE;
}

METHOD(radius_message_t, get_code, radius_message_code_t,
	private_radius_message_t *this)
{
	return this->msg->code;
}

METHOD(radius_message_t, get_identifier, uint8_t,
	private_radius_message_t *this)
{
	return this->msg->identifier;
}

METHOD(radius_message_t, set_identifier, void,
	private_radius_message_t *this, uint8_t identifier)
{
	this->msg->identifier = identifier;
}

METHOD(radius_message_t, get_authenticator, uint8_t*,
	private_radius_message_t *this)
{
	return this->msg->authenticator;
}


METHOD(radius_message_t, get_encoding, chunk_t,
	private_radius_message_t *this)
{
	return chunk_create((u_char*)this->msg, ntohs(this->msg->length));
}

METHOD(radius_message_t, destroy, void,
	private_radius_message_t *this)
{
	chunk_clear(&this->password);
	free(this->msg);
	free(this);
}

/**
 * Generic constructor
 */
static private_radius_message_t *radius_message_create_empty()
{
	private_radius_message_t *this;

	INIT(this,
		.public = {
			.create_enumerator = _create_enumerator,
			.create_vendor_enumerator = _create_vendor_enumerator,
			.add = _add,
			.get_code = _get_code,
			.get_identifier = _get_identifier,
			.set_identifier = _set_identifier,
			.get_authenticator = _get_authenticator,
			.get_encoding = _get_encoding,
			.sign = _sign,
			.verify = _verify,
			.crypt = _crypt,
			.destroy = _destroy,
		},
	);

	return this;
}

/**
 * See header
 */
radius_message_t *radius_message_create(radius_message_code_t code)
{
	private_radius_message_t *this = radius_message_create_empty();

	INIT(this->msg,
		.code = code,
		.identifier = 0,
		.length = htons(sizeof(rmsg_t)),
	);

	return &this->public;
}

/**
 * See header
 */
radius_message_t *radius_message_parse(chunk_t data)
{
	private_radius_message_t *this = radius_message_create_empty();

	this->msg = malloc(data.len);
	memcpy(this->msg, data.ptr, data.len);
	if (data.len < sizeof(rmsg_t) ||
		ntohs(this->msg->length) != data.len)
	{
		DBG1(DBG_IKE, "RADIUS message has invalid length");
		destroy(this);
		return NULL;
	}
	return &this->public;
}
