/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  For more information, see <https://www.knot-dns.cz/>
 */

#include <assert.h>
#include <inttypes.h>
#include <time.h>
#include <stdint.h>

#include "libdnssec/error.h"
#include "libdnssec/tsig.h"
#include "libknot/attribute.h"
#include "libknot/tsig-op.h"
#include "libknot/errcode.h"
#include "libknot/descriptor.h"
#include "libknot/rrtype/tsig.h"
#include "libknot/packet/wire.h"
#include "libknot/consts.h"
#include "libknot/packet/rrset-wire.h"
#include "libknot/wire.h"
#include "contrib/string.h"

const int KNOT_TSIG_MAX_DIGEST_SIZE = 64;    // size of HMAC-SHA512 digest
const uint16_t KNOT_TSIG_FUDGE_DEFAULT = 300;  // default Fudge value

static int check_algorithm(const knot_rrset_t *tsig_rr)
{
	if (tsig_rr == NULL) {
		return KNOT_EINVAL;
	}

	const knot_dname_t *alg_name = knot_tsig_rdata_alg_name(tsig_rr);
	if (!alg_name) {
		return KNOT_EMALF;
	}

	dnssec_tsig_algorithm_t alg = dnssec_tsig_algorithm_from_dname(alg_name);
	if (alg == DNSSEC_TSIG_UNKNOWN) {
		return KNOT_TSIG_EBADKEY;
	}

	return KNOT_EOK;
}

static int check_key(const knot_rrset_t *tsig_rr, const knot_tsig_key_t *tsig_key)
{
	if (tsig_rr == NULL || tsig_key == NULL) {
		return KNOT_EINVAL;
	}

	const knot_dname_t *tsig_name = tsig_rr->owner;
	if (!tsig_name) {
		return KNOT_EMALF;
	}

	if (!knot_dname_is_equal(tsig_name, tsig_key->name)) {
		return KNOT_TSIG_EBADKEY;
	}

	return KNOT_EOK;
}

static int compute_digest(const uint8_t *wire, size_t wire_len,
                          uint8_t *digest, size_t *digest_len,
                          const knot_tsig_key_t *key)
{
	if (!wire || !digest || !digest_len || !key) {
		return KNOT_EINVAL;
	}

	if (!key->name) {
		return KNOT_EMALF;
	}

	dnssec_tsig_ctx_t *ctx = NULL;
	int result = dnssec_tsig_new(&ctx, key->algorithm, &key->secret);
	if (result != DNSSEC_EOK) {
		return KNOT_TSIG_EBADSIG;
	}

	dnssec_binary_t cover = { .data = (uint8_t *)wire, .size = wire_len };
	dnssec_tsig_add(ctx, &cover);

	*digest_len = dnssec_tsig_size(ctx);
	dnssec_tsig_write(ctx, digest);
	dnssec_tsig_free(ctx);

	return KNOT_EOK;
}

static int check_time_signed(const knot_rrset_t *tsig_rr, uint64_t prev_time_signed)
{
	if (!tsig_rr) {
		return KNOT_EINVAL;
	}

	/* Get the time signed and fudge values. */
	uint64_t time_signed = knot_tsig_rdata_time_signed(tsig_rr);
	if (time_signed == 0) {
		return KNOT_TSIG_EBADTIME;
	}
	uint16_t fudge = knot_tsig_rdata_fudge(tsig_rr);
	if (fudge == 0) {
		return KNOT_TSIG_EBADTIME;
	}

	/* Get the current time. */
	time_t curr_time = time(NULL);

	/*!< \todo bleeding eyes. */
	double diff = difftime(curr_time, (time_t)time_signed);

	if (diff > fudge || diff < -fudge) {
		return KNOT_TSIG_EBADTIME;
	}

	diff = difftime((time_t)time_signed, prev_time_signed);

	if (diff < 0) {
		return KNOT_TSIG_EBADTIME;
	}

	return KNOT_EOK;
}

