/*
 * rlm_eap_sim.c    Handles that are called from eap for SIM
 *
 * The development of the EAP/SIM support was funded by Internet Foundation
 * Austria (http://www.nic.at/ipa).
 *
 * Version:     $Id: 38fe997a5f0fd11fee5759baa7a60843e560965c $
 *
 *   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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 *
 * Copyright 2003  Michael Richardson <mcr@sandelman.ottawa.on.ca>
 * Copyright 2003,2006  The FreeRADIUS server project
 *
 */

RCSID("$Id: 38fe997a5f0fd11fee5759baa7a60843e560965c $")

#include <stdio.h>
#include <stdlib.h>

#include "../../eap.h"
#include "eap_types.h"
#include "eap_sim.h"
#include "comp128.h"

#include <freeradius-devel/rad_assert.h>

typedef struct eap_sim_server_state {
	enum eapsim_serverstates state;
	struct eapsim_keys keys;
	int  sim_id;
} eap_sim_state_t;

static int eap_sim_sendstart(eap_handler_t *handler)
{
	VALUE_PAIR **vps, *newvp;
	uint16_t words[3];
	eap_sim_state_t *ess;
	RADIUS_PACKET *packet;
	uint8_t *p;

	rad_assert(handler->request != NULL);
	rad_assert(handler->request->reply);

	ess = (eap_sim_state_t *)handler->opaque;

	/* these are the outgoing attributes */
	packet = handler->request->reply;
	vps = &packet->vps;
	rad_assert(vps != NULL);


	/*
	 *	Add appropriate TLVs for the EAP things we wish to send.
	 */

	/* the version list. We support only version 1. */
	words[0] = htons(sizeof(words[1]));
	words[1] = htons(EAP_SIM_VERSION);
	words[2] = 0;

	newvp = fr_pair_afrom_num(packet, PW_EAP_SIM_VERSION_LIST, 0);
	fr_pair_value_memcpy(newvp, (uint8_t const *) words, sizeof(words));

	fr_pair_add(vps, newvp);

	/* set the EAP_ID - new value */
	newvp = fr_pair_afrom_num(packet, PW_EAP_ID, 0);
	newvp->vp_integer = ess->sim_id++;
	fr_pair_replace(vps, newvp);

	/* record it in the ess */
	ess->keys.versionlistlen = 2;
	memcpy(ess->keys.versionlist, words + 1, ess->keys.versionlistlen);

	/* the ANY_ID attribute. We do not support re-auth or pseudonym */
	newvp = fr_pair_afrom_num(packet, PW_EAP_SIM_FULLAUTH_ID_REQ, 0);
	newvp->vp_length = 2;
	newvp->vp_octets = p = talloc_array(newvp, uint8_t, 2);

	p[0] = 0;
	p[0] = 1;
	fr_pair_add(vps, newvp);

	/* the SUBTYPE, set to start. */
	newvp = fr_pair_afrom_num(packet, PW_EAP_SIM_SUBTYPE, 0);
	newvp->vp_integer = EAPSIM_START;
	fr_pair_replace(vps, newvp);

	return 1;
}

