/*
 * c_pkeycipher.c - asymmetric public-key encryption class implementation
 *
 * Copyright (C) 2026 Alessandro Rinaldi
 * Copyright (C) 2013-2019 Tobias Boege <tobias@gambas-buch.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL. If you modify
 * file(s) with this exception, you may extend this exception to
 * your version of the file(s), but you are not obligated to do so.
 * If you do not wish to do so, delete this exception statement
 * from your version. If you delete this exception statement from
 * all source files in the program, then also delete it here.
 */

#define __C_PKEYCIPHER_C

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/rsa.h>

#include "main.h"
#include "c_pkeycipher.h"
#include "c_pkey.h"

/* CPKEY struct is shared via c_pkey.h. Access pkey->pkey directly
 * here since c_pkeycipher.c needs the EVP_PKEY * for encrypt/decrypt ops. */

struct cipher_algo {
	const char *name;
	int         key_type;
	int         padding;
	const char *oaep_md_name; /* NULL for non-OAEP paddings */
};

static const struct cipher_algo CIPHER_ALGOS[] = {
	{ "rsa-pkcs1",       EVP_PKEY_RSA, RSA_PKCS1_PADDING,      NULL     },
	{ "rsa-oaep-sha1",   EVP_PKEY_RSA, RSA_PKCS1_OAEP_PADDING, "sha1"   },
	{ "rsa-oaep-sha256", EVP_PKEY_RSA, RSA_PKCS1_OAEP_PADDING, "sha256" },
	{ NULL, 0, 0, NULL }
};

static const struct cipher_algo *cipher_lookup(const char *name)
{
	int i;
	for (i = 0; CIPHER_ALGOS[i].name; i++) {
		if (!strcasecmp(CIPHER_ALGOS[i].name, name))
			return &CIPHER_ALGOS[i];
	}
	return NULL;
}

/* Per-virtual-object selected algorithm — set by PKeyCipher._get */
static const struct cipher_algo *_algo;

/**G
 * Return a list of all asymmetric cipher methods supported by this build.
 * Currently RSA only: rsa-pkcs1, rsa-oaep-sha1, rsa-oaep-sha256.
 **/
BEGIN_PROPERTY(PKeyCipher_List)

	GB_ARRAY arr;
	int i;
	char **slot;

	GB.Array.New(&arr, GB_T_STRING, 0);
	for (i = 0; CIPHER_ALGOS[i].name; i++) {
		slot = (char **) GB.Array.Add(arr);
		*slot = GB.NewZeroString(CIPHER_ALGOS[i].name);
	}
	GB.ReturnObject(arr);

END_PROPERTY

/**G
 * Check whether the named asymmetric cipher method is supported.
 **/
BEGIN_METHOD(PKeyCipher_IsSupported, GB_STRING method)

	GB.ReturnBoolean(cipher_lookup(GB.ToZeroString(ARG(method))) != NULL);

END_METHOD

/**G
 * Return a virtual object representing an asymmetric cipher method by
 * giving its name. Valid names can be looked up from PKeyCipher.List.
 **/
BEGIN_METHOD(PKeyCipher_get, GB_STRING method)

	const struct cipher_algo *a = cipher_lookup(GB.ToZeroString(ARG(method)));
	if (!a) {
		GB.Error("Unknown cipher method");
		return;
	}
	_algo = a;
	RETURN_SELF();

END_METHOD

/* Configure padding (and OAEP digest, if applicable) on a freshly
 * initialised EVP_PKEY_CTX. Returns 1 on success, 0 on failure. */
static int configure_padding(EVP_PKEY_CTX *ctx, const struct cipher_algo *algo)
{
	const EVP_MD *md;

	if (EVP_PKEY_CTX_set_rsa_padding(ctx, algo->padding) <= 0)
		return 0;

	if (algo->oaep_md_name) {
		md = EVP_get_digestbyname(algo->oaep_md_name);
		if (!md) return 0;
		/* MGF1 hash = OAEP hash; set both explicitly to be unambiguous
		 * across OpenSSL versions. */
		if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0) return 0;
		if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) return 0;
	}
	return 1;
}

/**G
 * Encrypt the given data with the supplied key using this cipher
 * method. Returns the raw binary ciphertext.
 *
 * Raises an exception if the key is unsuitable, the plaintext exceeds
 * the modulus size minus padding overhead, or OpenSSL rejects the
 * operation.
 **/
