Skip to content

PGP Encrypt & Decrypt — C source

Encrypt or decrypt messages with PGP public/private keys. Powered by OpenPGP.js, runs entirely in your browser.

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

/*
 * pgp-encrypt — read OpenPGP key metadata, encrypt (optionally signing), decrypt.
 *
 * 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 Encrypt & Decrypt tool,
 *           ported from src/lib/pgp-encrypt.ts (the canonical TypeScript
 *           implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * Three entry points, matching the TS public API one-for-one:
 *   pgp_read_key_info() — user ID, uppercase fingerprint, algorithm, creation
 *                         date, expiry (NULL when the key never expires), and
 *                         whether the armor holds a private key.
 *   pgp_encrypt()       — ASCII-armored PGP message for a recipient's public
 *                         key, optionally signed with the sender's private key.
 *   pgp_decrypt()       — plaintext from an armored message + private key.
 *
 * Armored key material is imported into a caller-supplied ephemeral GNUPGHOME,
 * so nothing is written to the user's real keyring — the C equivalent of the TS
 * island keeping every operation client-side.
 *
 * Build: cc -std=c11 pgp-encrypt.c $(gpgme-config --cflags --libs)
 */

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

#include <gpgme.h>

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

enum {
    PGP_FPR_LEN  = 40, /* v4 fingerprint: 40 hex chars */
    PGP_DATE_LEN = 10  /* "YYYY-MM-DD" */
};

/* The two armor headers the TS reference accepts as "this is a private key". */
#define PGP_PRIVATE_HEADER "-----BEGIN PGP PRIVATE KEY BLOCK-----"
#define PGP_SECRET_HEADER  "-----BEGIN PGP SECRET KEY BLOCK-----"

/** Mirrors the TS PGPKeyInfo interface. */
typedef struct {
    char user_id[256];
    /* Uppercase, no spaces — matches the TS getFingerprint().toUpperCase(). */
    char fingerprint[PGP_FPR_LEN + 1];
    char algorithm[32];
    /* ISO date only (the TS .toISOString().split('T')[0]). */
    char creation_date[PGP_DATE_LEN + 1];
    /* Empty string when the key never expires — the TS `null`. */
    char expiry[PGP_DATE_LEN + 1];
    bool is_private;
} pgp_key_info;

/* 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[256];
} 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;
}

/* Compose the TS "Invalid PGP key: <reason>" message from a GPGME code. */
static bool pgp_fail_key(pgp_err *err, gpgme_error_t code)
{
    if (err != NULL) {
        snprintf(err->message, sizeof err->message, "Invalid PGP key: %s",
                 (code != 0) ? gpgme_strerror(code) : "unrecognized key format");
    }
    return false;
}

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

/* Offsets of `s` with leading/trailing ASCII whitespace removed — the C
 * stand-in for the TS `.trim()`, without copying the (possibly large) armor. */
static const char *pgp_trim(const char *s, size_t *len)
{
    size_t end;

    if (s == NULL) {
        *len = 0;
        return "";
    }
    while (*s != '\0' && (unsigned char) *s <= ' ') {
        s++;
    }
    end = strlen(s);
    while (end > 0 && (unsigned char) s[end - 1] <= ' ') {
        end--;
    }
    *len = end;
    return s;
}

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

/* 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_BAD_PASSPHRASE);
    }
    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);
}

/*
 * Open an OpenPGP context bound to `home_dir`, armored output on. When
 * `passphrase` is non-NULL the loopback pinentry hands it to the agent, so a
 * protected private key can be used without a terminal prompt — the C analogue
 * of the TS decryptKey({ privateKey, passphrase }) step.
 */