static int eap_sim_get_challenge(eap_handler_t *handler, VALUE_PAIR *vps, int idx, eap_sim_state_t *ess)
{
	REQUEST *request = handler->request;
	VALUE_PAIR *vp, *ki, *algo_version;

	rad_assert(idx >= 0 && idx < 3);

	/*
	 *	Generate a new RAND value, and derive Kc and SRES from Ki
	 */
	ki = fr_pair_find_by_num(vps, PW_EAP_SIM_KI, 0, TAG_ANY);
	if (ki) {
		int i;

		/*
		 *	Check to see if have a Ki for the IMSI, this allows us to generate the rest
		 *	of the triplets.
		 */
		algo_version = fr_pair_find_by_num(vps, PW_EAP_SIM_ALGO_VERSION, 0, TAG_ANY);
		if (!algo_version) {
			REDEBUG("Found Ki, but missing EAP-Sim-Algo-Version");
			return 0;
		}

		for (i = 0; i < EAPSIM_RAND_SIZE; i++) {
			ess->keys.rand[idx][i] = fr_rand();
		}

		switch (algo_version->vp_integer) {
		case 1:
			comp128v1(ess->keys.sres[idx], ess->keys.Kc[idx], ki->vp_octets, ess->keys.rand[idx]);
			break;

		case 2:
			comp128v23(ess->keys.sres[idx], ess->keys.Kc[idx], ki->vp_octets, ess->keys.rand[idx],
				   true);
			break;

		case 3:
			comp128v23(ess->keys.sres[idx], ess->keys.Kc[idx], ki->vp_octets, ess->keys.rand[idx],
				   false);
			break;

		case 4:
			REDEBUG("Comp128-4 algorithm is not supported as details have not yet been published. "
				"If you have details of this algorithm please contact the FreeRADIUS "
				"maintainers");
			return 0;

		default:
			REDEBUG("Unknown/unsupported algorithm Comp128-%i", algo_version->vp_integer);
		}

		if (RDEBUG_ENABLED2) {
			char buffer[33];	/* 32 hexits (16 bytes) + 1 */
			char *p;

			RDEBUG2("Generated following triplets for round %i:", idx);

			RINDENT();
			p = buffer;
			for (i = 0; i < EAPSIM_RAND_SIZE; i++) {
				p += sprintf(p, "%02x", ess->keys.rand[idx][i]);
			}
			RDEBUG2("RAND : 0x%s", buffer);

			p = buffer;
			for (i = 0; i < EAPSIM_SRES_SIZE; i++) {
				p += sprintf(p, "%02x", ess->keys.sres[idx][i]);
			}
			RDEBUG2("SRES : 0x%s", buffer);

			p = buffer;
			for (i = 0; i < EAPSIM_KC_SIZE; i++) {
				p += sprintf(p, "%02x", ess->keys.Kc[idx][i]);
			}
			RDEBUG2("Kc   : 0x%s", buffer);
			REXDENT();
		}
		return 1;
	}

	/*
	 *	Use known RAND, SRES, and Kc values, these may of been pulled in from an AuC,
	 *	or created by sending challenges to the SIM directly.
	 */
	vp = fr_pair_find_by_num(vps, PW_EAP_SIM_RAND1 + idx, 0, TAG_ANY);
	/* Hack for backwards compatibility */
	if (!vp) {
		vp = fr_pair_find_by_num(request->reply->vps, PW_EAP_SIM_RAND1 + idx, 0, TAG_ANY);
	}
	if (!vp) {
		/* bad, we can't find stuff! */
		REDEBUG("EAP-SIM-RAND%i not found", idx + 1);
		return 0;
	}
	if (vp->vp_length != EAPSIM_RAND_SIZE) {
		REDEBUG("EAP-SIM-RAND%i is not " STRINGIFY(EAPSIM_RAND_SIZE) " bytes, got %zu bytes",
			idx + 1, vp->vp_length);
		return 0;
	}
	memcpy(ess->keys.rand[idx], vp->vp_strvalue, EAPSIM_RAND_SIZE);

	vp = fr_pair_find_by_num(vps, PW_EAP_SIM_SRES1 + idx, 0, TAG_ANY);
	/* Hack for backwards compatibility */
	if (!vp) {
		vp = fr_pair_find_by_num(request->reply->vps, PW_EAP_SIM_SRES1 + idx, 0, TAG_ANY);
	}
	if (!vp) {
		/* bad, we can't find stuff! */
		REDEBUG("EAP-SIM-SRES%i not found", idx + 1);
		return 0;
	}

	if (vp->vp_length != EAPSIM_SRES_SIZE) {
		REDEBUG("EAP-SIM-SRES%i is not " STRINGIFY(EAPSIM_SRES_SIZE) " bytes, got %zu bytes",
			idx + 1, vp->vp_length);
		return 0;
	}
	memcpy(ess->keys.sres[idx], vp->vp_strvalue, EAPSIM_SRES_SIZE);

	vp = fr_pair_find_by_num(vps, PW_EAP_SIM_KC1 + idx, 0, TAG_ANY);
	/* Hack for backwards compatibility */
	if (!vp) {
		vp = fr_pair_find_by_num(request->reply->vps, PW_EAP_SIM_KC1 + idx, 0, TAG_ANY);
	}

	if (!vp) {
		/* bad, we can't find stuff! */
		REDEBUG("EAP-SIM-Kc%i not found", idx + 1);
		return 0;
	}
	if (vp->vp_length != EAPSIM_KC_SIZE) {
		REDEBUG("EAP-SIM-Kc%i is not " STRINGIFY(EAPSIM_KC_SIZE) " bytes, got %zu bytes",
			idx + 1, vp->vp_length);
		return 0;
	}
	memcpy(ess->keys.Kc[idx], vp->vp_strvalue, EAPSIM_KC_SIZE);

	return 1;
}