BEGIN_METHOD(PKeyCipherMethod_Encrypt, GB_STRING data; GB_OBJECT key)

	CPKEY *key_obj = (CPKEY *) VARG(key);
	EVP_PKEY_CTX *ctx = NULL;
	unsigned char *out = NULL;
	size_t outlen = 0;

	ctx = EVP_PKEY_CTX_new(key_obj->pkey, NULL);
	if (!ctx) { MAIN_error("Cannot create PKEY context: &1"); return; }

	if (EVP_PKEY_encrypt_init(ctx) <= 0) goto err;
	if (!configure_padding(ctx, _algo)) goto err;

	if (EVP_PKEY_encrypt(ctx, NULL, &outlen,
	                     (const unsigned char *) STRING(data), LENGTH(data)) <= 0) goto err;  /* probe size */
	out = OPENSSL_malloc(outlen);
	if (!out) { EVP_PKEY_CTX_free(ctx); MAIN_error("Out of memory: &1"); return; }
	if (EVP_PKEY_encrypt(ctx, out, &outlen,
	                     (const unsigned char *) STRING(data), LENGTH(data)) <= 0) goto err;

	EVP_PKEY_CTX_free(ctx);
	GB.ReturnNewString((char *) out, outlen);
	OPENSSL_free(out);
	return;

err:
	if (out) OPENSSL_free(out);
	if (ctx) EVP_PKEY_CTX_free(ctx);
	MAIN_error("RSA encrypt failed: &1");

END_METHOD

/**G
 * Decrypt the given ciphertext with the supplied private key using
 * this cipher method. Returns the original plaintext.
 *
 * Raises an exception if the key has no private component, the
 * ciphertext does not match the key/method used to produce it, or
 * OpenSSL rejects the operation.
 **/
BEGIN_METHOD(PKeyCipherMethod_Decrypt, GB_STRING data; GB_OBJECT key)

	CPKEY *key_obj = (CPKEY *) VARG(key);
	EVP_PKEY_CTX *ctx = NULL;
	unsigned char *out = NULL;
	size_t outlen = 0;

	ctx = EVP_PKEY_CTX_new(key_obj->pkey, NULL);
	if (!ctx) { MAIN_error("Cannot create PKEY context: &1"); return; }

	if (EVP_PKEY_decrypt_init(ctx) <= 0) goto err;
	if (!configure_padding(ctx, _algo)) goto err;

	if (EVP_PKEY_decrypt(ctx, NULL, &outlen,
	                     (const unsigned char *) STRING(data), LENGTH(data)) <= 0) goto err;  /* probe size */
	out = OPENSSL_malloc(outlen);
	if (!out) { EVP_PKEY_CTX_free(ctx); MAIN_error("Out of memory: &1"); return; }
	if (EVP_PKEY_decrypt(ctx, out, &outlen,
	                     (const unsigned char *) STRING(data), LENGTH(data)) <= 0) goto err;

	EVP_PKEY_CTX_free(ctx);
	GB.ReturnNewString((char *) out, outlen);
	OPENSSL_free(out);
	return;

err:
	if (out) OPENSSL_free(out);
	if (ctx) EVP_PKEY_CTX_free(ctx);
	MAIN_error("RSA decrypt failed: &1");

END_METHOD

GB_DESC CPKeyCipher[] = {
	GB_DECLARE("PKeyCipher", 0),
	GB_NOT_CREATABLE(),

	GB_STATIC_PROPERTY_READ("List", "String[]", PKeyCipher_List),

	GB_STATIC_METHOD("_get", ".PKeyCipher.Method", PKeyCipher_get, "(Method)s"),
	GB_STATIC_METHOD("IsSupported", "b", PKeyCipher_IsSupported, "(Method)s"),

	GB_END_DECLARE
};

GB_DESC CPKeyCipherMethod[] = {
	GB_DECLARE(".PKeyCipher.Method", 0),
	GB_VIRTUAL_CLASS(),

	GB_STATIC_METHOD("Encrypt", "s", PKeyCipherMethod_Encrypt, "(Data)s(Key)PKey"),
	GB_STATIC_METHOD("Decrypt", "s", PKeyCipherMethod_Decrypt, "(Data)s(Key)PKey"),

	GB_END_DECLARE
};
