/*  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 <gnutls/gnutls.h>
#include <gnutls/crypto.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "libdnssec/shared/dname.h"
#include "libdnssec/error.h"
#include "libdnssec/shared/shared.h"
#include "libdnssec/tsig.h"

struct dnssec_tsig_ctx {
	gnutls_mac_algorithm_t algorithm;
	gnutls_hmac_hd_t hash;
};

/*!
 * TSIG algorithm identifiers.
 */
typedef struct {
	dnssec_tsig_algorithm_t id;
	gnutls_mac_algorithm_t gnutls_id;
	const char *name;
	const char *dname;
} algorithm_id_t;

/*!
 * DNAME to algorithm conversion table.
 */
static const algorithm_id_t ALGORITHM_ID_TABLE[] = {
	// RFC 4635
	{ DNSSEC_TSIG_HMAC_SHA1,   GNUTLS_MAC_SHA1,   "hmac-sha1",   "\x9hmac-sha1"   },
	{ DNSSEC_TSIG_HMAC_SHA224, GNUTLS_MAC_SHA224, "hmac-sha224", "\xbhmac-sha224" },
	{ DNSSEC_TSIG_HMAC_SHA256, GNUTLS_MAC_SHA256, "hmac-sha256", "\xbhmac-sha256" },
	{ DNSSEC_TSIG_HMAC_SHA384, GNUTLS_MAC_SHA384, "hmac-sha384", "\xbhmac-sha384" },
	{ DNSSEC_TSIG_HMAC_SHA512, GNUTLS_MAC_SHA512, "hmac-sha512", "\xbhmac-sha512" },
	// RFC 2845
	{ DNSSEC_TSIG_HMAC_MD5, GNUTLS_MAC_MD5, "hmac-md5", "\x8hmac-md5\x7sig-alg\x3reg\x3int" },
	{ 0 }
};

/*!
 * Algorithm match callback prototype.
 */
typedef bool (*algorithm_match_cb)(const algorithm_id_t *m, const void *data);

/*!
 * Lookup an algorithm in the algorithm table.
 */
static const algorithm_id_t *lookup_algorithm(algorithm_match_cb match,
					      const void *data)
{
	assert(match);

	for (const algorithm_id_t *a = ALGORITHM_ID_TABLE; a->id; a++) {
		if (match(a, data)) {
			return a;
		}
	}

	return NULL;
}

static bool match_dname(const algorithm_id_t *algorithm, const void *data)
{
	const uint8_t *search = data;
	return dname_equal(search, (uint8_t *)algorithm->dname);
}

static bool match_name(const algorithm_id_t *algorithm, const void *data)
{
	const char *search = data;
	return strcasecmp(search, algorithm->name) == 0;
}

static bool match_id(const algorithm_id_t *algorithm, const void *data)
{
	dnssec_tsig_algorithm_t search = *((dnssec_tsig_algorithm_t *)data);
	return algorithm->id == search;
}

/*!
 * Convert TSIG algorithm identifier to GnuTLS identifier.
 */
static gnutls_mac_algorithm_t algorithm_to_gnutls(dnssec_tsig_algorithm_t algorithm)
{
	const algorithm_id_t *found = lookup_algorithm(match_id, &algorithm);
	return (found ? found->gnutls_id : GNUTLS_MAC_UNKNOWN);
}

/* -- public API ----------------------------------------------------------- */

_public_
dnssec_tsig_algorithm_t dnssec_tsig_algorithm_from_dname(const uint8_t *dname)
{
	if (!dname) {
		return DNSSEC_TSIG_UNKNOWN;
	}

	const algorithm_id_t *found = lookup_algorithm(match_dname, dname);
	return (found ? found->id : DNSSEC_TSIG_UNKNOWN);
}

_public_
const uint8_t *dnssec_tsig_algorithm_to_dname(dnssec_tsig_algorithm_t algorithm)
{
	const algorithm_id_t *found = lookup_algorithm(match_id, &algorithm);
	return (found ? (uint8_t *)found->dname : NULL);
}

_public_
dnssec_tsig_algorithm_t dnssec_tsig_algorithm_from_name(const char *name)
{
	if (!name) {
		return DNSSEC_TSIG_UNKNOWN;
	}

	const algorithm_id_t *found = lookup_algorithm(match_name, name);
	return (found ? found->id : DNSSEC_TSIG_UNKNOWN);
}

_public_
const char *dnssec_tsig_algorithm_to_name(dnssec_tsig_algorithm_t algorithm)
{
	const algorithm_id_t *found = lookup_algorithm(match_id, &algorithm);
	return (found ? found->name : NULL);
}

_public_
int dnssec_tsig_optimal_key_size(dnssec_tsig_algorithm_t tsig)
{
	gnutls_mac_algorithm_t mac = algorithm_to_gnutls(tsig);
	if (mac == GNUTLS_MAC_UNKNOWN) {
		return 0;
	}

	return gnutls_mac_get_key_size(mac) * CHAR_BIT;
}

_public_
int dnssec_tsig_new(dnssec_tsig_ctx_t **ctx_ptr,
                    dnssec_tsig_algorithm_t algorithm,
		    const dnssec_binary_t *key)
{
	if (!ctx_ptr || !key) {
		return DNSSEC_EINVAL;
	}

	dnssec_tsig_ctx_t *ctx = calloc(1, sizeof(*ctx));
	if (!ctx) {
		return DNSSEC_ENOMEM;
	}

	ctx->algorithm = algorithm_to_gnutls(algorithm);
	if (ctx->algorithm == GNUTLS_MAC_UNKNOWN) {
		free(ctx);
		return DNSSEC_INVALID_KEY_ALGORITHM;
	}

	int result = gnutls_hmac_init(&ctx->hash, ctx->algorithm, key->data, key->size);
	if (result != 0) {
		free(ctx);
		return DNSSEC_SIGN_INIT_ERROR;
	}

	*ctx_ptr = ctx;

	return DNSSEC_EOK;
}

_public_
void dnssec_tsig_free(dnssec_tsig_ctx_t *ctx)
{
	if (!ctx) {
		return;
	}

	gnutls_hmac_deinit(ctx->hash, NULL);
	free(ctx);
}

_public_
int dnssec_tsig_add(dnssec_tsig_ctx_t *ctx, const dnssec_binary_t *data)
{
	if (!ctx || !data) {
		return DNSSEC_EINVAL;
	}

	int result = gnutls_hmac(ctx->hash, data->data, data->size);
	if (result != 0) {
		return DNSSEC_SIGN_ERROR;
	}

	return DNSSEC_EOK;
}

_public_
size_t dnssec_tsig_size(dnssec_tsig_ctx_t *ctx)
{
	if (!ctx) {
		return 0;
	}

	return gnutls_hmac_get_len(ctx->algorithm);
}

_public_
size_t dnssec_tsig_algorithm_size(dnssec_tsig_algorithm_t algorithm)
{
	int gnutls_algorithm = algorithm_to_gnutls(algorithm);
	return gnutls_hmac_get_len(gnutls_algorithm);
}

_public_
int dnssec_tsig_write(dnssec_tsig_ctx_t *ctx, uint8_t *mac)
{
	if (!ctx || !mac) {
		return DNSSEC_EINVAL;
	}

	gnutls_hmac_output(ctx->hash, mac);

	return DNSSEC_EOK;
}
