Skip to content

PGP Key Generator — C source

Generate PGP key pairs (ECC or RSA) in your browser. Download your public and private keys. Powered by OpenPGP.js.

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/*
 * pgp-keygen — OpenPGP key pair generation (ECC Curve25519, RSA-2048, RSA-4096).
 *
 * Language: C (C11, standard library + GPGME 1.x — the GnuPG project's official
 *           C binding, the native counterpart to the TS reference's openpgp.js)
 * Source:   CosmoDev polyglot showcase port of the PGP Key Generator tool,
 *           ported from src/lib/pgp-keygen.ts (the canonical TypeScript
 *           implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * The TS reference is a thin wrapper around openpgp.js `generateKey`; this port
 * keeps that shape and wraps GPGME instead — identity validation stays pure C,
 * key generation delegates to the library. Both produce the same four outputs:
 * an ASCII-armored public key, an ASCII-armored private key, the 40-hex-char v4
 * fingerprint, and an armored revocation certificate.
 *
 * The private key never leaves the process: GPGME is driven against a
 * caller-supplied ephemeral GNUPGHOME, mirroring the TS island's guarantee that
 * generation happens entirely client-side.
 *
 * Build: cc -std=c11 pgp-keygen.c $(gpgme-config --cflags --libs)
 */

#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <gpgme.h>
#include <locale.h>

/* --------------------------------------------------------------- constants --- */

enum {
    PGP_FINGERPRINT_LEN = 40, /* v4 fingerprint: 40 lowercase hex chars */
    PGP_USERID_MAX      = 512
};

/** Mirrors the TS PGPKeyGenAlgorithm union. */
typedef enum {
    PGP_ALG_ECC,     /* Curve25519 — fast, the TS default */
    PGP_ALG_RSA2048,
    PGP_ALG_RSA4096  /* can take a few seconds */
} pgp_algorithm;

/** Mirrors the TS PGPKeyGenOptions interface. */
typedef struct {
    /* User's real name (goes into the key's user ID packet). */
    const char *name;
    /* User's email (goes into the key's user ID packet). */
    const char *email;
    /* Optional passphrase. If given, the private key is encrypted with it.
     * NULL or "" means an unprotected key — matching the TS `pass` ternary,
     * where an empty string is deliberately treated as "no passphrase". */
    const char *passphrase;
    pgp_algorithm algorithm;
} pgp_keygen_options;

/** Mirrors the TS PGPKeyPair interface. All strings are heap-owned. */
typedef struct {
    /* ASCII-armored public key (BEGIN PGP PUBLIC KEY BLOCK). */
    char *public_key;
    /* ASCII-armored private key (BEGIN PGP PRIVATE KEY BLOCK). */
    char *private_key;
    /* v4 fingerprint, 40 lowercase hex chars, no spaces. */
    char fingerprint[PGP_FINGERPRINT_LEN + 1];
    /* ASCII-armored revocation certificate. */
    char *revocation_certificate;
} pgp_keypair;

/* Error reporting mirrors the TS `throw new Error(...)` messages: every entry
 * point writes one into `err` and returns false rather than aborting. */
typedef struct {
    char message[160];
} pgp_err;

static bool pgp_fail(pgp_err *err, const char *msg)
{
    if (err != NULL) {
        snprintf(err->message, sizeof err->message, "%s", msg);
    }
    return false;
}

/* ------------------------------------------------------------ trim helpers --- */

/* Copy `s` into `out` with leading/trailing ASCII whitespace removed — the C
 * stand-in for the TS `.trim()`. Returns the trimmed length. */
static size_t pgp_trim(const char *s, char *out, size_t out_size)
{
    size_t start = 0;
    size_t end;

    if (s == NULL) {
        out[0] = '\0';
        return 0;
    }
    while (s[start] != '\0' && (unsigned char) s[start] <= ' ') {
        start++;
    }
    end = strlen(s);
    while (end > start && (unsigned char) s[end - 1] <= ' ') {
        end--;
    }
    if (end - start >= out_size) {
        end = start + out_size - 1;
    }
    memcpy(out, s + start, end - start);
    out[end - start] = '\0';
    return end - start;
}