static bool pgp_context(gpgme_ctx_t *ctx, const char *home_dir,
                        const char *passphrase, pgp_err *err)
{
    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) {
        gpgme_release(*ctx);
        *ctx = NULL;
        return pgp_fail(err, "Failed to configure the OpenPGP engine.");
    }
    gpgme_set_armor(*ctx, 1);

    if (passphrase != NULL) {
        gpgme_set_pinentry_mode(*ctx, GPGME_PINENTRY_MODE_LOOPBACK);
        gpgme_set_passphrase_cb(*ctx, pgp_passphrase_cb, (void *) passphrase);
    }
    return true;
}

/* Import armored key material and hand back the first imported key. */
static bool pgp_import_key(gpgme_ctx_t ctx, const char *armored, size_t len,
                           gpgme_key_t *out, pgp_err *err)
{
    gpgme_data_t data = NULL;
    gpgme_import_result_t result;
    gpgme_error_t rc;

    *out = NULL;
    rc = gpgme_data_new_from_mem(&data, armored, len, 0);
    if (rc != 0) {
        return pgp_fail_key(err, rc);
    }
    rc = gpgme_op_import(ctx, data);
    gpgme_data_release(data);
    if (rc != 0) {
        return pgp_fail_key(err, rc);
    }
    result = gpgme_op_import_result(ctx);
    if (result == NULL || result->imports == NULL || result->imports->fpr == NULL) {
        return pgp_fail_key(err, 0);
    }
    rc = gpgme_get_key(ctx, result->imports->fpr, out, 0);
    if (rc != 0 || *out == NULL) {
        return pgp_fail_key(err, rc);
    }
    return true;
}

/* 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;
}

/* Format a UNIX timestamp as "YYYY-MM-DD" in UTC — the TS
 * .toISOString().split('T')[0]. */
static void pgp_format_date(long timestamp, char out[PGP_DATE_LEN + 1])
{
    time_t t = (time_t) timestamp;
    struct tm utc;

    if (timestamp <= 0 || gmtime_r(&t, &utc) == NULL) {
        out[0] = '\0';
        return;
    }
    snprintf(out, PGP_DATE_LEN + 1, "%04d-%02d-%02d",
             utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday);
}

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

/*
 * Read a PGP key (public or private) and extract its metadata. Mirrors the TS
 * readKeyInfo(); fails with "Invalid PGP key: ..." on unrecognized material.
 */
bool pgp_read_key_info(const char *armored_key, const char *home_dir,
                       pgp_key_info *out, pgp_err *err)
{
    size_t len;
    const char *trimmed;
    char *owned = NULL;
    gpgme_ctx_t ctx = NULL;
    gpgme_key_t key = NULL;
    bool ok = false;

    if (out == NULL) {
        return pgp_fail(err, "Output parameter must not be NULL.");
    }
    memset(out, 0, sizeof *out);

    trimmed = pgp_trim(armored_key, &len);
    if (len == 0) {
        return pgp_fail(err, "Key input is empty or invalid.");
    }

    /* Detect the key type from the armor header, before any parsing — exactly
     * where the TS reference does it. strstr() needs a NUL-terminated copy. */
    owned = malloc(len + 1);
    if (owned == NULL) {
        return pgp_fail(err, "Out of memory.");
    }
    memcpy(owned, trimmed, len);
    owned[len] = '\0';
    out->is_private = strstr(owned, PGP_PRIVATE_HEADER) != NULL ||
                      strstr(owned, PGP_SECRET_HEADER) != NULL;

    if (!pgp_context(&ctx, home_dir, NULL, err) ||
        !pgp_import_key(ctx, owned, len, &key, err)) {
        goto done;
    }

    /* users[0] || 'unknown' */
    snprintf(out->user_id, sizeof out->user_id, "%s",
             (key->uids != NULL && key->uids->uid != NULL && key->uids->uid[0] != '\0')
                 ? key->uids->uid
                 : "unknown");

    if (key->fpr != NULL) {
        size_t i;

        snprintf(out->fingerprint, sizeof out->fingerprint, "%s", key->fpr);
        for (i = 0; out->fingerprint[i] != '\0'; i++) {
            if (out->fingerprint[i] >= 'a' && out->fingerprint[i] <= 'f') {
                out->fingerprint[i] = (char) (out->fingerprint[i] - 'a' + 'A');
            }
        }
    }

    snprintf(out->algorithm, sizeof out->algorithm, "%s",
             (key->subkeys != NULL)
                 ? gpgme_pubkey_algo_name(key->subkeys->pubkey_algo)
                 : "unknown");

    if (key->subkeys != NULL) {
        pgp_format_date(key->subkeys->timestamp, out->creation_date);
        /* expires == 0 is GPGME's "never" — the TS Infinity/null branch. */
        if (key->subkeys->expires != 0) {
            pgp_format_date(key->subkeys->expires, out->expiry);
        }
    }
    ok = true;

done:
    free(owned);
    gpgme_key_unref(key);
    gpgme_release(ctx);
    return ok;
}

