/*
 * IEEE 802.1X-2010 Controlled Port of PAE state machine - CP state machine
 * Copyright (c) 2013-2014, Qualcomm Atheros, Inc.
 *
 * This software may be distributed under the terms of the BSD license.
 * See README for more details.
 */

#include "utils/includes.h"

#include "utils/common.h"
#include "utils/eloop.h"
#include "common/defs.h"
#include "common/ieee802_1x_defs.h"
#include "utils/state_machine.h"
#include "ieee802_1x_kay.h"
#include "ieee802_1x_secy_ops.h"
#include "pae/ieee802_1x_cp.h"

#define STATE_MACHINE_DATA struct ieee802_1x_cp_sm
#define STATE_MACHINE_DEBUG_PREFIX "CP"

static u64 default_cs_id = CS_ID_GCM_AES_128;

/* The variable defined in clause 12 in IEEE Std 802.1X-2010 */
enum connect_type { PENDING, UNAUTHENTICATED, AUTHENTICATED, SECURE };

struct ieee802_1x_cp_sm {
	enum cp_states {
		CP_BEGIN, CP_INIT, CP_CHANGE, CP_ALLOWED, CP_AUTHENTICATED,
		CP_SECURED, CP_RECEIVE, CP_RECEIVING, CP_READY, CP_TRANSMIT,
		CP_TRANSMITTING, CP_ABANDON, CP_RETIRE
	} CP_state;
	bool changed;

	/* CP -> Client */
	bool port_valid;

	/* Logon -> CP */
	enum connect_type connect;

	/* KaY -> CP */
	bool chgd_server; /* clear by CP */
	bool elected_self;
	enum confidentiality_offset cipher_offset;
	u64 cipher_suite;
	bool new_sak; /* clear by CP */
	struct ieee802_1x_mka_ki distributed_ki;
	u8 distributed_an;
	bool using_receive_sas;
	bool all_receiving;
	bool server_transmitting;
	bool using_transmit_sa;

	/* CP -> KaY */
	struct ieee802_1x_mka_ki *lki;
	u8 lan;
	bool ltx;
	bool lrx;
	struct ieee802_1x_mka_ki *oki;
	u8 oan;
	bool otx;
	bool orx;

	/* CP -> SecY */
	bool protect_frames;
	enum validate_frames validate_frames;

	bool replay_protect;
	u32 replay_window;

	u64 current_cipher_suite;
	enum confidentiality_offset confidentiality_offset;
	bool controlled_port_enabled;

	/* SecY -> CP */
	bool port_enabled; /* SecY->CP */

	/* private */
	u32 transmit_when;
	u32 transmit_delay;
	u32 retire_when;
	u32 retire_delay;

	/* not defined IEEE Std 802.1X-2010 */
	struct ieee802_1x_kay *kay;
};

static void ieee802_1x_cp_retire_when_timeout(void *eloop_ctx,
					      void *timeout_ctx);
static void ieee802_1x_cp_transmit_when_timeout(void *eloop_ctx,
						void *timeout_ctx);


static int changed_cipher(struct ieee802_1x_cp_sm *sm)
{
	return sm->confidentiality_offset != sm->cipher_offset ||
		sm->current_cipher_suite != sm->cipher_suite;
}


static int changed_connect(struct ieee802_1x_cp_sm *sm)
{
	return sm->connect != SECURE || sm->chgd_server || changed_cipher(sm);
}


SM_STATE(CP, INIT)
{
	SM_ENTRY(CP, INIT);

	sm->controlled_port_enabled = false;
	secy_cp_control_enable_port(sm->kay, sm->controlled_port_enabled);

	sm->port_valid = false;

	os_free(sm->lki);
	sm->lki = NULL;
	sm->ltx = false;
	sm->lrx = false;

	os_free(sm->oki);
	sm->oki = NULL;
	sm->otx = false;
	sm->orx = false;

	sm->port_enabled = true;
	sm->chgd_server = false;
}


SM_STATE(CP, CHANGE)
{
	SM_ENTRY(CP, CHANGE);

	sm->port_valid = false;
	sm->controlled_port_enabled = false;
	secy_cp_control_enable_port(sm->kay, sm->controlled_port_enabled);

	if (sm->lki)
		ieee802_1x_kay_delete_sas(sm->kay, sm->lki);
	if (sm->oki)
		ieee802_1x_kay_delete_sas(sm->kay, sm->oki);
	/* The standard doesn't say it but we should clear out the latest
	 * and old key values. Why would we keep advertising them if
	 * they've been deleted and the key server has been changed?
	 */
	os_free(sm->oki);
	sm->oki = NULL;
	sm->otx = false;
	sm->orx = false;
	sm->oan = 0;
	ieee802_1x_kay_set_old_sa_attr(sm->kay, sm->oki, sm->oan,
				       sm->otx, sm->orx);
	os_free(sm->lki);
	sm->lki = NULL;
	sm->lrx = false;
	sm->ltx = false;
	sm->lan = 0;
	ieee802_1x_kay_set_latest_sa_attr(sm->kay, sm->lki, sm->lan,
					  sm->ltx, sm->lrx);
}