/* ------------------------------------------------------ identity validation --- */

/*
 * The TS reference validates with /^[^\s@]+@[^\s@]+\.[^\s@]+$/. C has no regex
 * in its standard library, so the same three conditions are checked directly:
 * exactly one '@', a non-empty local part, and a domain holding a dot with
 * non-empty labels either side. No whitespace anywhere.
 */
static bool pgp_email_valid(const char *email)
{
    const char *at  = NULL;
    const char *dot = NULL;
    const char *p;

    for (p = email; *p != '\0'; p++) {
        if ((unsigned char) *p <= ' ') {
            return false; /* \s is excluded from every character class */
        }
        if (*p == '@') {
            if (at != NULL) {
                return false; /* a second '@' cannot match [^\s@]+ */
            }
            at = p;
        }
    }
    if (at == NULL || at == email || at[1] == '\0') {
        return false; /* missing, empty local part, or empty domain */
    }
    /* The final '.' splits the domain; both sides must be non-empty. */
    dot = strrchr(at + 1, '.');
    return dot != NULL && dot != at + 1 && dot[1] != '\0';
}

/*
 * Validate the identity that goes into the key's user ID. Mirrors the TS
 * validateKeyGenIdentity(), including its exact error messages.
 */
bool validate_keygen_identity(const char *name, const char *email, pgp_err *err)
{
    char trimmed_name[PGP_USERID_MAX];
    char trimmed_email[PGP_USERID_MAX];

    if (pgp_trim(name, trimmed_name, sizeof trimmed_name) == 0) {
        return pgp_fail(err, "Name is required.");
    }
    if (pgp_trim(email, trimmed_email, sizeof trimmed_email) == 0) {
        return pgp_fail(err, "Email is required.");
    }
    if (!pgp_email_valid(trimmed_email)) {
        return pgp_fail(err, "Invalid email address.");
    }
    return true;
}

/* ------------------------------------------------------------ gpgme plumbing --- */

/* GPGME's algorithm strings, matching the TS branches: 'curve25519' for ECC
 * (modern X25519/Ed25519 keys), explicit bit counts for RSA. */
static const char *pgp_algorithm_string(pgp_algorithm algorithm)
{
    switch (algorithm) {
    case PGP_ALG_ECC:     return "ed25519/cert,sign+cv25519/encr";
    case PGP_ALG_RSA2048: return "rsa2048";
    case PGP_ALG_RSA4096: return "rsa4096";
    default:              return NULL;
    }
}

/* Supply the passphrase non-interactively (loopback pinentry). */
static gpgme_error_t pgp_passphrase_cb(void *hook, const char *uid_hint,
                                       const char *passphrase_info,
                                       int prev_was_bad, int fd)
{
    const char *pass = (const char *) hook;
    size_t len;

    (void) uid_hint;
    (void) passphrase_info;
    if (prev_was_bad) {
        return gpg_error(GPG_ERR_CANCELED);
    }
    len = strlen(pass);
    if (len != 0 && (size_t) gpgme_io_write(fd, pass, len) != len) {
        return gpg_error(GPG_ERR_CANCELED);
    }
    return (gpgme_io_write(fd, "\n", 1) == 1) ? 0 : gpg_error(GPG_ERR_CANCELED);
}

/* Drain a GPGME data object into a fresh NUL-terminated heap string. */
static char *pgp_data_to_string(gpgme_data_t data)
{
    size_t len = 0;
    char *raw  = gpgme_data_release_and_get_mem(data, &len);
    char *out;

    if (raw == NULL) {
        return NULL;
    }
    out = malloc(len + 1);
    if (out != NULL) {
        memcpy(out, raw, len);
        out[len] = '\0';
    }
    gpgme_free(raw);
    return out;
}