/*
 * Encrypt a plaintext message for a recipient's public key, returning an
 * ASCII-armored PGP message. When `signing_private_key_armored` is given the
 * message is signed too (with `passphrase` if the key is protected).
 * Mirrors the TS pgpEncrypt(). The caller owns *out_armored.
 */
bool pgp_encrypt(const char *message, const char *public_key_armored,
                 const char *signing_private_key_armored, const char *passphrase,
                 const char *home_dir, char **out_armored, pgp_err *err)
{
    size_t pub_len, sign_len;
    const char *pub_trimmed;
    const char *sign_trimmed;
    gpgme_ctx_t ctx = NULL;
    gpgme_key_t recipients[2] = {NULL, NULL};
    gpgme_key_t signing_key = NULL;
    gpgme_data_t plain = NULL;
    gpgme_data_t cipher = NULL;
    gpgme_error_t rc;
    bool ok = false;

    if (out_armored == NULL) {
        return pgp_fail(err, "Output parameter must not be NULL.");
    }
    *out_armored = NULL;

    if (message == NULL || message[0] == '\0') {
        return pgp_fail(err, "Message must not be empty.");
    }
    pub_trimmed = pgp_trim(public_key_armored, &pub_len);
    if (pub_len == 0) {
        return pgp_fail(err, "Recipient public key must not be empty.");
    }
    sign_trimmed = pgp_trim(signing_private_key_armored, &sign_len);

    /* A passphrase only matters when a signing key was supplied. */
    if (!pgp_context(&ctx, home_dir,
                     (sign_len != 0 && passphrase != NULL && passphrase[0] != '\0')
                         ? passphrase
                         : NULL,
                     err)) {
        return false;
    }
    if (!pgp_import_key(ctx, pub_trimmed, pub_len, &recipients[0], err)) {
        goto done;
    }
    if (sign_len != 0) {
        if (!pgp_import_key(ctx, sign_trimmed, sign_len, &signing_key, err)) {
            goto done;
        }
        rc = gpgme_signers_add(ctx, signing_key);
        if (rc != 0) {
            pgp_fail(err, "Failed to select the signing key.");
            goto done;
        }
    }

    if (gpgme_data_new_from_mem(&plain, message, strlen(message), 0) != 0 ||
        gpgme_data_new(&cipher) != 0) {
        pgp_fail(err, "Out of memory.");
        goto done;
    }

    /* GPGME_ENCRYPT_ALWAYS_TRUST matches openpgp.js, which encrypts to whatever
     * key it was handed without consulting a web of trust. */
    rc = (sign_len != 0)
             ? gpgme_op_encrypt_sign(ctx, recipients, GPGME_ENCRYPT_ALWAYS_TRUST,
                                     plain, cipher)
             : gpgme_op_encrypt(ctx, recipients, GPGME_ENCRYPT_ALWAYS_TRUST,
                                plain, cipher);
    if (rc != 0) {
        pgp_fail(err, gpgme_strerror(rc));
        goto done;
    }

    *out_armored = pgp_data_to_string(cipher);
    cipher = NULL; /* released by pgp_data_to_string */
    if (*out_armored == NULL) {
        pgp_fail(err, "Out of memory.");
        goto done;
    }
    ok = true;

done:
    gpgme_data_release(plain);
    gpgme_data_release(cipher);
    gpgme_key_unref(recipients[0]);
    gpgme_key_unref(signing_key);
    gpgme_release(ctx);
    return ok;
}