SM_STATE(CP, ALLOWED)
{
	SM_ENTRY(CP, ALLOWED);

	sm->protect_frames = false;
	sm->replay_protect = false;
	sm->validate_frames = Checked;

	sm->port_valid = false;
	sm->controlled_port_enabled = true;

	secy_cp_control_enable_port(sm->kay, sm->controlled_port_enabled);
	secy_cp_control_protect_frames(sm->kay, sm->protect_frames);
	secy_cp_control_encrypt(sm->kay, sm->kay->macsec_encrypt);
	secy_cp_control_validate_frames(sm->kay, sm->validate_frames);
	secy_cp_control_replay(sm->kay, sm->replay_protect, sm->replay_window);
}


SM_STATE(CP, AUTHENTICATED)
{
	SM_ENTRY(CP, AUTHENTICATED);

	sm->protect_frames = false;
	sm->replay_protect = false;
	sm->validate_frames = Checked;

	sm->port_valid = false;
	sm->controlled_port_enabled = true;

	secy_cp_control_enable_port(sm->kay, sm->controlled_port_enabled);
	secy_cp_control_protect_frames(sm->kay, sm->protect_frames);
	secy_cp_control_encrypt(sm->kay, sm->kay->macsec_encrypt);
	secy_cp_control_validate_frames(sm->kay, sm->validate_frames);
	secy_cp_control_replay(sm->kay, sm->replay_protect, sm->replay_window);
}


SM_STATE(CP, SECURED)
{
	SM_ENTRY(CP, SECURED);

	sm->chgd_server = false;

	sm->protect_frames = sm->kay->macsec_protect;
	sm->replay_protect = sm->kay->macsec_replay_protect;
	sm->validate_frames = sm->kay->macsec_validate;

	/* NOTE: now no other than default cipher suite (AES-GCM-128) */
	sm->current_cipher_suite = sm->cipher_suite;
	secy_cp_control_current_cipher_suite(sm->kay, sm->current_cipher_suite);

	sm->confidentiality_offset = sm->cipher_offset;

	sm->port_valid = true;

	secy_cp_control_confidentiality_offset(sm->kay,
					       sm->confidentiality_offset);
	secy_cp_control_protect_frames(sm->kay, sm->protect_frames);
	secy_cp_control_encrypt(sm->kay, sm->kay->macsec_encrypt);
	secy_cp_control_validate_frames(sm->kay, sm->validate_frames);
	secy_cp_control_replay(sm->kay, sm->replay_protect, sm->replay_window);
}


SM_STATE(CP, RECEIVE)
{
	SM_ENTRY(CP, RECEIVE);

	sm->lki = os_malloc(sizeof(*sm->lki));
	if (!sm->lki) {
		wpa_printf(MSG_ERROR, "CP-%s: Out of memory", __func__);
		return;
	}
	os_memcpy(sm->lki, &sm->distributed_ki, sizeof(*sm->lki));
	sm->lan = sm->distributed_an;
	sm->ltx = false;
	sm->lrx = false;
	ieee802_1x_kay_set_latest_sa_attr(sm->kay, sm->lki, sm->lan,
					  sm->ltx, sm->lrx);
	ieee802_1x_kay_create_sas(sm->kay, sm->lki);
	ieee802_1x_kay_enable_rx_sas(sm->kay, sm->lki);
	sm->new_sak = false;
	sm->all_receiving = false;
}


SM_STATE(CP, RECEIVING)
{
	SM_ENTRY(CP, RECEIVING);

	sm->lrx = true;
	ieee802_1x_kay_set_latest_sa_attr(sm->kay, sm->lki, sm->lan,
					  sm->ltx, sm->lrx);
	sm->transmit_when = sm->transmit_delay;
	eloop_cancel_timeout(ieee802_1x_cp_transmit_when_timeout, sm, NULL);
	eloop_register_timeout(sm->transmit_when / 1000, 0,
			       ieee802_1x_cp_transmit_when_timeout, sm, NULL);
	/* the electedSelf have been set before CP entering to RECEIVING
	 * but the CP will transmit from RECEIVING to READY under
	 * the !electedSelf when KaY is not key server */
	ieee802_1x_cp_sm_step(sm);
	sm->using_receive_sas = false;
	sm->server_transmitting = false;
}