/* Export one key by fingerprint, armored. `secret` picks public vs private. */
static char *pgp_export(gpgme_ctx_t ctx, const char *fingerprint, bool secret)
{
    gpgme_data_t out = NULL;
    gpgme_export_mode_t mode = secret ? GPGME_EXPORT_MODE_SECRET : 0;

    if (gpgme_data_new(&out) != 0) {
        return NULL;
    }
    if (gpgme_op_export(ctx, fingerprint, mode, out) != 0) {
        gpgme_data_release(out);
        return NULL;
    }
    return pgp_data_to_string(out);
}

/* --------------------------------------------------------------- public api --- */

void pgp_keypair_free(pgp_keypair *kp)
{
    if (kp == NULL) {
        return;
    }
    if (kp->private_key != NULL) {
        /* Wipe the secret material before handing the pages back. */
        memset(kp->private_key, 0, strlen(kp->private_key));
    }
    free(kp->public_key);
    free(kp->private_key);
    free(kp->revocation_certificate);
    memset(kp, 0, sizeof *kp);
}

/*
 * Generate an ASCII-armored PGP key pair. ECC (Curve25519) is fast; RSA-4096
 * can take a few seconds. Mirrors the TS generatePGPKeyPair().
 *
 * `home_dir` is the GNUPGHOME the key is created in — pass a fresh temporary
 * directory to keep generation ephemeral, or NULL to use the default keyring.
 */
