/*-
 * Copyright 2016 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "config.h"
#include "rspamadm.h"
#include "printf.h"
#include "str_util.h"
#include "libcryptobox/cryptobox.h"
#include "contrib/libottery/ottery.h"
#include "lua/lua_common.h"
#include "unix-std.h"

#include <openssl/rsa.h>
#include <openssl/bn.h>
#include <openssl/pem.h>

static gchar *privkey_file = NULL;
static gchar *selector = NULL;
static gchar *domain = NULL;
static guint bits = 1024;
static gchar *type = "rsa";

static void rspamadm_dkim_keygen (gint argc, gchar **argv,
								  const struct rspamadm_command *cmd);
static const char *rspamadm_dkim_keygen_help (gboolean full_help,
											  const struct rspamadm_command *cmd);
static void rspamadm_dkim_keygen_lua_subrs (gpointer pL);

struct rspamadm_command dkim_keygen_command = {
		.name = "dkim_keygen",
		.flags = 0,
		.help = rspamadm_dkim_keygen_help,
		.run = rspamadm_dkim_keygen,
		.lua_subrs = rspamadm_dkim_keygen_lua_subrs,
};

static GOptionEntry entries[] = {
		{"domain",  'd', 0, G_OPTION_ARG_STRING, &domain,
				"Use the specified domain", NULL},
		{"selector",  's', 0, G_OPTION_ARG_STRING, &selector,
				"Use the specified selector", NULL},
		{"privkey",  'k', 0, G_OPTION_ARG_STRING, &privkey_file,
				"Save private key in the specified file", NULL},
		{"bits",  'b', 0, G_OPTION_ARG_INT, &bits,
				"Set key length to N bits (1024 by default)", NULL},
		{"type",  't', 0, G_OPTION_ARG_STRING, &type,
				"Key type: rsa or ed25519 (rsa by default)", NULL},
		{NULL,       0,   0, G_OPTION_ARG_NONE, NULL, NULL, NULL}
};

static const char *
rspamadm_dkim_keygen_help (gboolean full_help, const struct rspamadm_command *cmd)
{
	const char *help_str;

	if (full_help) {
		help_str = "Create key pairs for dkim signing\n\n"
				"Usage: rspamadm dkim_keygen -s selector -d domain [-k privkey] [-b bits]\n"
				"Where options are:\n\n"
				"-d: use the specified domain\n"
				"-s: use the specified selector\n"
				"-k: save private key to file instead of printing it to stdout\n"
				"-b: set number of bits instead of 1024\n"
				"--help: shows available options and commands";
	}
	else {
		help_str = "Create dkim key pairs";
	}

	return help_str;
}

static void
rspamd_dkim_generate_rsa_keypair (const gchar *domain, const gchar *selector,
								  const gchar *priv_fname, const gchar *pub_fname,
								  guint keylen)
{
	BIGNUM *e;
	RSA *r;
	BIO *pubout, *privout;
	EVP_PKEY *pk;
	gint rc;
	glong publen;
	gsize b64_len;
	gchar *pubdata, *b64_data;
	FILE *pubfile = NULL;

	if (bits > 4096 || bits < 512) {
		fprintf (stderr, "Bits number must be in the interval 512...4096\n");
		exit (EXIT_FAILURE);
	}

	e = BN_new ();
	r = RSA_new ();
	pk = EVP_PKEY_new ();
	g_assert (BN_set_word (e, RSA_F4) == 1);
	g_assert (RSA_generate_key_ex (r, bits, e, NULL) == 1);
	g_assert (EVP_PKEY_set1_RSA (pk, r) == 1);

	if (priv_fname) {
		int fd = open (priv_fname, O_WRONLY | O_CREAT | O_TRUNC, 0640);

		if (fd < 0) {
			rspamd_fprintf (stderr, "cannot open output file %s: %s\n",
					priv_fname, strerror (errno));
			exit (EXIT_FAILURE);
		}

		FILE *fp = fdopen (fd, "w");

		if (fp == NULL) {
			close (fd);
			rspamd_fprintf (stderr, "cannot open output file %s: %s\n",
					priv_fname, strerror (errno));
			exit (EXIT_FAILURE);
		}

		privout = BIO_new_fp (fp, BIO_CLOSE);

		if (privout == NULL) {
			fclose (fp);
			rspamd_fprintf (stderr, "cannot open output file %s: %s\n",
					priv_fname, strerror (errno));
			exit (EXIT_FAILURE);
		}
	} else {
		privout = BIO_new_fp (stdout, BIO_NOCLOSE);
	}

	rc = PEM_write_bio_PrivateKey (privout, pk, NULL, NULL, 0, NULL, NULL);

	if (rc != 1) {
		rspamd_fprintf (stderr, "cannot write key to the output file %s: %s\n",
				priv_fname ? priv_fname : "stdout", strerror (errno));
		exit (EXIT_FAILURE);
	}

	BIO_free (privout);
	fflush (stdout);

	pubout = BIO_new (BIO_s_mem ());

	rc = i2d_RSA_PUBKEY_bio (pubout, r);
	publen = BIO_get_mem_data (pubout, &pubdata);

	g_assert (publen > 0);
	b64_data = rspamd_encode_base64 (pubdata, publen, -1, &b64_len);

	if (pub_fname) {
		pubfile = fopen (pub_fname, "w");

		if (pubfile == NULL) {
			rspamd_fprintf (stderr, "cannot open output file %s: %s\n",
					pub_fname, strerror (errno));
			exit (EXIT_FAILURE);
		}
	} else {
		pubfile = stdout;
	}

	if (b64_len < 255 - 2) {
		rspamd_fprintf (pubfile, "%s._domainkey IN TXT ( \"v=DKIM1; k=rsa; \"\n"
								 "\t\"p=%s\" ) ;\n",
				selector ? selector : "selector",
				b64_data);
	} else {
		guint i;
		gint step = 253, remain = b64_len;

		rspamd_fprintf (pubfile, "%s._domainkey IN TXT ( \"v=DKIM1; k=rsa; \"\n",
				selector ? selector : "selector");

		for (i = 0; i < b64_len; i += step, remain -= step) {
			if (i == 0) {
				rspamd_fprintf (pubfile, "\t\"p=%*s\"\n", MIN(step, remain), &b64_data[i]);
			} else {
				step = 255;
				rspamd_fprintf (pubfile, "\t\"%*s\"\n", MIN(step, remain), &b64_data[i]);
			}
		}

		rspamd_fprintf (pubfile, ") ; \n");
	}

	if (pubfile != stdout) {
		fclose (pubfile);
	}

	g_free (b64_data);
	BIO_free (pubout);
	EVP_PKEY_free (pk);
	RSA_free (r);
	BN_free (e);
}

static void
rspamd_dkim_generate_ed25519_keypair (const gchar *domain, const gchar *selector,
								  const gchar *priv_fname, const gchar *pub_fname,
								  guint keylen, gboolean seeded)
{
	rspamd_sig_sk_t ed_sk;
	rspamd_sig_pk_t ed_pk;
	gchar *base64_pk, *base64_sk;
	FILE *pubfile = NULL, *privfile = NULL;

	rspamd_cryptobox_keypair_sig (ed_pk, ed_sk, RSPAMD_CRYPTOBOX_MODE_25519);
	if (seeded) {
		/* Just encode seed, not the full sk */
		base64_sk = rspamd_encode_base64_common (ed_sk, 32, 0, NULL, FALSE,
				RSPAMD_TASK_NEWLINES_LF);
	}
	else {
		base64_sk = rspamd_encode_base64_common (ed_sk,
				rspamd_cryptobox_sk_sig_bytes (RSPAMD_CRYPTOBOX_MODE_25519),
				0, NULL, FALSE,
				RSPAMD_TASK_NEWLINES_LF);
	}
	base64_pk = rspamd_encode_base64_common (ed_pk, sizeof (ed_pk), 0, NULL, FALSE,
			RSPAMD_TASK_NEWLINES_LF);

	/* Cleanup sensitive data */
	rspamd_explicit_memzero (ed_sk, sizeof (ed_sk));

	if (priv_fname) {
		privfile = fopen (priv_fname, "w");

		if (privfile == NULL) {
			rspamd_fprintf (stderr, "cannot open output file %s: %s\n",
					priv_fname, strerror (errno));
			rspamd_explicit_memzero (base64_sk, strlen (base64_sk));
			g_free (base64_sk);
			g_free (base64_pk);
			exit (EXIT_FAILURE);
		}
	}
	else {
		privfile = stdout;
	}

	if (rspamd_fprintf (privfile, "%s\n", base64_sk) == -1) {
		rspamd_fprintf (stderr, "cannot write to output file %s: %s\n",
				priv_fname, strerror (errno));
		rspamd_explicit_memzero (base64_sk, strlen (base64_sk));
		g_free (base64_sk);
		g_free (base64_pk);

		if (privfile != stdout) {
			fclose (privfile);
		}

		exit (EXIT_FAILURE);
	}

	if (privfile != stdout) {
		fclose (privfile);
	}

	if (pub_fname) {
		pubfile = fopen (pub_fname, "w");

		if (pubfile == NULL) {
			rspamd_fprintf (stderr, "cannot open output file %s: %s\n",
					pub_fname, strerror (errno));
			rspamd_explicit_memzero (base64_sk, strlen (base64_sk));
			g_free (base64_sk);
			g_free (base64_pk);
			exit (EXIT_FAILURE);
		}
	}
	else {
		pubfile = stdout;
	}

	rspamd_fprintf (pubfile, "%s._domainkey IN TXT ( \"v=DKIM1; k=ed25519; \"\n"
							 "\t\"p=%s\" ) ;\n",
			selector ? selector : "selector",
			base64_pk);

	if (pubfile != stdout) {
		fclose (pubfile);
	}

	rspamd_explicit_memzero (base64_sk, strlen (base64_sk));
	g_free (base64_sk);
	g_free (base64_pk);
}

