/*
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * Author: Nikos Mavrogiannopoulos
 *
 * This file is part of GnuTLS.
 *
 * The GnuTLS is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>
 *
 */

#include "gnutls_int.h"
#include "errors.h"
#include "handshake.h"
#include "tls13/key_update.h"
#include "mem.h"
#include "mbuffers.h"
#include "secrets.h"
#include "system/ktls.h"

#define KEY_UPDATES_WINDOW 1000
#define KEY_UPDATES_PER_WINDOW 8

/*
 * Sets kTLS keys if enabled.
 * If this operation fails with GNUTLS_E_INTERNAL_ERROR, KTLS is disabled
 * because KTLS most likely doesn't support key update.
 */
static inline int set_ktls_keys(gnutls_session_t session,
				gnutls_transport_ktls_enable_flags_t iface)
{
	if (_gnutls_ktls_set_keys(session, iface) < 0) {
		session->internals.ktls_enabled = 0;
		session->internals.invalid_connection = true;
		session->internals.resumable = false;
		_gnutls_audit_log(
			session,
			"invalidating session: KTLS - couldn't update keys\n");
		return GNUTLS_E_INTERNAL_ERROR;
	}
	return 0;
}

static int update_keys(gnutls_session_t session, hs_stage_t stage)
{
	int ret;

	ret = _tls13_update_secret(session,
				   session->key.proto.tls13.temp_secret,
				   session->key.proto.tls13.temp_secret_size);
	if (ret < 0)
		return gnutls_assert_val(ret);

	_gnutls_epoch_bump(session);
	ret = _gnutls_epoch_dup(session, EPOCH_READ_CURRENT);
	if (ret < 0)
		return gnutls_assert_val(ret);

	/* If we send a key update during early start, only update our
	 * write keys */
	if (session->internals.recv_state == RECV_STATE_EARLY_START) {
		ret = _tls13_write_connection_state_init(session, stage);
		if (ret < 0)
			return gnutls_assert_val(ret);

		if (IS_KTLS_ENABLED(session, GNUTLS_KTLS_SEND))
			ret = set_ktls_keys(session, GNUTLS_KTLS_SEND);
	} else {
		ret = _tls13_connection_state_init(session, stage);
		if (ret < 0)
			return gnutls_assert_val(ret);

		if (IS_KTLS_ENABLED(session, GNUTLS_KTLS_SEND) &&
		    stage == STAGE_UPD_OURS)
			ret = set_ktls_keys(session, GNUTLS_KTLS_SEND);
		else if (IS_KTLS_ENABLED(session, GNUTLS_KTLS_RECV) &&
			 stage == STAGE_UPD_PEERS)
			ret = set_ktls_keys(session, GNUTLS_KTLS_RECV);
	}
	if (ret < 0)
		return gnutls_assert_val(ret);

	return 0;
}