bool generate_pgp_keypair(const pgp_keygen_options *options, const char *home_dir,
                          pgp_keypair *out, pgp_err *err)
{
    char name[PGP_USERID_MAX];
    char email[PGP_USERID_MAX];
    char userid[PGP_USERID_MAX];
    const char *algorithm;
    const char *pass;
    gpgme_ctx_t ctx = NULL;
    gpgme_genkey_result_t result;
    gpgme_key_t key = NULL;
    unsigned int flags;
    bool ok = false;

    if (options == NULL || out == NULL) {
        return pgp_fail(err, "Options and output must not be NULL.");
    }
    memset(out, 0, sizeof *out);

    if (!validate_keygen_identity(options->name, options->email, err)) {
        return false;
    }
    algorithm = pgp_algorithm_string(options->algorithm);
    if (algorithm == NULL) {
        return pgp_fail(err, "Unsupported algorithm.");
    }

    /* An empty-string passphrase would still encrypt the key; only a real
     * passphrase should — the same guard as the TS `pass` ternary. */
    pass = (options->passphrase != NULL && options->passphrase[0] != '\0')
               ? options->passphrase
               : NULL;

    pgp_trim(options->name, name, sizeof name);
    pgp_trim(options->email, email, sizeof email);
    /* GPGME takes the user ID as one "Name <email>" string, the same two fields
     * the TS reference passes as `userIDs: [{ name, email }]`. */
    snprintf(userid, sizeof userid, "%s <%s>", name, email);

    gpgme_check_version(NULL);
    gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
    if (gpgme_new(&ctx) != 0) {
        return pgp_fail(err, "Failed to create a GPGME context.");
    }
    if (gpgme_set_protocol(ctx, GPGME_PROTOCOL_OPENPGP) != 0 ||
        gpgme_ctx_set_engine_info(ctx, GPGME_PROTOCOL_OPENPGP, NULL, home_dir) != 0) {
        pgp_fail(err, "Failed to configure the OpenPGP engine.");
        goto done;
    }
    gpgme_set_armor(ctx, 1); /* every output is ASCII-armored */

    if (pass != NULL) {
        gpgme_set_pinentry_mode(ctx, GPGME_PINENTRY_MODE_LOOPBACK);
        gpgme_set_passphrase_cb(ctx, pgp_passphrase_cb, (void *) pass);
        flags = 0;
    } else {
        flags = GPGME_CREATE_NOPASSWD;
    }
    /* The TS reference generates non-expiring keys with a revocation
     * certificate; GPGME needs both asked for explicitly. */
    flags |= GPGME_CREATE_CERT | GPGME_CREATE_SIGN | GPGME_CREATE_NOEXPIRE;

    if (gpgme_op_createkey(ctx, userid, algorithm, 0, 0, NULL, flags) != 0) {
        pgp_fail(err, "Key generation failed.");
        goto done;
    }
    result = gpgme_op_genkey_result(ctx);
    if (result == NULL || result->fpr == NULL) {
        pgp_fail(err, "Key generation returned no fingerprint.");
        goto done;
    }
    snprintf(out->fingerprint, sizeof out->fingerprint, "%s", result->fpr);

    /* With ECC the encryption subkey is requested inline via the algorithm
     * string; RSA needs an explicit encrypt-capable subkey to match the TS
     * reference's key shape. */
    if (options->algorithm != PGP_ALG_ECC) {
        if (gpgme_get_key(ctx, out->fingerprint, &key, 1) != 0 || key == NULL) {
            pgp_fail(err, "Failed to read back the generated key.");
            goto done;
        }
        if (gpgme_op_createsubkey(ctx, key, algorithm, 0, 0,
                                  GPGME_CREATE_ENCR | GPGME_CREATE_NOEXPIRE |
                                      (pass != NULL ? 0u : GPGME_CREATE_NOPASSWD)) != 0) {
            pgp_fail(err, "Failed to add the encryption subkey.");
            goto done;
        }
    }

    out->public_key  = pgp_export(ctx, out->fingerprint, false);
    out->private_key = pgp_export(ctx, out->fingerprint, true);
    if (out->public_key == NULL || out->private_key == NULL) {
        pgp_fail(err, "Failed to export the generated key.");
        goto done;
    }

    /* GPGME writes the revocation certificate into the home directory as
     * openpgp-revocs.d/<FINGERPRINT>.rev; read it back so the caller gets the
     * same armored string the TS reference returns. */
    if (home_dir != NULL) {
        char path[1024];
        FILE *fp;

        snprintf(path, sizeof path, "%s/openpgp-revocs.d/%s.rev",
                 home_dir, out->fingerprint);
        fp = fopen(path, "rb");
        if (fp != NULL) {
            long size;

            if (fseek(fp, 0, SEEK_END) == 0 && (size = ftell(fp)) > 0 &&
                fseek(fp, 0, SEEK_SET) == 0) {
                out->revocation_certificate = malloc((size_t) size + 1);
                if (out->revocation_certificate != NULL) {
                    size_t read = fread(out->revocation_certificate, 1, (size_t) size, fp);
                    out->revocation_certificate[read] = '\0';
                }
            }
            fclose(fp);
        }
    }
    ok = true;

done:
    gpgme_key_unref(key);
    gpgme_release(ctx);
    if (!ok) {
        pgp_keypair_free(out);
    }
    return ok;
}

/* -------------------------------------------------------------------- demo --- */

int main(void)
{
    /* An ephemeral GNUPGHOME keeps the demo from touching the real keyring. */
    const char *home = "/tmp/cosmodev-pgp-keygen";
    pgp_keygen_options options = {
        .name       = "Ada Lovelace",
        .email      = "ada@example.com",
        .passphrase = NULL,
        .algorithm  = PGP_ALG_ECC
    };
    pgp_keypair kp;
    pgp_err err = {0};

    if (!validate_keygen_identity("Ada", "not-an-email", &err)) {
        printf("rejected as expected: %s\n", err.message);
    }

    if (!generate_pgp_keypair(&options, home, &kp, &err)) {
        fprintf(stderr, "pgp-keygen: %s\n", err.message);
        return 1;
    }
    printf("fingerprint: %s\n\n%s\n", kp.fingerprint, kp.public_key);
    pgp_keypair_free(&kp);
    return 0;
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →