/*
 * NAME:
 *	openpgp2pem - play games with secret keys
 */
/*
 * RCSid:
 *	$Id: convert-gpg-seckey-to-pem.c,v 1.7 2023/10/29 06:47:38 sjg Exp $
 *
 *	Copyright (c) 2015-2021 Simon J. Gerraty
 *	Copyright (c) 2014, Juniper Networks, Inc.
 *
 *	This file is provided in the hope that it will
 *	be of use.  There is absolutely NO WARRANTY.
 *	Permission to copy, redistribute or otherwise
 *	use this file is hereby granted provided that 
 *	the above copyright notice and this notice are
 *	left intact. 
 */
#include <sys/stat.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

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

static int dbg_lvl = 0;

#define BINARY_TAG_FLAG 0x80
#define NEW_TAG_FLAG    0x40
#define TAG_MASK        0x3f
#define PARTIAL_MASK    0x1f
#define TAG_COMPRESSED     8
#define OLD_TAG_SHIFT      2
#define OLD_LEN_MASK    0x03
#define CRITICAL_BIT    0x80
#define CRITICAL_MASK   0x7f
#define ONE_BYTE_LEN    0x00
#define TWO_BYTE_LEN    0x01
#define THREE_BYTE_LEN  0x02
#define PKT_SECRET_KEY  0x05
#define PKT_PUBLIC_KEY  0x06
#define PKT_USER_ID     0x0d
#define PKT_SIGNATURE   0x04

/*
 * Read the entire file into a single buffer malloc() based on the
 * size of the file according to stat().
 */
static ssize_t
slurp_file(const char *file, unsigned char **buf, size_t *bufsz)
{
    struct stat sb;
    int fd;
    int sz;
    int retval;

    if (bufsz == NULL || buf == NULL) {
	errno = EFAULT;
	return -1;
    }

    if (*buf == NULL)
	*bufsz = 0;
    else
	memset(*buf, 0, *bufsz);

    if (stat(file, &sb) != 0)
	return -1;		/* errno is already set */

    if (S_ISREG(sb.st_mode) == 0) {
	errno = ENODEV;
	return -1;
    }
    if (sb.st_size == 0)
	return 0;

    fd = open(file, O_RDONLY | O_NONBLOCK);
    if (fd == -1)
	return -1;
    if (*bufsz < sb.st_size) {
	if (*bufsz > 0) {
	    free(buf);
	    *bufsz = 0;
	}
	*buf = malloc(sb.st_size);
	if (*buf == NULL)
	    return -1;
	*bufsz = sb.st_size;
    }	

    sz = read(fd, *buf, *bufsz);
    if (sz == 0)
	return 0;

    if (sz == -1 || (sz != *bufsz))
	return -1;

    while ((retval = close(fd))) {
	if (retval == -1 && errno == EINTR)
	    errno = 0;
	else
	    break;
    }

    return sz;
}

/*
 * Convert an OpenPGP multi-precision-integer into an
 * OpenSSL BIGNUM format number.
 */
static int
mpi2bn(unsigned char **pptr, BIGNUM **bn, int zeroize)
{
    int mlen;

    unsigned char *ptr;

    if (pptr == NULL)
	return -1;

    ptr = *pptr;

    mlen = (*ptr++ << 8);
    mlen |= *ptr++;		     /* number of bits */
    mlen = (mlen + 7) / 8;	     /* number of bytes */

    if (bn != NULL)
	*bn = BN_bin2bn(ptr, mlen, *bn);
    if (zeroize)
	memset(ptr, '\0', mlen);

    ptr += mlen;
    *pptr = ptr;

    return 0;
}


/*
 * Extract a length from a OpenPGP packet in new-style format.
 */
static int
get_new_len(unsigned char **pptr, int *partial)
{
    unsigned char *ptr;
    int len;

    if (pptr == NULL)
	return -1;
    ptr = *pptr;

    if (*ptr < 224 || *ptr == 255)
	*partial = 0;
    else
	*partial = 1;

    if (*ptr < 192)
	len = *ptr++;
    else if (*ptr < 224) {
	len = ((*ptr - 192) << 8) + *(ptr+1) + 192;
	ptr++;
    } else if (*ptr == 255) {
	len = (*ptr++ << 24);
	len |= (*ptr++ << 16);
	len |= (*ptr++ < 8);
	len |= *ptr++;
    } else
	len = 1 << (*ptr++ & PARTIAL_MASK);

    *pptr = ptr;
    return len;
}

