/*
 * c_pkey.c - asymmetric key 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_PKEY_C

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

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

#define THIS ((CPKEY *) _object)

/**G
 * Return the algorithm family of this key: "RSA" for RSA keys.
 * Future versions may return "EC", "ED25519", etc. for other algorithms.
 **/
BEGIN_PROPERTY(PKey_Type)
	int t = EVP_PKEY_base_id(THIS->pkey);
	const char *name;
	switch (t) {
		case EVP_PKEY_RSA:   name = "RSA"; break;
		default:             name = OBJ_nid2sn(t); break;
	}
	GB.ReturnNewZeroString(name);
END_PROPERTY

/**G
 * Return the key size in bits (e.g. 2048 for an RSA-2048 key, meaning
 * a 2048-bit modulus).
 **/
BEGIN_PROPERTY(PKey_Bits)
	GB.ReturnInteger(EVP_PKEY_bits(THIS->pkey));
END_PROPERTY

/**G
 * Return TRUE if this key contains private material (and can therefore
 * be used to Sign or Decrypt). Return FALSE if it is a public-only key
 * (which can only Verify or Encrypt).
 **/
BEGIN_PROPERTY(PKey_IsPrivate)
	/* i2d_PrivateKey returns >0 only if private material is present;
	 * we don't care about the output, just the size probe. Clear any
	 * error queue entry it may have left behind on failure. */
	int len = i2d_PrivateKey(THIS->pkey, NULL);
	if (len <= 0) ERR_clear_error();
	GB.ReturnBoolean(len > 0);
END_PROPERTY

/**G
 * Load an RSA private key from a PEM-encoded string. If the PEM is
 * encrypted, Passphrase must be supplied; otherwise it can be omitted
 * or passed as an empty string.
 *
 * Raises an exception if the PEM is malformed, if the passphrase is
 * wrong, or if no private material can be parsed from the input.
 **/
BEGIN_METHOD(PKey_LoadPrivate, GB_STRING pem; GB_STRING passphrase)

	BIO *bio = NULL;
	EVP_PKEY *pkey = NULL;
	char *pwd = NULL;
	CPKEY *res;

	bio = BIO_new_mem_buf(STRING(pem), LENGTH(pem));
	if (!bio) {
		MAIN_error("Cannot create BIO: &1");
		return;
	}
	if (!MISSING(passphrase) && LENGTH(passphrase) > 0)
		pwd = GB.ToZeroString(ARG(passphrase));

	pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, pwd);
	BIO_free(bio);
	if (!pkey) {
		MAIN_error("Cannot parse private key: &1");
		return;
	}

	res = (CPKEY *) GB.New(GB.FindClass("PKey"), NULL, NULL);
	res->pkey = pkey;
	GB.ReturnObject(res);

END_METHOD

/**G
 * Export this key as a PEM-encoded string.
 * If this is a private key and Passphrase is supplied, the PEM will be
 * encrypted with AES-256-CBC. Without passphrase, an unencrypted PEM is
 * returned. For public keys, Passphrase is ignored.
 **/
BEGIN_METHOD(PKey_ToPem, GB_STRING passphrase)

	BIO *bio = NULL;
	char *data;
	long len;
	int ok;

	bio = BIO_new(BIO_s_mem());
	if (!bio) { MAIN_error("Cannot create BIO: &1"); return; }

	/* If private, use PEM_write_bio_PrivateKey (with optional encryption);
	 * if public, use PEM_write_bio_PUBKEY. We dispatch on IsPrivate. */
	if (i2d_PrivateKey(THIS->pkey, NULL) > 0) {
		if (!MISSING(passphrase) && LENGTH(passphrase) > 0) {
			ok = PEM_write_bio_PrivateKey(bio, THIS->pkey,
			                              EVP_aes_256_cbc(),
			                              (unsigned char *) STRING(passphrase),
			                              LENGTH(passphrase),
			                              NULL, NULL);
		} else {
			ok = PEM_write_bio_PrivateKey(bio, THIS->pkey,
			                              NULL, NULL, 0, NULL, NULL);
		}
	} else {
		ERR_clear_error();        /* i2d_PrivateKey set the error queue */
		ok = PEM_write_bio_PUBKEY(bio, THIS->pkey);
	}

	if (!ok) {
		BIO_free(bio);
		MAIN_error("PEM export failed: &1");
		return;
	}

	len = BIO_get_mem_data(bio, &data);
	GB.ReturnNewString(data, len);
	BIO_free(bio);