int _gnutls13_recv_key_update(gnutls_session_t session, gnutls_buffer_st *buf)
{
	int ret;
	struct timespec now;

	if (buf->length != 1)
		return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);

	gnutls_gettime(&now);

	/* Roll over the counter if the time window has elapsed */
	if (session->internals.key_update_count == 0 ||
	    timespec_sub_ms(&now, &session->internals.last_key_update) >
		    KEY_UPDATES_WINDOW) {
		session->internals.last_key_update = now;
		session->internals.key_update_count = 0;
	}

	if (unlikely(++session->internals.key_update_count >
		     KEY_UPDATES_PER_WINDOW)) {
		_gnutls_debug_log(
			"reached maximum number of key updates per %d milliseconds (%d)\n",
			KEY_UPDATES_WINDOW, KEY_UPDATES_PER_WINDOW);
		return gnutls_assert_val(GNUTLS_E_TOO_MANY_HANDSHAKE_PACKETS);
	}

	_gnutls_epoch_gc(session);

	_gnutls_handshake_log("HSK[%p]: received TLS 1.3 key update (%u)\n",
			      session, (unsigned)buf->data[0]);

	switch (buf->data[0]) {
	case 0:
		/* peer updated its key, not requested our key update */
		ret = update_keys(session, STAGE_UPD_PEERS);
		if (ret < 0)
			return gnutls_assert_val(ret);

		break;
	case 1:
		if (session->internals.hsk_flags & HSK_KEY_UPDATE_ASKED) {
			/* if we had asked a key update we shouldn't get this
			 * reply */
			return gnutls_assert_val(GNUTLS_E_ILLEGAL_PARAMETER);
		}

		/* peer updated its key, requested our key update */
		ret = update_keys(session, STAGE_UPD_PEERS);
		if (ret < 0)
			return gnutls_assert_val(ret);

		/* we mark that a key update is schedule, and it
		 * will be performed prior to sending the next application
		 * message.
		 */
		if (session->internals.rsend_state == RECORD_SEND_NORMAL)
			session->internals.rsend_state =
				RECORD_SEND_KEY_UPDATE_1;
		else if (session->internals.rsend_state == RECORD_SEND_CORKED)
			session->internals.rsend_state =
				RECORD_SEND_CORKED_TO_KU;

		break;
	default:
		return gnutls_assert_val(GNUTLS_E_RECEIVED_ILLEGAL_PARAMETER);
	}

	session->internals.hsk_flags &= ~(unsigned)(HSK_KEY_UPDATE_ASKED);

	return 0;
}

int _gnutls13_send_key_update(gnutls_session_t session, unsigned again,
			      unsigned flags /* GNUTLS_KU_* */)
{
	int ret;
	mbuffer_st *bufel = NULL;
	uint8_t val;

	if (again == 0) {
		if (flags & GNUTLS_KU_PEER) {
			/* mark that we asked a key update to prevent an
			 * infinite ping pong when receiving the reply */
			session->internals.hsk_flags |= HSK_KEY_UPDATE_ASKED;
			val = 0x01;
		} else {
			val = 0x00;
		}

		_gnutls_handshake_log("HSK[%p]: sending key update (%u)\n",
				      session, (unsigned)val);

		bufel = _gnutls_handshake_alloc(session, 1);
		if (bufel == NULL)
			return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR);

		_mbuffer_set_udata_size(bufel, 0);
		ret = _mbuffer_append_data(bufel, &val, 1);
		if (ret < 0) {
			gnutls_assert();
			goto cleanup;
		}
	}

	return _gnutls_send_handshake(session, bufel,
				      GNUTLS_HANDSHAKE_KEY_UPDATE);

cleanup:
	_mbuffer_xfree(&bufel);
	return ret;
}

/**
 * gnutls_session_key_update:
 * @session: is a #gnutls_session_t type.
 * @flags: zero of %GNUTLS_KU_PEER
 *
 * This function will update/refresh the session keys when the
 * TLS protocol is 1.3 or better. The peer is notified of the
 * update by sending a message, so this function should be
 * treated similarly to gnutls_record_send() --i.e., it may
 * return %GNUTLS_E_AGAIN or %GNUTLS_E_INTERRUPTED.
 *
 * When this flag %GNUTLS_KU_PEER is specified, this function
 * in addition to updating the local keys, will ask the peer to
 * refresh its keys too.
 *
 * If the negotiated version is not TLS 1.3 or better this
 * function will return %GNUTLS_E_INVALID_REQUEST.
 *
 * Returns: %GNUTLS_E_SUCCESS on success, otherwise a negative error code.
 *
 * Since: 3.6.3
 **/
int gnutls_session_key_update(gnutls_session_t session, unsigned flags)
{
	int ret;
	const version_entry_st *vers = get_version(session);

	if (!vers->tls13_sem)
		return GNUTLS_E_INVALID_REQUEST;

	ret = _gnutls13_send_key_update(session, AGAIN(STATE150), flags);
	STATE = STATE150;

	if (ret < 0) {
		gnutls_assert();
		return ret;
	}
	STATE = STATE0;

	_gnutls_epoch_gc(session);

	/* it was completely sent, update the keys */
	ret = update_keys(session, STAGE_UPD_OURS);
	if (ret < 0)
		return gnutls_assert_val(ret);

	return 0;
}