static int write_tsig_variables(uint8_t *wire, const knot_rrset_t *tsig_rr)
{
	if (wire == NULL || tsig_rr == NULL) {
		return KNOT_EINVAL;
	}

	/* Copy TSIG variables - starting with key name. */
	const knot_dname_t *tsig_owner = tsig_rr->owner;
	if (!tsig_owner) {
		return KNOT_EINVAL;
	}

	int offset = 0;

	offset += knot_dname_to_wire(wire + offset, tsig_owner, KNOT_DNAME_MAXLEN);

	/*!< \todo which order? */

	/* Copy class. */
	knot_wire_write_u16(wire + offset, tsig_rr->rclass);
	offset += sizeof(uint16_t);

	/* Copy TTL - always 0. */
	knot_wire_write_u32(wire + offset, tsig_rr->ttl);
	offset += sizeof(uint32_t);

	/* Copy alg name. */
	const knot_dname_t *alg_name = knot_tsig_rdata_alg_name(tsig_rr);
	if (!alg_name) {
		return KNOT_EINVAL;
	}

	/* Te algorithm name must be in canonical form, i.e. in lowercase. */
	uint8_t *alg_name_wire = wire + offset;
	offset += knot_dname_to_wire(alg_name_wire, alg_name, KNOT_DNAME_MAXLEN);
	knot_dname_to_lower(alg_name_wire);

	/* Following data are written in network order. */
	/* Time signed. */
	knot_wire_write_u48(wire + offset, knot_tsig_rdata_time_signed(tsig_rr));
	offset += 6;
	/* Fudge. */
	knot_wire_write_u16(wire + offset, knot_tsig_rdata_fudge(tsig_rr));
	offset += sizeof(uint16_t);
	/* TSIG error. */
	knot_wire_write_u16(wire + offset, knot_tsig_rdata_error(tsig_rr));
	offset += sizeof(uint16_t);
	/* Get other data length. */
	uint16_t other_data_length = knot_tsig_rdata_other_data_length(tsig_rr);
	/* Get other data. */
	const uint8_t *other_data = knot_tsig_rdata_other_data(tsig_rr);
	if (!other_data) {
		return KNOT_EINVAL;
	}

	/*
	 * We cannot write the whole other_data, as it contains its length in
	 * machine order.
	 */
	knot_wire_write_u16(wire + offset, other_data_length);
	offset += sizeof(uint16_t);

	/* Skip the length. */
	memcpy(wire + offset, other_data, other_data_length);

	return KNOT_EOK;
}

static int wire_write_timers(uint8_t *wire, const knot_rrset_t *tsig_rr)
{
	if (wire == NULL || tsig_rr == NULL) {
		return KNOT_EINVAL;
	}

	//write time signed
	knot_wire_write_u48(wire, knot_tsig_rdata_time_signed(tsig_rr));
	//write fudge
	knot_wire_write_u16(wire + 6, knot_tsig_rdata_fudge(tsig_rr));

	return KNOT_EOK;
}

static int create_sign_wire(const uint8_t *msg, size_t msg_len,
                            const uint8_t *request_mac, size_t request_mac_len,
                            uint8_t *digest, size_t *digest_len,
                            const knot_rrset_t *tmp_tsig,
                            const knot_tsig_key_t *key)
{
	if (!msg || !key || digest_len == NULL) {
		return KNOT_EINVAL;
	}

	/* Create tmp TSIG. */
	int ret = KNOT_EOK;

	/*
	 * Create tmp wire, it should contain message
	 * plus request mac plus tsig variables.
	 */
	size_t wire_len = msg_len + request_mac_len + (request_mac_len > 0 ? 2 : 0) +
	                  knot_tsig_rdata_tsig_variables_length(tmp_tsig);
	uint8_t *wire = malloc(wire_len);
	if (!wire) {
		return KNOT_ENOMEM;
	}

	memset(wire, 0, wire_len);

	uint8_t *pos = wire;

	/* Copy the request MAC - should work even if NULL. */
	if (request_mac_len > 0) {
		knot_wire_write_u16(pos, request_mac_len);
		pos += 2;
		memcpy(pos, request_mac, request_mac_len);
	}
	pos += request_mac_len;
	/* Copy the original message. */
	memcpy(pos, msg, msg_len);
	pos += msg_len;
	/* Copy TSIG variables. */
	ret = write_tsig_variables(pos, tmp_tsig);
	if (ret != KNOT_EOK) {
		free(wire);
		return ret;
	}

	/* Compute digest. */
	ret = compute_digest(wire, wire_len, digest, digest_len, key);
	if (ret != KNOT_EOK) {
		*digest_len = 0;
		free(wire);
		return ret;
	}

	free(wire);

	return KNOT_EOK;
}

static int create_sign_wire_next(const uint8_t *msg, size_t msg_len,
                                 const uint8_t *prev_mac, size_t prev_mac_len,
                                 uint8_t *digest, size_t *digest_len,
                                 const knot_rrset_t *tmp_tsig,
                                 const knot_tsig_key_t *key)
{
	if (!msg || !key || digest_len == NULL) {
		return KNOT_EINVAL;
	}

	/* Create tmp TSIG. */
	int ret = KNOT_EOK;

	/*
	 * Create tmp wire, it should contain message
	 * plus request mac plus tsig variables.
	 */
	size_t wire_len = msg_len + prev_mac_len + knot_tsig_rdata_tsig_timers_length() + 2;
	uint8_t *wire = malloc(wire_len);
	if (!wire) {
		return KNOT_ENOMEM;
	}

	memset(wire, 0, wire_len);

	/* Copy the request MAC - should work even if NULL. */
	knot_wire_write_u16(wire, prev_mac_len);
	memcpy(wire + 2, prev_mac, prev_mac_len);
	/* Copy the original message. */
	memcpy(wire + prev_mac_len + 2, msg, msg_len);
	/* Copy TSIG variables. */

	ret = wire_write_timers(wire + prev_mac_len + msg_len + 2, tmp_tsig);
	if (ret != KNOT_EOK) {
		free(wire);
		return ret;
	}

	/* Compute digest. */
	ret = compute_digest(wire, wire_len, digest, digest_len, key);
	if (ret != KNOT_EOK) {
		*digest_len = 0;
		free(wire);
		return ret;
	}

	free(wire);

	return KNOT_EOK;
}

_public_
int knot_tsig_sign(uint8_t *msg, size_t *msg_len, size_t msg_max_len,
                   const uint8_t *request_mac, size_t request_mac_len,
                   uint8_t *digest, size_t *digest_len,
                   const knot_tsig_key_t *key, uint16_t tsig_rcode,
                   uint64_t request_time_signed)
{
	if (!msg || !msg_len || !key || digest == NULL || digest_len == NULL) {
		return KNOT_EINVAL;
	}

	knot_rrset_t *tmp_tsig = knot_rrset_new(key->name, KNOT_RRTYPE_TSIG,
	                                        KNOT_CLASS_ANY, 0, NULL);
	if (!tmp_tsig) {
		return KNOT_ENOMEM;
	}

	/* Create rdata for TSIG RR. */
	uint16_t rdata_rcode = KNOT_RCODE_NOERROR;
	if (tsig_rcode == KNOT_RCODE_BADTIME) {
		rdata_rcode = tsig_rcode;
	}

	const uint8_t *alg_name = dnssec_tsig_algorithm_to_dname(key->algorithm);
	size_t alg_size = dnssec_tsig_algorithm_size(key->algorithm);
	knot_tsig_create_rdata(tmp_tsig, alg_name, alg_size, rdata_rcode);

	/* Distinguish BADTIME response. */
	if (tsig_rcode == KNOT_RCODE_BADTIME) {
		/* Set client's time signed into the time signed field. */
		knot_tsig_rdata_set_time_signed(tmp_tsig, request_time_signed);

		/* Store current time into Other data. */
		uint8_t time_signed[6];
		time_t now = time(NULL);
		knot_wire_write_u48(time_signed, now);

		knot_tsig_rdata_set_other_data(tmp_tsig, 6, time_signed);
	} else {
		knot_tsig_rdata_set_time_signed(tmp_tsig, time(NULL));

		/* Set other len. */
		knot_tsig_rdata_set_other_data(tmp_tsig, 0, 0);
	}

	knot_tsig_rdata_set_fudge(tmp_tsig, KNOT_TSIG_FUDGE_DEFAULT);

	/* Set original ID */
	knot_tsig_rdata_set_orig_id(tmp_tsig, knot_wire_get_id(msg));

	uint8_t digest_tmp[KNOT_TSIG_MAX_DIGEST_SIZE];
	size_t digest_tmp_len = 0;

	int ret = create_sign_wire(msg, *msg_len, /*msg_max_len,*/
	                           request_mac, request_mac_len,
	                           digest_tmp, &digest_tmp_len, tmp_tsig, key);
	if (ret != KNOT_EOK) {
		knot_rrset_free(tmp_tsig, NULL);
		return ret;
	}

	/* Set the digest. */
	knot_tsig_rdata_set_mac(tmp_tsig, digest_tmp_len, digest_tmp);

	/* Write RRSet to wire */
	ret = knot_rrset_to_wire(tmp_tsig, msg + *msg_len,
	                         msg_max_len - *msg_len, NULL);
	if (ret < 0) {
		*digest_len = 0;
		knot_rrset_free(tmp_tsig, NULL);
		return ret;
	}

	size_t tsig_wire_len = ret;

	knot_rrset_free(tmp_tsig, NULL);

	*msg_len += tsig_wire_len;

	uint16_t arcount = knot_wire_get_arcount(msg);
	knot_wire_set_arcount(msg, ++arcount);

	/* everything went ok, save the digest to the output parameter */
	memcpy(digest, digest_tmp, digest_tmp_len);
	*digest_len = digest_tmp_len;

	return KNOT_EOK;
}

_public_
int knot_tsig_sign_next(uint8_t *msg, size_t *msg_len, size_t msg_max_len,
                        const uint8_t *prev_digest, size_t prev_digest_len,
                        uint8_t *digest, size_t *digest_len,
                        const knot_tsig_key_t *key, uint8_t *to_sign,
                        size_t to_sign_len)
{
	if (!msg || !msg_len || !key || !digest || !digest_len) {
		return KNOT_EINVAL;
	}

	uint8_t digest_tmp[KNOT_TSIG_MAX_DIGEST_SIZE];
	size_t digest_tmp_len = 0;
	knot_rrset_t *tmp_tsig = knot_rrset_new(key->name, KNOT_RRTYPE_TSIG,
	                                        KNOT_CLASS_ANY, 0, NULL);
	if (!tmp_tsig) {
		return KNOT_ENOMEM;
	}

	/* Create rdata for TSIG RR. */
	const uint8_t *alg_name = dnssec_tsig_algorithm_to_dname(key->algorithm);
	size_t alg_size = dnssec_tsig_algorithm_size(key->algorithm);
	knot_tsig_create_rdata(tmp_tsig, alg_name, alg_size, 0);
	knot_tsig_rdata_set_time_signed(tmp_tsig, time(NULL));
	knot_tsig_rdata_set_fudge(tmp_tsig, KNOT_TSIG_FUDGE_DEFAULT);

	/* Create wire to be signed. */
	size_t wire_len = prev_digest_len + to_sign_len + KNOT_TSIG_TIMERS_LENGTH + 2;
	uint8_t *wire = malloc(wire_len);
	if (!wire) {
		knot_rrset_free(tmp_tsig, NULL);
		return KNOT_ENOMEM;
	}
	memset(wire, 0, wire_len);

	/* Write previous digest length. */
	knot_wire_write_u16(wire, prev_digest_len);
	/* Write previous digest. */
	memcpy(wire + 2, prev_digest, prev_digest_len);
	/* Write original message. */
	memcpy(wire + prev_digest_len + 2, to_sign, to_sign_len);
	/* Write timers. */
	wire_write_timers(wire + prev_digest_len + to_sign_len + 2, tmp_tsig);

	int ret = compute_digest(wire, wire_len, digest_tmp, &digest_tmp_len, key);
	free(wire);
	if (ret != KNOT_EOK) {
		knot_rrset_free(tmp_tsig, NULL);
		*digest_len = 0;
		return ret;
	}

	if (digest_tmp_len > *digest_len) {
		knot_rrset_free(tmp_tsig, NULL);
		*digest_len = 0;
		return KNOT_ESPACE;
	}

	/* Set the MAC. */
	knot_tsig_rdata_set_mac(tmp_tsig, digest_tmp_len, digest_tmp);

	/* Set original id. */
	knot_tsig_rdata_set_orig_id(tmp_tsig, knot_wire_get_id(msg));

	/* Set other data. */
	knot_tsig_rdata_set_other_data(tmp_tsig, 0, NULL);

	ret = knot_rrset_to_wire(tmp_tsig, msg + *msg_len,
	                         msg_max_len - *msg_len, NULL);
	if (ret < 0) {
		knot_rrset_free(tmp_tsig, NULL);
		*digest_len = 0;
		return ret;
	}

	size_t tsig_wire_size = ret;

	knot_rrset_free(tmp_tsig, NULL);

	*msg_len += tsig_wire_size;
	uint16_t arcount = knot_wire_get_arcount(msg);
	knot_wire_set_arcount(msg, ++arcount);

	memcpy(digest, digest_tmp, digest_tmp_len);
	*digest_len = digest_tmp_len;

	return KNOT_EOK;
}

static int check_digest(const knot_rrset_t *tsig_rr,
                        const uint8_t *wire, size_t size,
                        const uint8_t *request_mac, size_t request_mac_len,
                        const knot_tsig_key_t *tsig_key,
                        uint64_t prev_time_signed, int use_times)
{
	if (!wire || !tsig_key) {
		return KNOT_EINVAL;
	}

	/* No TSIG record means verification failure. */
	if (tsig_rr == NULL) {
		return KNOT_TSIG_EBADKEY;
	}

	/* Check that libknot knows the algorithm. */
	int ret = check_algorithm(tsig_rr);
	if (ret != KNOT_EOK) {
		return ret;
	}

	/* Check that key is valid, ie. the same as given in args. */
	ret = check_key(tsig_rr, tsig_key);
	if (ret != KNOT_EOK) {
		return ret;
	}

	uint8_t *wire_to_sign = malloc(size);
	if (!wire_to_sign) {
		return KNOT_ENOMEM;
	}

	memcpy(wire_to_sign, wire, size);

	// restore message ID to which the signature had been created with
	knot_wire_set_id(wire_to_sign, knot_tsig_rdata_orig_id(tsig_rr));

	uint8_t digest_tmp[KNOT_TSIG_MAX_DIGEST_SIZE];
	size_t digest_tmp_len = 0;
	assert(tsig_rr->rrs.count > 0);

	if (use_times) {
		/* Wire is not a single packet, TSIG RRs must be stripped already. */
		ret = create_sign_wire_next(wire_to_sign, size,
		                            request_mac, request_mac_len,
		                            digest_tmp, &digest_tmp_len,
		                            tsig_rr, tsig_key);
	} else {
		ret = create_sign_wire(wire_to_sign, size,
		                       request_mac, request_mac_len,
		                       digest_tmp, &digest_tmp_len,
		                       tsig_rr, tsig_key);
	}

	assert(tsig_rr->rrs.count > 0);
	free(wire_to_sign);

	if (ret != KNOT_EOK) {
		return ret;
	}

	/* Compare MAC from TSIG RR RDATA with just computed digest. */

	/*!< \todo move to function. */
	const knot_dname_t *alg_name = knot_tsig_rdata_alg_name(tsig_rr);
	dnssec_tsig_algorithm_t alg = dnssec_tsig_algorithm_from_dname(alg_name);

	/*! \todo [TSIG] TRUNCATION */
	uint16_t mac_length = knot_tsig_rdata_mac_length(tsig_rr);
	const uint8_t *tsig_mac = knot_tsig_rdata_mac(tsig_rr);

	if (mac_length != dnssec_tsig_algorithm_size(alg)) {
		return KNOT_TSIG_EBADSIG;
	}

	if (const_time_memcmp(tsig_mac, digest_tmp, mac_length) != 0) {
		return KNOT_TSIG_EBADSIG;
	}

	/* Check TSIG validity period, must be after the signature check! */
	ret = check_time_signed(tsig_rr, prev_time_signed);
	if (ret != KNOT_EOK) {
		return ret;
	}

	return KNOT_EOK;
}