SM_STATE(CP, READY)
{
	SM_ENTRY(CP, READY);

	ieee802_1x_kay_enable_new_info(sm->kay);
}


SM_STATE(CP, TRANSMIT)
{
	SM_ENTRY(CP, TRANSMIT);

	sm->controlled_port_enabled = true;
	secy_cp_control_enable_port(sm->kay, sm->controlled_port_enabled);
	sm->ltx = true;
	ieee802_1x_kay_set_latest_sa_attr(sm->kay, sm->lki, sm->lan,
					  sm->ltx, sm->lrx);
	ieee802_1x_kay_enable_tx_sas(sm->kay,  sm->lki);
	sm->all_receiving = false;
	sm->server_transmitting = false;
}


SM_STATE(CP, TRANSMITTING)
{
	SM_ENTRY(CP, TRANSMITTING);
	sm->retire_when = sm->orx ? sm->retire_delay : 0;
	sm->otx = false;
	ieee802_1x_kay_set_old_sa_attr(sm->kay, sm->oki, sm->oan,
				       sm->otx, sm->orx);
	ieee802_1x_kay_enable_new_info(sm->kay);
	eloop_cancel_timeout(ieee802_1x_cp_retire_when_timeout, sm, NULL);
	eloop_register_timeout(sm->retire_when / 1000, 0,
			       ieee802_1x_cp_retire_when_timeout, sm, NULL);
	sm->using_transmit_sa = false;
}


SM_STATE(CP, ABANDON)
{
	SM_ENTRY(CP, ABANDON);
	sm->lrx = false;
	ieee802_1x_kay_set_latest_sa_attr(sm->kay, sm->lki, sm->lan,
					  sm->ltx, sm->lrx);
	ieee802_1x_kay_delete_sas(sm->kay, sm->lki);

	os_free(sm->lki);
	sm->lki = NULL;
	ieee802_1x_kay_set_latest_sa_attr(sm->kay, sm->lki, sm->lan,
					  sm->ltx, sm->lrx);
}


SM_STATE(CP, RETIRE)
{
	SM_ENTRY(CP, RETIRE);
	if (sm->oki) {
		ieee802_1x_kay_delete_sas(sm->kay, sm->oki);
		os_free(sm->oki);
		sm->oki = NULL;
	}
	sm->oki = sm->lki;
	sm->otx = sm->ltx;
	sm->orx = sm->lrx;
	sm->oan = sm->lan;
	ieee802_1x_kay_set_old_sa_attr(sm->kay, sm->oki, sm->oan,
				       sm->otx, sm->orx);
	sm->lki = NULL;
	sm->ltx = false;
	sm->lrx = false;
	sm->lan = 0;
	ieee802_1x_kay_set_latest_sa_attr(sm->kay, sm->lki, sm->lan,
					  sm->ltx, sm->lrx);
}


/**
 * CP state machine handler entry
 */