/** Send the challenge itself
 *
 * Challenges will come from one of three places eventually:
 *
 * 1  from attributes like PW_EAP_SIM_RANDx
 *	    (these might be retrieved from a database)
 *
 * 2  from internally implemented SIM authenticators
 *	    (a simple one based upon XOR will be provided)
 *
 * 3  from some kind of SS7 interface.
 *
 * For now, they only come from attributes.
 * It might be that the best way to do 2/3 will be with a different
 * module to generate/calculate things.
 *
 */
static int eap_sim_sendchallenge(eap_handler_t *handler)
{
	REQUEST *request = handler->request;
	eap_sim_state_t *ess;
	VALUE_PAIR **invps, **outvps, *newvp;
	RADIUS_PACKET *packet;
	uint8_t *p;

	ess = (eap_sim_state_t *)handler->opaque;
	rad_assert(handler->request != NULL);
	rad_assert(handler->request->reply);

	/*
	 *	Invps is the data from the client but this is for non-protocol data here.
	 *	We should already have consumed any client originated data.
	 */
	invps = &handler->request->packet->vps;

	/*
	 *	Outvps is the data to the client
	 */
	packet = handler->request->reply;
	outvps = &packet->vps;

	if (RDEBUG_ENABLED2) {
		RDEBUG2("EAP-SIM decoded packet");
		rdebug_pair_list(L_DBG_LVL_2, request, *invps, NULL);
	}

	/*
	 *	Okay, we got the challenges! Put them into an attribute.
	 */
	newvp = fr_pair_afrom_num(packet, PW_EAP_SIM_RAND, 0);
	newvp->vp_length = 2 + (EAPSIM_RAND_SIZE * 3);
	newvp->vp_octets = p = talloc_array(newvp, uint8_t, newvp->vp_length);

	memset(p, 0, 2); /* clear reserved bytes */
	p += 2;
	memcpy(p, ess->keys.rand[0], EAPSIM_RAND_SIZE);
	p += EAPSIM_RAND_SIZE;
	memcpy(p, ess->keys.rand[1], EAPSIM_RAND_SIZE);
	p += EAPSIM_RAND_SIZE;
	memcpy(p, ess->keys.rand[2], EAPSIM_RAND_SIZE);
	fr_pair_add(outvps, newvp);

	/*
	 *	Set the EAP_ID - new value
	 */
	newvp = fr_pair_afrom_num(packet, PW_EAP_ID, 0);
	newvp->vp_integer = ess->sim_id++;
	fr_pair_replace(outvps, newvp);

	/*
	 *	Make a copy of the identity
	 */
	ess->keys.identitylen = strlen(handler->identity);
	memcpy(ess->keys.identity, handler->identity, ess->keys.identitylen);

	/*
	 *	Use the SIM identity, if available
	 */
	newvp = fr_pair_find_by_num(*invps, PW_EAP_SIM_IDENTITY, 0, TAG_ANY);
	if (newvp && newvp->vp_length > 2) {
		uint16_t len;

		memcpy(&len, newvp->vp_octets, sizeof(uint16_t));
		len = ntohs(len);
		if (len <= newvp->vp_length - 2 && len <= MAX_STRING_LEN) {
			ess->keys.identitylen = len;
			memcpy(ess->keys.identity, newvp->vp_octets + 2, ess->keys.identitylen);
		}
	}

	/*
	 *	All set, calculate keys!
	 */
	eapsim_calculate_keys(&ess->keys);

#ifdef EAP_SIM_DEBUG_PRF
	eapsim_dump_mk(&ess->keys);
#endif

	/*
	 *	Need to include an AT_MAC attribute so that it will get
	 *	calculated. The NONCE_MT and the MAC are both 16 bytes, so
	 *	We store the NONCE_MT in the MAC for the encoder, which
	 *	will pull it out before it does the operation.
	 */
	newvp = fr_pair_afrom_num(packet, PW_EAP_SIM_MAC, 0);
	fr_pair_value_memcpy(newvp, ess->keys.nonce_mt, 16);
	fr_pair_replace(outvps, newvp);

	newvp = fr_pair_afrom_num(packet, PW_EAP_SIM_KEY, 0);
	fr_pair_value_memcpy(newvp, ess->keys.K_aut, 16);
	fr_pair_replace(outvps, newvp);

	/* the SUBTYPE, set to challenge. */
	newvp = fr_pair_afrom_num(packet, PW_EAP_SIM_SUBTYPE, 0);
	newvp->vp_integer = EAPSIM_CHALLENGE;
	fr_pair_replace(outvps, newvp);

	return 1;
}

#ifndef EAPTLS_MPPE_KEY_LEN
#define EAPTLS_MPPE_KEY_LEN     32
#endif

/*
 * this code sends the success message.
 *
 * the only work to be done is the add the appropriate SEND/RECV
 * radius attributes derived from the MSK.
 *
 */
static int eap_sim_sendsuccess(eap_handler_t *handler)
{
	unsigned char *p;
	eap_sim_state_t *ess;
	VALUE_PAIR *vp;
	RADIUS_PACKET *packet;

	/* outvps is the data to the client. */
	packet = handler->request->reply;
	ess = (eap_sim_state_t *)handler->opaque;

	/* set the EAP_ID - new value */
	vp = fr_pair_afrom_num(packet, PW_EAP_ID, 0);
	vp->vp_integer = ess->sim_id++;
	fr_pair_replace(&handler->request->reply->vps, vp);

	p = ess->keys.msk;
	eap_add_reply(handler->request, "MS-MPPE-Recv-Key", p, EAPTLS_MPPE_KEY_LEN);
	p += EAPTLS_MPPE_KEY_LEN;
	eap_add_reply(handler->request, "MS-MPPE-Send-Key", p, EAPTLS_MPPE_KEY_LEN);

	return 1;
}


/** Run the server state machine
 *
 */
static void eap_sim_state_enter(REQUEST *request, eap_handler_t *handler,
				eap_sim_state_t *ess,
				enum eapsim_serverstates newstate)
{
	switch (newstate) {
	/*
	 * 	Send the EAP-SIM Start message, listing the versions that we support.
	 */
	case EAPSIM_SERVER_START:
		eap_sim_sendstart(handler);
		break;
	/*
	 *	Send the EAP-SIM Challenge message.
	 */
	case EAPSIM_SERVER_CHALLENGE:
		eap_sim_sendchallenge(handler);
		break;

	/*
	 * 	Send the EAP Success message
	 */
	case EAPSIM_SERVER_SUCCESS:
		eap_sim_sendsuccess(handler);
		handler->eap_ds->request->code = PW_EAP_SUCCESS;
		break;
	/*
	 *	Nothing to do for this transition.
	 */
	default:

		break;
	}

	ess->state = newstate;

	/* build the target packet */
	/* we will set the ID on requests, since we have to HMAC it */
	handler->eap_ds->set_request_id = 1;

	if (!map_eapsim_basictypes(handler->request->reply,
				   handler->eap_ds->request)) {
		REDEBUG("Failed encoding EAP-SIM packet");
	}
}

/*
 *	Initiate the EAP-SIM session by starting the state machine
 *      and initiating the state.
 */
static int mod_session_init(UNUSED void *instance, eap_handler_t *handler)
{
	REQUEST *request = handler->request;
	eap_sim_state_t *ess;
	time_t n;

	ess = talloc_zero(handler, eap_sim_state_t);
	if (!ess) {
		RDEBUG2("No space for EAP-SIM state");
		return 0;
	}

	handler->opaque = ess;
	handler->stage = PROCESS;

	/*
	 *	Save the keying material, because it could change on a subsequent retrival.
	 */
	if (!eap_sim_get_challenge(handler, request->config, 0, ess) ||
	    !eap_sim_get_challenge(handler, request->config, 1, ess) ||
	    !eap_sim_get_challenge(handler, request->config, 2, ess)) {
		return 0;	/* already printed error */
	}

	/*
	 *	This value doesn't have be strong, but it is good if it is different now and then.
	 */
	time(&n);
	ess->sim_id = (n & 0xff);

	eap_sim_state_enter(request, handler, ess, EAPSIM_SERVER_START);

	return 1;
}


/** Process an EAP-Sim/Response/Start
 *
 * Verify that client chose a version, and provided a NONCE_MT,
 * and if so, then change states to challenge, and send the new
 * challenge, else, resend the Request/Start.
 */
static int process_eap_sim_start(eap_handler_t *handler, VALUE_PAIR *vps)
{
	REQUEST *request = handler->request;
	VALUE_PAIR *nonce_vp, *selectedversion_vp;
	eap_sim_state_t *ess;
	uint16_t simversion;
	ess = (eap_sim_state_t *)handler->opaque;

	nonce_vp = fr_pair_find_by_num(vps, PW_EAP_SIM_NONCE_MT, 0, TAG_ANY);
	selectedversion_vp = fr_pair_find_by_num(vps, PW_EAP_SIM_SELECTED_VERSION, 0, TAG_ANY);
	if (!nonce_vp || !selectedversion_vp) {
		RDEBUG2("Client did not select a version and send a NONCE");
		eap_sim_state_enter(request, handler, ess, EAPSIM_SERVER_START);
		return 1;
	}

	/*
	 *	Okay, good got stuff that we need. Check the version we found.
	 */
	if (selectedversion_vp->vp_length < 2) {
		REDEBUG("EAP-SIM version field is too short");
		return 0;
	}
	memcpy(&simversion, selectedversion_vp->vp_strvalue, sizeof(simversion));
	simversion = ntohs(simversion);
	if(simversion != EAP_SIM_VERSION) {
		REDEBUG("EAP-SIM version %i is unknown", simversion);
		return 0;
	}

	/*
	 *	Record it for later keying
	 */
	memcpy(ess->keys.versionselect, selectedversion_vp->vp_strvalue, sizeof(ess->keys.versionselect));

	/*
	 *	Double check the nonce size.
	 */
	if(nonce_vp->vp_length != 18) {
		REDEBUG("EAP-SIM nonce_mt must be 16 bytes (+2 bytes padding), not %zu", nonce_vp->vp_length);
		return 0;
	}
	memcpy(ess->keys.nonce_mt, nonce_vp->vp_strvalue + 2, 16);

	/*
	 *	Everything looks good, change states
	 */
	eap_sim_state_enter(request, handler, ess, EAPSIM_SERVER_CHALLENGE);

	return 1;
}


/** Process an EAP-Sim/Response/Challenge
 *
 * Verify that MAC that we received matches what we would have
 * calculated from the packet with the SRESx appended.
 *
 */
