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 →