SM_STEP(CP)
{
	if (!sm->port_enabled)
		SM_ENTER(CP, INIT);

	switch (sm->CP_state) {
	case CP_BEGIN:
		SM_ENTER(CP, INIT);
		break;

	case CP_INIT:
		SM_ENTER(CP, CHANGE);
		break;

	case CP_CHANGE:
		if (sm->connect == UNAUTHENTICATED)
			SM_ENTER(CP, ALLOWED);
		else if (sm->connect == AUTHENTICATED)
			SM_ENTER(CP, AUTHENTICATED);
		else if (sm->connect == SECURE)
			SM_ENTER(CP, SECURED);
		break;

	case CP_ALLOWED:
		if (sm->connect != UNAUTHENTICATED)
			SM_ENTER(CP, CHANGE);
		break;

	case CP_AUTHENTICATED:
		if (sm->connect != AUTHENTICATED)
			SM_ENTER(CP, CHANGE);
		break;

	case CP_SECURED:
		if (changed_connect(sm))
			SM_ENTER(CP, CHANGE);
		else if (sm->new_sak)
			SM_ENTER(CP, RECEIVE);
		break;

	case CP_RECEIVE:
		if (sm->using_receive_sas)
			SM_ENTER(CP, RECEIVING);
		break;

	case CP_RECEIVING:
		if (sm->new_sak || changed_connect(sm))
			SM_ENTER(CP, ABANDON);
		if (!sm->elected_self)
			SM_ENTER(CP, READY);
		if (sm->elected_self &&
		    (sm->all_receiving || !sm->controlled_port_enabled ||
		     !sm->transmit_when))
			SM_ENTER(CP, TRANSMIT);
		break;

	case CP_TRANSMIT:
		if (sm->using_transmit_sa)
			SM_ENTER(CP, TRANSMITTING);
		break;

	case CP_TRANSMITTING:
		if (!sm->retire_when || changed_connect(sm))
			SM_ENTER(CP, RETIRE);
		break;

	case CP_RETIRE:
		if (changed_connect(sm))
			SM_ENTER(CP, CHANGE);
		else if (sm->new_sak)
			SM_ENTER(CP, RECEIVE);
		break;

	case CP_READY:
		if (sm->new_sak || changed_connect(sm))
			SM_ENTER(CP, ABANDON);
		if (sm->server_transmitting || !sm->controlled_port_enabled)
			SM_ENTER(CP, TRANSMIT);
		break;
	case CP_ABANDON:
		if (changed_connect(sm))
			SM_ENTER(CP, RETIRE);
		else if (sm->new_sak)
			SM_ENTER(CP, RECEIVE);
		break;
	default:
		wpa_printf(MSG_ERROR, "CP: the state machine is not defined");
		break;
	}
}


/**
 * ieee802_1x_cp_sm_init -
 */
struct ieee802_1x_cp_sm * ieee802_1x_cp_sm_init(struct ieee802_1x_kay *kay)
{
	struct ieee802_1x_cp_sm *sm;

	sm = os_zalloc(sizeof(*sm));
	if (sm == NULL) {
		wpa_printf(MSG_ERROR, "CP-%s: out of memory", __func__);
		return NULL;
	}

	sm->kay = kay;

	sm->port_valid = false;

	sm->chgd_server = false;

	sm->protect_frames = kay->macsec_protect;
	sm->validate_frames = kay->macsec_validate;
	sm->replay_protect = kay->macsec_replay_protect;
	sm->replay_window = kay->macsec_replay_window;

	sm->controlled_port_enabled = false;

	sm->lki = NULL;
	sm->lrx = false;
	sm->ltx = false;
	sm->oki = NULL;
	sm->orx = false;
	sm->otx = false;

	sm->current_cipher_suite = default_cs_id;
	sm->cipher_suite = default_cs_id;
	sm->cipher_offset = CONFIDENTIALITY_OFFSET_0;
	sm->confidentiality_offset = sm->cipher_offset;
	sm->transmit_delay = MKA_LIFE_TIME;
	sm->retire_delay = MKA_SAK_RETIRE_TIME;
	sm->CP_state = CP_BEGIN;
	sm->changed = false;

	wpa_printf(MSG_DEBUG, "CP: state machine created");

	secy_cp_control_protect_frames(sm->kay, sm->protect_frames);
	secy_cp_control_encrypt(sm->kay, sm->kay->macsec_encrypt);
	secy_cp_control_validate_frames(sm->kay, sm->validate_frames);
	secy_cp_control_replay(sm->kay, sm->replay_protect, sm->replay_window);
	secy_cp_control_enable_port(sm->kay, sm->controlled_port_enabled);
	secy_cp_control_confidentiality_offset(sm->kay,
					       sm->confidentiality_offset);

	SM_STEP_RUN(CP);

	return sm;
}


static void ieee802_1x_cp_step_run(struct ieee802_1x_cp_sm *sm)
{
	enum cp_states prev_state;
	int i;

	for (i = 0; i < 100; i++) {
		prev_state = sm->CP_state;
		SM_STEP_RUN(CP);
		if (prev_state == sm->CP_state)
			break;
	}
}


static void ieee802_1x_cp_step_cb(void *eloop_ctx, void *timeout_ctx)
{
	struct ieee802_1x_cp_sm *sm = eloop_ctx;
	ieee802_1x_cp_step_run(sm);
}


/**
 * ieee802_1x_cp_sm_deinit -
 */
void ieee802_1x_cp_sm_deinit(struct ieee802_1x_cp_sm *sm)
{
	wpa_printf(MSG_DEBUG, "CP: state machine removed");
	if (!sm)
		return;

	eloop_cancel_timeout(ieee802_1x_cp_retire_when_timeout, sm, NULL);
	eloop_cancel_timeout(ieee802_1x_cp_transmit_when_timeout, sm, NULL);
	eloop_cancel_timeout(ieee802_1x_cp_step_cb, sm, NULL);
	os_free(sm->lki);
	os_free(sm->oki);
	os_free(sm);
}