static int process_eap_sim_challenge(eap_handler_t *handler, VALUE_PAIR *vps)
{
	REQUEST *request = handler->request;
	eap_sim_state_t *ess = handler->opaque;

	uint8_t srescat[EAPSIM_SRES_SIZE * 3];
	uint8_t *p = srescat;

	uint8_t calcmac[EAPSIM_CALCMAC_SIZE];

	memcpy(p, ess->keys.sres[0], EAPSIM_SRES_SIZE);
	p += EAPSIM_SRES_SIZE;
	memcpy(p, ess->keys.sres[1], EAPSIM_SRES_SIZE);
	p += EAPSIM_SRES_SIZE;
	memcpy(p, ess->keys.sres[2], EAPSIM_SRES_SIZE);

	/*
	 *	Verify the MAC, now that we have all the keys
	 */
	if (eapsim_checkmac(handler, vps, ess->keys.K_aut, srescat, sizeof(srescat), calcmac)) {
		RDEBUG2("MAC check succeed");
	} else {
		int i, j;
		char macline[20*3];
		char *m = macline;

		j=0;
		for (i = 0; i < EAPSIM_CALCMAC_SIZE; i++) {
			if(j==4) {
			  *m++ = '_';
			  j=0;
			}
			j++;

			sprintf(m, "%02x", calcmac[i]);
			m = m + strlen(m);
		}
		REDEBUG("Calculated MAC (%s) did not match", macline);
		return 0;
	}

	/* everything looks good, change states */
	eap_sim_state_enter(request, handler, ess, EAPSIM_SERVER_SUCCESS);
	return 1;
}


/** Authenticate a previously sent challenge
 *
 */
static int mod_process(UNUSED void *arg, eap_handler_t *handler)
{
	REQUEST *request = handler->request;
	eap_sim_state_t *ess = handler->opaque;

	VALUE_PAIR *vp, *vps;

	enum eapsim_subtype subtype;

	int success;

	/*
	 *	VPS is the data from the client
	 */
	vps = handler->request->packet->vps;

	success = unmap_eapsim_basictypes(handler->request->packet,
					  handler->eap_ds->response->type.data,
					  handler->eap_ds->response->type.length);

	if (!success) {
		REDEBUG("Failed decoding EAP-SIM packet: %s", fr_strerror());
		return 0;
	}

	/*
	 *	See what kind of message we have gotten
	 */
	vp = fr_pair_find_by_num(vps, PW_EAP_SIM_SUBTYPE, 0, TAG_ANY);
	if (!vp) {
		REDEBUG2("No subtype attribute was created, message dropped");
		return 0;
	}
	subtype = vp->vp_integer;

	/*
	 *	Client error supersedes anything else.
	 */
	if (subtype == EAPSIM_CLIENT_ERROR) {
		return 0;
	}

	switch (ess->state) {
	case EAPSIM_SERVER_START:
		switch (subtype) {
		/*
		 *	Pretty much anything else here is illegal, so we will retransmit the request.
		 */
		default:

			eap_sim_state_enter(request, handler, ess, EAPSIM_SERVER_START);
			return 1;
		/*
		 * 	A response to our EAP-Sim/Request/Start!
		 */
		case EAPSIM_START:
			return process_eap_sim_start(handler, vps);
		}

	case EAPSIM_SERVER_CHALLENGE:
		switch (subtype) {
		/*
		 *	Pretty much anything else here is illegal, so we will retransmit the request.
		 */
		default:
			eap_sim_state_enter(request, handler, ess, EAPSIM_SERVER_CHALLENGE);
			return 1;
		/*
		 *	A response to our EAP-Sim/Request/Challenge!
		 */
		case EAPSIM_CHALLENGE:
			return process_eap_sim_challenge(handler, vps);
		}

	default:
		rad_assert(0 == 1);
	}

	return 0;
}

/*
 *	The module name should be the only globally exported symbol.
 *	That is, everything else should be 'static'.
 */
extern rlm_eap_module_t rlm_eap_sim;
rlm_eap_module_t rlm_eap_sim = {
	.name		= "eap_sim",
	.session_init	= mod_session_init,	/* Initialise a new EAP session */
	.process	= mod_process,		/* Process next round of EAP method */
};