_public_
int knot_tsig_server_check(const knot_rrset_t *tsig_rr,
                           const uint8_t *wire, size_t size,
                           const knot_tsig_key_t *tsig_key)
{
	return check_digest(tsig_rr, wire, size, NULL, 0, tsig_key, 0, 0);
}

_public_
int knot_tsig_client_check(const knot_rrset_t *tsig_rr,
                           const uint8_t *wire, size_t size,
                           const uint8_t *request_mac, size_t request_mac_len,
                           const knot_tsig_key_t *tsig_key,
                           uint64_t prev_time_signed)
{
	return check_digest(tsig_rr, wire, size, request_mac, request_mac_len,
	                    tsig_key, prev_time_signed, 0);
}

_public_
int knot_tsig_client_check_next(const knot_rrset_t *tsig_rr,
                                const uint8_t *wire, size_t size,
                                const uint8_t *prev_digest,
                                size_t prev_digest_len,
                                const knot_tsig_key_t *tsig_key,
                                uint64_t prev_time_signed)
{
	return check_digest(tsig_rr, wire, size, prev_digest,
	                    prev_digest_len, tsig_key, prev_time_signed, 1);
}

_public_
int knot_tsig_add(uint8_t *msg, size_t *msg_len, size_t msg_max_len,
                  uint16_t tsig_rcode, const knot_rrset_t *tsig_rr)
{
	/*! \todo Revise!! */

	if (!msg || !msg_len || !tsig_rr) {
		return KNOT_EINVAL;
	}

	/*! \todo What key to use, when we do not sign? Does this even work? */
	knot_rrset_t *tmp_tsig = knot_rrset_new(tsig_rr->owner, KNOT_RRTYPE_TSIG,
		                                KNOT_CLASS_ANY, 0, NULL);
	if (!tmp_tsig) {
		return KNOT_ENOMEM;
	}

	assert(tsig_rcode != KNOT_RCODE_BADTIME);
	knot_tsig_create_rdata(tmp_tsig, knot_tsig_rdata_alg_name(tsig_rr), 0, tsig_rcode);
	knot_tsig_rdata_set_time_signed(tmp_tsig, knot_tsig_rdata_time_signed(tsig_rr));

	/* Comparing to BIND it was found out that the Fudge should always be
	 * set to the server's value.
	 */
	knot_tsig_rdata_set_fudge(tmp_tsig, KNOT_TSIG_FUDGE_DEFAULT);

	/* Set original ID */
	knot_tsig_rdata_set_orig_id(tmp_tsig, knot_wire_get_id(msg));

	/* Set other len. */
	knot_tsig_rdata_set_other_data(tmp_tsig, 0, 0);

	/* Append TSIG RR. */
	int ret = knot_tsig_append(msg, msg_len, msg_max_len, tmp_tsig);

	/* key_name already referenced in RRSet, no need to free separately. */
	knot_rrset_free(tmp_tsig, NULL);

	return ret;
}

_public_
int knot_tsig_append(uint8_t *msg, size_t *msg_len, size_t msg_max_len,
                     const knot_rrset_t *tsig_rr)
{
	/* Write RRSet to wire */
	int ret = knot_rrset_to_wire(tsig_rr, msg + *msg_len,
	                             msg_max_len - *msg_len, NULL);
	if (ret < 0) {
		return ret;
	}

	*msg_len += ret;

	knot_wire_set_arcount(msg, knot_wire_get_arcount(msg) + 1);

	return KNOT_EOK;
}