/**
 * ieee802_1x_cp_connect_pending
 */
void ieee802_1x_cp_connect_pending(void *cp_ctx)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;

	sm->connect = PENDING;
}


/**
 * ieee802_1x_cp_connect_unauthenticated
 */
void ieee802_1x_cp_connect_unauthenticated(void *cp_ctx)
{
	struct ieee802_1x_cp_sm *sm = (struct ieee802_1x_cp_sm *)cp_ctx;

	sm->connect = UNAUTHENTICATED;
}


/**
 * ieee802_1x_cp_connect_authenticated
 */
void ieee802_1x_cp_connect_authenticated(void *cp_ctx)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;

	sm->connect = AUTHENTICATED;
}


/**
 * ieee802_1x_cp_connect_secure
 */
void ieee802_1x_cp_connect_secure(void *cp_ctx)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;

	sm->connect = SECURE;
}


/**
 * ieee802_1x_cp_set_chgdserver -
 */
void ieee802_1x_cp_signal_chgdserver(void *cp_ctx)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;

	sm->chgd_server = true;
}


/**
 * ieee802_1x_cp_set_electedself -
 */
void ieee802_1x_cp_set_electedself(void *cp_ctx, bool status)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->elected_self = status;
}


/**
 * ieee802_1x_cp_set_ciphersuite -
 */
void ieee802_1x_cp_set_ciphersuite(void *cp_ctx, u64 cs)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->cipher_suite = cs;
}


/**
 * ieee802_1x_cp_set_offset -
 */
void ieee802_1x_cp_set_offset(void *cp_ctx, enum confidentiality_offset offset)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->cipher_offset = offset;
}


/**
 * ieee802_1x_cp_signal_newsak -
 */
void ieee802_1x_cp_signal_newsak(void *cp_ctx)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->new_sak = true;
}


/**
 * ieee802_1x_cp_set_distributedki -
 */
void ieee802_1x_cp_set_distributedki(void *cp_ctx,
				     const struct ieee802_1x_mka_ki *dki)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	os_memcpy(&sm->distributed_ki, dki, sizeof(struct ieee802_1x_mka_ki));
}


/**
 * ieee802_1x_cp_set_distributedan -
 */
void ieee802_1x_cp_set_distributedan(void *cp_ctx, u8 an)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->distributed_an = an;
}


/**
 * ieee802_1x_cp_set_usingreceivesas -
 */
void ieee802_1x_cp_set_usingreceivesas(void *cp_ctx, bool status)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->using_receive_sas = status;
}


/**
 * ieee802_1x_cp_set_allreceiving -
 */
void ieee802_1x_cp_set_allreceiving(void *cp_ctx, bool status)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->all_receiving = status;
}


/**
 * ieee802_1x_cp_set_servertransmitting -
 */
void ieee802_1x_cp_set_servertransmitting(void *cp_ctx, bool status)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->server_transmitting = status;
}


/**
 * ieee802_1x_cp_set_usingtransmitsas -
 */
void ieee802_1x_cp_set_usingtransmitas(void *cp_ctx, bool status)
{
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	sm->using_transmit_sa = status;
}


/**
 * ieee802_1x_cp_sm_step - Advance EAPOL state machines
 * @sm: EAPOL state machine
 *
 * This function is called to advance CP state machines after any change
 * that could affect their state.
 */
void ieee802_1x_cp_sm_step(void *cp_ctx)
{
	/*
	 * Run ieee802_1x_cp_step_run from a registered timeout
	 * to make sure that other possible timeouts/events are processed
	 * and to avoid long function call chains.
	 */
	struct ieee802_1x_cp_sm *sm = cp_ctx;
	eloop_cancel_timeout(ieee802_1x_cp_step_cb, sm, NULL);
	eloop_register_timeout(0, 0, ieee802_1x_cp_step_cb, sm, NULL);
}


static void ieee802_1x_cp_retire_when_timeout(void *eloop_ctx,
					      void *timeout_ctx)
{
	struct ieee802_1x_cp_sm *sm = eloop_ctx;
	sm->retire_when = 0;
	ieee802_1x_cp_step_run(sm);
}


static void
ieee802_1x_cp_transmit_when_timeout(void *eloop_ctx, void *timeout_ctx)
{
	struct ieee802_1x_cp_sm *sm = eloop_ctx;
	sm->transmit_when = 0;
	ieee802_1x_cp_step_run(sm);
}