END_METHOD

/**G
 * Load an RSA public key from a PEM-encoded string. Accepts SPKI
 * (-----BEGIN PUBLIC KEY-----) format.
 *
 * Raises an exception if the PEM is malformed or contains no public key.
 **/
BEGIN_METHOD(PKey_LoadPublic, GB_STRING pem)

	BIO *bio;
	EVP_PKEY *pkey;
	CPKEY *res;

	bio = BIO_new_mem_buf(STRING(pem), LENGTH(pem));
	if (!bio) { MAIN_error("Cannot create BIO: &1"); return; }

	pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
	BIO_free(bio);
	if (!pkey) { MAIN_error("Cannot parse public key: &1"); return; }

	res = (CPKEY *) GB.New(GB.FindClass("PKey"), NULL, NULL);
	res->pkey = pkey;
	GB.ReturnObject(res);

END_METHOD

/**G
 * Return a new PKey containing only the public portion of this key.
 * For a public-only key, returns an equivalent public key (effectively
 * a clone). The returned object is independent — freeing it does not
 * affect the original.
 **/
BEGIN_PROPERTY(PKey_PublicPart)

	BIO *bio = BIO_new(BIO_s_mem());
	EVP_PKEY *pub;
	CPKEY *res;

	if (!bio) { MAIN_error("Cannot create BIO: &1"); return; }
	if (!PEM_write_bio_PUBKEY(bio, THIS->pkey)) {
		BIO_free(bio);
		MAIN_error("Cannot extract public part: &1");
		return;
	}
	BIO_seek(bio, 0);     /* reset BIO read pointer to start */
	pub = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
	BIO_free(bio);
	if (!pub) { MAIN_error("Cannot extract public part: &1"); return; }

	res = (CPKEY *) GB.New(GB.FindClass("PKey"), NULL, NULL);
	res->pkey = pub;
	GB.ReturnObject(res);

END_PROPERTY

/**G
 * Export the public portion of this key as a PEM-encoded string
 * (SPKI / -----BEGIN PUBLIC KEY----- format). Works on both private
 * and public-only keys.
 **/
BEGIN_METHOD_VOID(PKey_ToPublicPem)

	BIO *bio = BIO_new(BIO_s_mem());
	char *data;
	long len;
	if (!bio) { MAIN_error("Cannot create BIO: &1"); return; }

	if (!PEM_write_bio_PUBKEY(bio, THIS->pkey)) {
		BIO_free(bio);
		MAIN_error("Public PEM export failed: &1");
		return;
	}
	len = BIO_get_mem_data(bio, &data);
	GB.ReturnNewString(data, len);
	BIO_free(bio);

END_METHOD

/**G
 * Load an RSA private key from a binary DER blob (PKCS#8 format).
 **/
BEGIN_METHOD(PKey_LoadPrivateDer, GB_STRING der)

	const unsigned char *p = (const unsigned char *) STRING(der);
	EVP_PKEY *pkey = d2i_AutoPrivateKey(NULL, &p, LENGTH(der));
	CPKEY *res;

	if (!pkey) { MAIN_error("Cannot parse DER private key: &1"); return; }
	res = (CPKEY *) GB.New(GB.FindClass("PKey"), NULL, NULL);
	res->pkey = pkey;
	GB.ReturnObject(res);

END_METHOD

/**G
 * Load an RSA public key from a binary DER blob (SPKI / X.509
 * SubjectPublicKeyInfo format).
 **/
BEGIN_METHOD(PKey_LoadPublicDer, GB_STRING der)

	const unsigned char *p = (const unsigned char *) STRING(der);
	EVP_PKEY *pkey = d2i_PUBKEY(NULL, &p, LENGTH(der));
	CPKEY *res;

	if (!pkey) { MAIN_error("Cannot parse DER public key: &1"); return; }
	res = (CPKEY *) GB.New(GB.FindClass("PKey"), NULL, NULL);
	res->pkey = pkey;
	GB.ReturnObject(res);

END_METHOD

/**G
 * Export this private key as a binary DER blob (PKCS#8 format).
 * Raises an exception if this is a public-only key.
 **/