/*
 * Extract a length from a OpenPGP packet in old-style format.
 */
static int
get_old_len(unsigned char **pptr)
{
    int tlen;
    int len;
    unsigned char *ptr;

    if (pptr == NULL)
	return -1;

    len = -1;				/* keep compiler happy */
    ptr = *pptr;
    tlen = *ptr++ & OLD_LEN_MASK;
    switch (tlen) {
    case 0:
	len = *ptr++;
	break;
    case 1:
	len = (*ptr++ << 8);
	len |= *ptr++;
	break;
    case 2:
	len =  *ptr++ << 24;
	len |= *ptr++ << 16;
	len |= *ptr++ << 8;
	len |= *ptr++;
	break;
    case 3:
	/* Not supported */
	len = -1;
    }

    *pptr = ptr;
    return len;
}

/*
 * The public RSA key material is not encrypted and is just two
 * multi-precision-integer values for the public key modulus and the
 * private key exponent.
 */
static int
get_public_material(unsigned char **pptr, RSA *rsa)
{
    BIGNUM *n, *e;

    if (pptr == NULL || rsa == NULL) return -1;

    /* We need the RSA components non-NULL */
    if ((n=BN_new()) == NULL) return -1;
    if ((e=BN_new()) == NULL) return -1;

    /* RSA n - public key modulus */
    if (mpi2bn(pptr, &n, 0) != 0) return -1;

    /* RSA e - private key exponent */
    if (mpi2bn(pptr, &e, 0) != 0) return -1;

    RSA_set0_key(rsa, n, e, NULL);
    return 0;
}

static int
get_secret_material(unsigned char **pptr, RSA *rsa)
{
    BIGNUM *r1 = NULL, *r2 = NULL;
    BIGNUM *d, *p, *q, *dmp1, *dmq1, *iqmp;
    BN_CTX *ctx = NULL;
    int rc = -1;
    
    if (pptr == NULL || rsa == NULL) return -1;

    /* We need the RSA secret components non-NULL */
    if ((d=BN_new()) == NULL) goto err;
    if ((p=BN_new()) == NULL) goto err;
    if ((q=BN_new()) == NULL) goto err;
    if ((dmp1=BN_new()) == NULL) goto err;
    if ((dmq1=BN_new()) == NULL) goto err;
    if ((iqmp=BN_new()) == NULL) goto err;

    /* RSA d */
    if (mpi2bn(pptr, &d, 1) != 0) goto err;

    /* RSA p */
    if (mpi2bn(pptr, &p, 1) != 0) goto err;

    /* RSA q */
    if (mpi2bn(pptr, &q, 1) != 0) goto err;

    /* OpenPGP u - the multiplicative inverse of p, mod q. */
    if (mpi2bn(pptr, NULL, 1) != 0) goto err;


    /* Checksum field 2 bytes */
    *pptr += 2;

    /*
     * Following this would be:
     *  + User ID Packet (tag 13)
     *  + Signature Packet (tag 2)
     *  + (optional Trust Packet (tag 12)
     * We do not really care about those as we need to get them all
     * from the public keyring file anyway.
     */
    /* We are finished using the secret key buffer, zeroize it. */
    *pptr = NULL;

    /*
     * The OpenPGP folks use 
     */
    ctx = BN_CTX_new();
    if (ctx == NULL) goto err;
    BN_CTX_start(ctx);
    r1 = BN_CTX_get(ctx);
    r2 = BN_CTX_get(ctx);

    if (!BN_sub(r1, p, BN_value_one())) goto err;	/* p-1 */
    if (!BN_sub(r2, q, BN_value_one())) goto err;	/* q-1 */

    /* dmp1 = d mod (p-1) */
    if (!BN_mod(dmp1, d, r1, ctx)) goto err;

    /* calculate d mod (q-1) */
    if (!BN_mod(dmq1, d, r2, ctx)) goto err;

    /* q^-1 mod p */
    if (!BN_mod_inverse(iqmp, q, p, ctx)) goto err;

    if (!RSA_set0_key(rsa, NULL, NULL, d)) goto err;
    if (!RSA_set0_factors(rsa, p, q)) goto err;
    if (!RSA_set0_crt_params(rsa, dmp1, dmq1, iqmp)) goto err;

    rc = 0;
 err:
    if (ctx != NULL) {
	BN_CTX_end(ctx);
	BN_CTX_free(ctx);
	r1 = NULL;
	r2 = NULL;
	ctx = NULL;
    }
    
    return rc;
}