/*
 * Decrypt an ASCII-armored PGP message with the recipient's private key.
 * Mirrors the TS pgpDecrypt(). The caller owns *out_plaintext.
 */
bool pgp_decrypt(const char *armored_message, const char *private_key_armored,
                 const char *passphrase, const char *home_dir,
                 char **out_plaintext, pgp_err *err)
{
    size_t msg_len, key_len;
    const char *msg_trimmed;
    const char *key_trimmed;
    gpgme_ctx_t ctx = NULL;
    gpgme_key_t key = NULL;
    gpgme_data_t cipher = NULL;
    gpgme_data_t plain = NULL;
    gpgme_error_t rc;
    bool ok = false;

    if (out_plaintext == NULL) {
        return pgp_fail(err, "Output parameter must not be NULL.");
    }
    *out_plaintext = NULL;

    msg_trimmed = pgp_trim(armored_message, &msg_len);
    if (msg_len == 0) {
        return pgp_fail(err, "Armored message must not be empty.");
    }
    key_trimmed = pgp_trim(private_key_armored, &key_len);
    if (key_len == 0) {
        return pgp_fail(err, "Private key must not be empty.");
    }

    if (!pgp_context(&ctx, home_dir,
                     (passphrase != NULL && passphrase[0] != '\0') ? passphrase : NULL,
                     err)) {
        return false;
    }
    if (!pgp_import_key(ctx, key_trimmed, key_len, &key, err)) {
        goto done;
    }
    if (gpgme_data_new_from_mem(&cipher, msg_trimmed, msg_len, 0) != 0 ||
        gpgme_data_new(&plain) != 0) {
        pgp_fail(err, "Out of memory.");
        goto done;
    }

    rc = gpgme_op_decrypt(ctx, cipher, plain);
    if (rc != 0) {
        pgp_fail(err, gpgme_strerror(rc));
        goto done;
    }

    *out_plaintext = pgp_data_to_string(plain);
    plain = NULL; /* released by pgp_data_to_string */
    if (*out_plaintext == NULL) {
        pgp_fail(err, "Out of memory.");
        goto done;
    }
    ok = true;

done:
    gpgme_data_release(cipher);
    gpgme_data_release(plain);
    gpgme_key_unref(key);
    gpgme_release(ctx);
    return ok;
}

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

int main(void)
{
    /* An ephemeral GNUPGHOME keeps the demo off the real keyring. Point these
     * at real armored key material to run the round trip. */
    const char *home = "/tmp/cosmodev-pgp-encrypt";
    const char *public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n"
                             "-----END PGP PUBLIC KEY BLOCK-----\n";
    pgp_key_info info;
    pgp_err err = {0};
    char *armored = NULL;

    if (!pgp_read_key_info("", home, &info, &err)) {
        printf("rejected as expected: %s\n", err.message);
    }
    if (pgp_read_key_info(public_key, home, &info, &err)) {
        printf("%s | %s | %s | created %s | expires %s\n",
               info.user_id, info.fingerprint, info.algorithm,
               info.creation_date,
               (info.expiry[0] != '\0') ? info.expiry : "never");
    } else {
        printf("%s\n", err.message);
    }

    if (pgp_encrypt("hello", public_key, NULL, NULL, home, &armored, &err)) {
        printf("%s\n", armored);
        free(armored);
    } else {
        printf("%s\n", err.message);
    }
    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 →