BEGIN_METHOD_VOID(PKey_ToDer)

	unsigned char *buf = NULL;
	int len;

	len = i2d_PrivateKey(THIS->pkey, &buf);
	if (len <= 0) {
		ERR_clear_error();
		GB.Error("Key is not a private key");
		return;
	}
	GB.ReturnNewString((char *) buf, len);
	OPENSSL_free(buf);

END_METHOD

/**G
 * Export the public portion as a binary DER blob (SPKI / X.509
 * SubjectPublicKeyInfo format).
 **/
BEGIN_METHOD_VOID(PKey_ToPublicDer)

	unsigned char *buf = NULL;
	int len = i2d_PUBKEY(THIS->pkey, &buf);
	if (len <= 0) { MAIN_error("Public DER export failed: &1"); return; }
	GB.ReturnNewString((char *) buf, len);
	OPENSSL_free(buf);

END_METHOD

/**G
 * Generate a new RSA key pair of the given modulus size in bits.
 * Reasonable values are 2048 (default for most uses), 3072, or 4096.
 * The public exponent is fixed to 65537. Returns the new key.
 *
 * This operation is computationally expensive (seconds, sometimes more
 * for 4096 bits) and consumes entropy from the OS.
 **/
BEGIN_METHOD(PKey_Generate, GB_INTEGER bits)

	EVP_PKEY_CTX *ctx = NULL;
	EVP_PKEY *pkey = NULL;
	CPKEY *res;

	if (VARG(bits) < 512) {
		GB.Error("Invalid argument: Bits must be at least 512");
		return;
	}

	ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
	if (!ctx) {
		MAIN_error("RSA keygen failed: &1");
		return;
	}
	if (EVP_PKEY_keygen_init(ctx) <= 0) {
		EVP_PKEY_CTX_free(ctx);
		MAIN_error("RSA keygen init failed: &1");
		return;
	}
	if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, VARG(bits)) <= 0) {
		EVP_PKEY_CTX_free(ctx);
		MAIN_error("RSA keygen bits set failed: &1");
		return;
	}
	if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
		EVP_PKEY_CTX_free(ctx);
		MAIN_error("RSA keygen failed: &1");
		return;
	}
	EVP_PKEY_CTX_free(ctx);

	res = (CPKEY *) GB.New(GB.FindClass("PKey"), NULL, NULL);
	if (!res) {
		EVP_PKEY_free(pkey);
		return;  /* GB.New already raised the error */
	}
	res->pkey = pkey;
	GB.ReturnObject(res);

END_METHOD

BEGIN_METHOD_VOID(PKey_free)

	if (THIS->pkey) {
		EVP_PKEY_free(THIS->pkey);
		THIS->pkey = NULL;
	}

END_METHOD

GB_DESC CPKey[] = {
	GB_DECLARE("PKey", sizeof(CPKEY)),
	GB_NOT_CREATABLE(),

	GB_PROPERTY_READ("Type",       "s",    PKey_Type),
	GB_PROPERTY_READ("Bits",       "i",    PKey_Bits),
	GB_PROPERTY_READ("IsPrivate",  "b",    PKey_IsPrivate),
	GB_PROPERTY_READ("PublicPart", "PKey", PKey_PublicPart),

	GB_STATIC_METHOD("Generate",      "PKey", PKey_Generate,      "(Bits)i"),
	GB_STATIC_METHOD("LoadPrivate",   "PKey", PKey_LoadPrivate,   "(Pem)s[(Passphrase)s]"),
	GB_STATIC_METHOD("LoadPublic",    "PKey", PKey_LoadPublic,    "(Pem)s"),
	GB_STATIC_METHOD("LoadPrivateDer","PKey", PKey_LoadPrivateDer,"(Der)s"),
	GB_STATIC_METHOD("LoadPublicDer", "PKey", PKey_LoadPublicDer, "(Der)s"),

	GB_METHOD("ToPem",       "s", PKey_ToPem,       "[(Passphrase)s]"),
	GB_METHOD("ToPublicPem", "s", PKey_ToPublicPem, NULL),
	GB_METHOD("ToDer",       "s", PKey_ToDer,       NULL),
	GB_METHOD("ToPublicDer", "s", PKey_ToPublicDer, NULL),
	GB_METHOD("_free", NULL, PKey_free, NULL),

	GB_END_DECLARE
};
