/*
 * gcr
 *
 * Copyright (C) 2023 Daiki Ueno
 *
 * This program 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 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
 * Lesser General Public License for more details.
 *
 * You should have received copies of the GNU General Public License and
 * the GNU Lesser General Public License along with this program.  If
 * not, see http://www.gnu.org/licenses/.
 *
 * Author: Daiki Ueno
 */

#include "config.h"

#include "egg-dh.h"

/* Enabling this is a complete security compromise */
#define DEBUG_DH_SECRET 0

#include <gnutls/gnutls.h>
#include <gnutls/abstract.h>

struct egg_dh_params {
	gnutls_dh_params_t inner;
	guint bits;
};

struct egg_dh_pubkey {
	gnutls_pubkey_t inner;
};

struct egg_dh_privkey {
	gnutls_privkey_t inner;
};

egg_dh_params *
egg_dh_default_params (const gchar *name)
{
	const egg_dh_group *group;
	egg_dh_params *params;

	g_return_val_if_fail (name, NULL);

	for (group = egg_dh_groups; group->name; ++group) {
		if (g_str_equal (group->name, name)) {
			gnutls_dh_params_t inner;
			gnutls_datum_t prime, generator;
			int ret;

			ret = gnutls_dh_params_init (&inner);
			if (ret < 0)
				return NULL;

			prime.data = (void *)group->prime;
			prime.size = group->n_prime;
			generator.data = (void *)group->base;
			generator.size = group->n_base;

			ret = gnutls_dh_params_import_raw (inner,
							   &prime,
							   &generator);
			if (ret < 0) {
				gnutls_dh_params_deinit (inner);
				return NULL;
			}

			params = g_new (struct egg_dh_params, 1);
			if (!params) {
				gnutls_dh_params_deinit (inner);
				return NULL;
			}
			params->inner = g_steal_pointer (&inner);
			params->bits = group->bits;
			return params;
		}
	}

	return NULL;
}

gboolean
egg_dh_gen_pair (egg_dh_params *params, guint bits,
                 egg_dh_pubkey **pub, egg_dh_privkey **priv)
{
	gnutls_pubkey_t pub_inner = NULL;
	gnutls_privkey_t priv_inner = NULL;
	egg_dh_pubkey *pub_outer = NULL;
	egg_dh_privkey *priv_outer = NULL;
	gnutls_keygen_data_st data;
	gboolean ok = FALSE;
	int ret;

	g_return_val_if_fail (params, FALSE);
	g_return_val_if_fail (pub, FALSE);
	g_return_val_if_fail (priv, FALSE);

	if (bits == 0) {
		bits = params->bits;
	} else if (bits > params->bits) {
		g_return_val_if_reached (FALSE);
	}

	ret = gnutls_privkey_init (&priv_inner);
	if (ret < 0)
		goto out;

	data.type = GNUTLS_KEYGEN_DH;
	data.data = (void *)params->inner;

	ret = gnutls_privkey_generate2 (priv_inner, GNUTLS_PK_DH, bits, 0,
					&data, 1);
	if (ret < 0)
		goto out;

	ret = gnutls_pubkey_init (&pub_inner);
	if (ret < 0)
		goto out;

	ret = gnutls_pubkey_import_privkey (pub_inner, priv_inner, 0, 0);
	if (ret < 0)
		goto out;

	pub_outer = g_new0 (struct egg_dh_pubkey, 1);
	if (!pub_outer)
		goto out;
	pub_outer->inner = g_steal_pointer (&pub_inner);

	priv_outer = g_new0 (struct egg_dh_privkey, 1);
	if (!priv_outer)
		goto out;
	priv_outer->inner = g_steal_pointer (&priv_inner);

	*pub = g_steal_pointer (&pub_outer);
	*priv = g_steal_pointer (&priv_outer);

	ok = TRUE;

 out:
	if (priv_inner)
		gnutls_privkey_deinit (priv_inner);
	if (pub_inner)
		gnutls_pubkey_deinit (pub_inner);

	egg_dh_privkey_free (priv_outer);
	egg_dh_pubkey_free (pub_outer);

	return ok;
}

GBytes *
egg_dh_gen_secret (egg_dh_pubkey *peer, egg_dh_privkey *priv,
                   egg_dh_params *params)
{
	int ret;
	gnutls_datum_t k;
#if DEBUG_DH_SECRET
	gnutls_datum_t h;
#endif

	g_return_val_if_fail (peer, NULL);
	g_return_val_if_fail (priv, NULL);
	g_return_val_if_fail (params, NULL);

	ret = gnutls_privkey_derive_secret (priv->inner, peer->inner,
					    NULL, &k, 0);
	if (ret < 0)
		return NULL;

#if DEBUG_DH_SECRET
	ret = gnutls_hex_encode2 (&k, &h);
	g_assert (ret >= 0);
	g_printerr ("DH SECRET: %s\n", h.data);
	gnutls_free (h.data);
#endif

	return g_bytes_new_with_free_func (k.data, k.size,
					   (GDestroyNotify)gnutls_free,
					   k.data);
}

void
egg_dh_params_free (egg_dh_params *params)
{
	gnutls_dh_params_deinit (params->inner);
	g_free (params);
}

void
egg_dh_pubkey_free (egg_dh_pubkey *pubkey)
{
	if (!pubkey)
		return;
	if (pubkey->inner)
		gnutls_pubkey_deinit (pubkey->inner);
	g_free (pubkey);
}

void
egg_dh_privkey_free (egg_dh_privkey *privkey)
{
	if (!privkey)
		return;
	if (privkey->inner)
		gnutls_privkey_deinit (privkey->inner);
	g_free (privkey);
}

GBytes *
egg_dh_pubkey_export (const egg_dh_pubkey *pubkey)
{
	gnutls_datum_t data;
	int ret;

	ret = gnutls_pubkey_export_dh_raw (pubkey->inner, NULL, &data, 0);
	if (ret < 0)
		return NULL;

	return g_bytes_new_with_free_func (data.data, data.size,
					   (GDestroyNotify)gnutls_free,
					   data.data);
}

egg_dh_pubkey *
egg_dh_pubkey_new_from_bytes (const egg_dh_params *params, GBytes *bytes)
{
	egg_dh_pubkey *pub;
	gnutls_pubkey_t inner;
	gnutls_datum_t data;
	int ret;

	ret = gnutls_pubkey_init (&inner);
	if (ret < 0)
		return NULL;

	data.data = (void *)g_bytes_get_data (bytes, NULL);
	data.size = g_bytes_get_size (bytes);

	ret = gnutls_pubkey_import_dh_raw (inner, params->inner, &data);
	if (ret < 0) {
		gnutls_pubkey_deinit (inner);
		return NULL;
	}

	pub = g_new (struct egg_dh_pubkey, 1);
	if (!pub) {
		gnutls_pubkey_deinit (inner);
		return NULL;
	}

	pub->inner = g_steal_pointer (&inner);
	return pub;
}