static unsigned char *
dearmor(char *pem, size_t nbytes, long *len)
{
    BIO *bp;
    char *name = NULL;
    char *header = NULL;
    unsigned char *data = NULL;
    char *cp;
    char *ep;

    /* we need to remove the Armor tail */
    if ((cp = strstr(pem, "\n=")) &&
	(ep = strstr(cp, "\n---"))) {
	memmove(cp, ep, nbytes - (ep - pem));
	nbytes -= (ep - cp);
	pem[nbytes] = '\0';
    }
    *len = nbytes;
    if ((bp = BIO_new_mem_buf(pem, nbytes))) {
	if (!PEM_read_bio(bp, &name, &header, &data, len))
	    data = NULL;
	BIO_free(bp);
    }
    return data;
}


/*
 * Read a binary secring.gpg file and convert the first secret key in
 * the file into a form suitable to be written into a
 * '-----BEGIN RSA PRIVATE KEY-----' format PEM file.
 */
static EVP_PKEY *
parse_key(unsigned char *buf, size_t bufsz, int *ispriv)
{
    unsigned char *ptr;
    unsigned char *bptr;
    unsigned int tag;
    long len;
    int partial = -1;
    int version = -1;
    RSA *rsa = NULL;
    EVP_PKEY *pkey = NULL;
    
    /* Process the OpenPGP secret key ring. */
    ptr = bptr = buf;

    /*
     * first byte is probably 0x95 == '1001 0101'binary
     * BINARY_TAG_FLAG | (PKT_SECRET_KEY << OLD_TAG_SHIFT) | TWO_BYTE_LEN
     */ 
    if ((*ptr & BINARY_TAG_FLAG) == 0) {
	ptr = bptr = dearmor((char *)buf, bufsz, &len);
	if (!ptr)
	    goto err;
	bufsz = len;
    }
    tag = *ptr & TAG_MASK;
    if (tag == TAG_COMPRESSED) {
	fprintf(stderr, "Compressed format not yet supported.\n");
	goto err;
    }

    if ((*ptr & NEW_TAG_FLAG)) {
	/* Code path for this case has NOT been tested. */
	ptr++;
	if ((len = get_new_len(&ptr, &partial)) == -1) goto err;
    } else {
	tag >>= OLD_TAG_SHIFT;
	if ((len = get_old_len(&ptr)) == -1) goto err;
    }

    switch (tag) {
    case PKT_SECRET_KEY:
	*ispriv = 1;
	break;
    case PKT_PUBLIC_KEY:
	*ispriv = 0;
	break;
    default:
	fprintf(stderr, "Expecting Key: (tag %u not in [5,6]).\n", tag);
	goto err;
    }

    version = *ptr++;
    if (version != 0x04 && version != 0x02 && version != 0x03) {
	fprintf(stderr, "Expected Version 2, 3, or 4 format, got %u\n", *ptr);
	goto err;
    }

    /* Skip creation time */
    ptr += 4;			/* four bytes for creation time */
    if (*ptr != 1 && *ptr != 2 && *ptr != 3) {
	fprintf(stderr, "Public Key Algorithm %u not supported.\n", *ptr);
	goto err;
    }
    ptr++;

    /*
     * Populate the public key modulus and exponent
     * followed by the private key d,p,q values
     * and compute the dmpq, dmq1, and iqmp values.
     */
    rsa = RSA_new();
    if (rsa == NULL) goto err;
    if (get_public_material(&ptr, rsa) != 0) goto err;

    if (tag == PKT_SECRET_KEY) {
	/*
	 * RFC 4880 5.5.3.  Secret-Key Packet Formats
	 *
	 * The Secret Key Packet material may be found with
	 * 0: secret-key data is not encrypted
	 * 254 or 255: string-to-key specifier is being given
	 * All other values are a symmetric-key encryption algorithm
	 * identifier.
	 */
	if (*ptr != 0) {
	    fprintf(stderr, "Only unencrypted secret key supported not s2k=%u\n",
		    *ptr);
	    goto err;
	}
	ptr++;
	if (get_secret_material(&ptr, rsa) != 0) goto err;
    }
    /*
     * Everything goes into an EVP_PKEY for the caller.
     */
    pkey = EVP_PKEY_new();
    if (pkey != NULL)
	if (EVP_PKEY_set1_RSA(pkey, rsa) == 0) {
	    EVP_PKEY_free(pkey);
	    pkey = NULL;
	}

  err:
    if (bptr != buf) {
	memset(bptr, 0, bufsz);
	free(bptr);
    }
    if (rsa != NULL)
	RSA_free(rsa);

    return pkey;
}