static void
rspamadm_dkim_generate_keypair (const gchar *domain, const gchar *selector,
		const gchar *priv_fname, const gchar *pub_fname, guint keylen)
{
	if (strcmp (type, "rsa") == 0) {
		rspamd_dkim_generate_rsa_keypair (domain, selector, priv_fname,
				pub_fname, keylen);
	}
	else if (strcmp (type, "ed25519") == 0) {
		rspamd_dkim_generate_ed25519_keypair (domain, selector, priv_fname,
				pub_fname, keylen, FALSE);
	}
	else if (strcmp (type, "ed25519-seed") == 0) {
		rspamd_dkim_generate_ed25519_keypair (domain, selector, priv_fname,
				pub_fname, keylen, TRUE);
	}
	else {
		fprintf (stderr, "invalid key type: %s\n", type);
		exit (EXIT_FAILURE);
	}
}

static gint
rspamadm_dkim_keygen_lua_generate (lua_State *L)
{
	const gchar *domain = luaL_checkstring (L, 1);
	const gchar *selector = luaL_checkstring (L, 2);
	const gchar *privfile = NULL, *pubfile = NULL;
	guint key_bits = 1024;

	if (domain == NULL || selector == NULL) {
		return luaL_error (L, "invalid arguments");
	}

	if (lua_type (L, 3) == LUA_TSTRING) {
		privfile = lua_tostring (L, 3);
	}

	if (lua_type (L, 4) == LUA_TSTRING) {
		pubfile = lua_tostring (L, 4);
	}

	if (lua_type (L, 5) == LUA_TNUMBER) {
		key_bits = lua_tonumber (L, 5);
	}

	rspamadm_dkim_generate_keypair (domain, selector, privfile, pubfile, key_bits);

	return 0;
}

static void
rspamadm_dkim_keygen_lua_subrs (gpointer pL)
{
	lua_State *L = pL;

	lua_pushstring (L, "dkim_keygen");
	lua_pushcfunction (L, rspamadm_dkim_keygen_lua_generate);
	lua_settable (L, -3);
}

static void
rspamadm_dkim_keygen (gint argc, gchar **argv, const struct rspamadm_command *cmd)
{
	GOptionContext *context;
	GError *error = NULL;

	context = g_option_context_new (
			"dkim_keygen - create dkim keys");
	g_option_context_set_summary (context,
			"Summary:\n  Rspamd administration utility version "
					RVERSION
					"\n  Release id: "
					RID);
	g_option_context_add_main_entries (context, entries, NULL);

	if (!g_option_context_parse (context, &argc, &argv, &error)) {
		fprintf (stderr, "option parsing failed: %s\n", error->message);
		g_error_free (error);
		g_option_context_free (context);
		exit (EXIT_FAILURE);
	}

	g_option_context_free (context);
	rspamadm_dkim_generate_keypair (domain, selector, privkey_file, NULL, bits);
}
