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 →