/*
 * The main event. Read the input secring.pgp file and output the .pem
 * file. Optionally, use -d to provide debugging messages.
 */
int
main(int argc, char *argv[])
{
    FILE *fp;
    EVP_PKEY *pkey = NULL;
    int rc = EXIT_FAILURE;
    unsigned char *buf = NULL;
    size_t bufsz = 0;
    ssize_t read_bytes = 0;
    RSA *rsa = NULL;
    char *inputfile = "secring.gpg";
    char *outputfile = NULL;
    int c;
    int ispriv;
    char *progname;

    progname = argv[0];

    while ((c = getopt(argc, argv, "i:o:dp:")) != -1) {
	switch (c) {
	case 'i':
	    inputfile = optarg;
	    break;
	case 'o':
	    outputfile = optarg;
	    break;
	case 'd':
	    dbg_lvl++;
	    break;
	default:
	    fprintf(stderr, "%s: [-i secring.pgp][-o secring.pem]\n",
		    progname);
	    return rc;
	}
    }

    if (dbg_lvl > 0)
	fprintf(stderr, "%s -i %s -o %s\n", progname, inputfile, outputfile);

    read_bytes = slurp_file(inputfile, &buf, &bufsz);
    if (read_bytes == -1) {
	fprintf(stderr, "%s: Problem reading %s: %s\n",
		progname, inputfile, strerror(errno));
	goto err;
    } else if (read_bytes == 0) {
	fprintf(stderr, "%s: Zero bytes found in %s\n", progname, inputfile);
	goto err;
    }
	
    pkey = parse_key(buf, bufsz, &ispriv);
    memset(buf, 0, bufsz);

    free(buf);
    buf = NULL;
    if (pkey == NULL) {
	    perror("could not parse key");
	    goto err;
    }

    /*
     * OpenPGP handles more than just RSA keys, but this program only
     * cares about RSA keys.
     */
    if (EVP_PKEY_base_id(pkey) == EVP_PKEY_RSA) {
	if (outputfile != NULL) {
	    rsa = EVP_PKEY_get1_RSA(pkey);
	    if (rsa != NULL) {
		if ((fp = fopen(outputfile, "w")) == NULL) {
		    perror("Unable to write key.pem");
		    goto err;
		}

		if (ispriv)
		    rc = PEM_write_RSAPrivateKey(fp, rsa, NULL, 0, 0, NULL, NULL);
		else
		    rc = PEM_write_RSAPublicKey(fp, rsa);

		if (!rc) {
		    rc = EXIT_FAILURE;
		    perror("Error writing PEM key");
		    fflush(fp);
		    fclose(fp);
		    goto err;
		}
		fflush(fp);
		fclose(fp);
		rc = EXIT_SUCCESS;
	    }
	}
    }

  err:
    if (buf) {
	if (bufsz > 0)
	    memset(buf, 0, bufsz);
	free(buf);
    }
    if (rsa) RSA_free(rsa);
    if (pkey) EVP_PKEY_free(pkey);
    rsa = NULL;
    pkey = NULL;
    return rc;
}
