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++17 (standard library + GPGME 1.x — the GnuPG project's official
// C binding, the native counterpart to the TS reference's openpgp.js)
// Ported from src/lib/pgp-encrypt.ts (the canonical TypeScript implementation).
// display source — part of CosmoDev's polyglot tool pages.
//
// Three entry points, matching the TS public API one-for-one:
// readKeyInfo() — user ID, uppercase fingerprint, algorithm, creation date,
// expiry (nullopt when the key never expires), and whether
// the armor holds a private key.
// pgpEncrypt() — ASCII-armored PGP message for a recipient's public key,
// optionally signed with the sender's private key.
// pgpDecrypt() — plaintext from an armored message + private key.
//
// Armored key material is imported into a caller-supplied ephemeral GNUPGHOME
// (set before the context is created), so nothing is written to the user's
// real keyring — the C++ equivalent of the TS island keeping every operation
// client-side.
//
// Build: c++ -std=c++17 pgp-encrypt.cpp $(gpgme-config --cflags --libs)
#include <cctype>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
#include <gpgme.h>
namespace pgpencrypt {
struct PGPKeyInfo {
std::string userID;
std::string fingerprint;
std::string algorithm;
std::string creationDate; // ISO date "YYYY-MM-DD"
std::optional<std::string> expiry; // nullopt = never expires
bool isPrivate = false;
};
// ── RAII + error plumbing ──────────────────────────────────────────────────
struct Deleter {
void operator()(gpgme_ctx_t ctx) const { gpgme_release(ctx); }
void operator()(gpgme_data_t data) const { gpgme_data_release(data); }
};
using Context = std::unique_ptr<gpgme_ctx_t, Deleter>;
using Data = std::unique_ptr<gpgme_data_t, Deleter>;
/** Throw with the GPGME error text for a non-zero status. */
static void check(gpgme_error_t err, const std::string& what) {
if (gpgme_err_code(err) != GPG_ERR_NO_ERROR) {
throw std::runtime_error(what + ": " + gpgme_strerror(err));
}
}
/** Read a gpgme_data_t fully into a string. */
static std::string dataToString(gpgme_data_t data) {
static const size_t CHUNK = 4096;
std::string out;
std::vector<char> buffer(CHUNK);
size_t read = 0;
while ((read = gpgme_data_read(data, buffer.data(), CHUNK)) > 0) {
out.append(buffer.data(), read);
}
return out;
}
/** gpgme_data_t over an in-memory string. */
static Data dataFromString(const std::string& text) {
gpgme_data_t raw = nullptr;
if (gpgme_data_new_from_mem(&raw, text.data(), text.size(), 1 /* copy */) != GPG_ERR_NO_ERROR) {
throw std::runtime_error("GPGME data buffer allocation failed.");
}
return Data(raw);
}
/** Empty writable gpgme_data_t for output. */
static Data dataForOutput() {
gpgme_data_t raw = nullptr;
if (gpgme_data_new(&raw) != GPG_ERR_NO_ERROR) {
throw std::runtime_error("GPGME data buffer allocation failed.");
}
return Data(raw);
}
/** ISO date "YYYY-MM-DD" from a unix timestamp (0 → nullopt). */
static std::optional<std::string> isoDate(long timestamp) {
if (timestamp <= 0) return std::nullopt;
std::tm tm{};
const time_t tt = static_cast<time_t>(timestamp);
gmtime_r(&tt, &tm);
char buf[16];
std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm);
return std::string(buf);
}
// ── context ────────────────────────────────────────────────────────────────
/**
* OpenPGP context with ASCII armor on. Call setHomeDir() with an ephemeral
* directory before any import so nothing touches the real keyring.
*/
class Gpg {
public:
Gpg() {
if (gpgme_check_version(nullptr) == nullptr) {
throw std::runtime_error("GPGME is not available.");
}
gpgme_ctx_t raw = nullptr;
check(gpgme_new(&raw), "gpgme_new failed");
ctx_.reset(raw);
check(gpgme_set_protocol(ctx_.get(), GPGME_PROTOCOL_OpenPGP), "protocol selection failed");
gpgme_set_armor(ctx_.get(), 1);
}
gpgme_ctx_t get() const { return ctx_.get(); }
/** Point the crypto engine at `dir` (an empty ephemeral GNUPGHOME). */
void setHomeDir(const std::string& dir) { gpgme_ctx_set_engine_info(ctx_.get(), GPGME_PROTOCOL_OpenPGP, nullptr, dir.c_str()); }
/** Import armored key material; returns the primary fingerprint. */
std::string import(const std::string& armored) {
const Data keyData = dataFromString(armored);
check(gpgme_op_import(ctx_.get(), keyData.get()), "key import failed");
const gpgme_import_result_t result = gpgme_op_import_result(ctx_.get());
if (result == nullptr || result->imports == nullptr) {
throw std::runtime_error("Invalid PGP key: unrecognized key format");
}
return result->imports->fpr;
}
/** Look up one key by fingerprint (secret=true for the private key). */
gpgme_key_t findKey(const std::string& fpr, bool secret) {
check(gpgme_op_keylist_start(ctx_.get(), fpr.c_str(), secret ? 1 : 0), "key lookup failed");
gpgme_key_t key = nullptr;
const gpgme_error_t err = gpgme_op_keylist_next(ctx_.get(), &key);
gpgme_op_keylist_end(ctx_.get());
if (gpgme_err_code(err) != GPG_ERR_NO_ERROR || key == nullptr) {
if (key != nullptr) gpgme_key_unref(key);
throw std::runtime_error("Invalid PGP key: imported key not found");
}
return key; // released in releaseKey() — the context owns the listing
}
static void releaseKey(gpgme_key_t key) { gpgme_key_unref(key); }
private:
Context ctx_;
};
/** Adapt Gpg for use as a scoped key handle. */
struct KeyDeleter {
void operator()(gpgme_key_t key) const { gpgme_key_unref(key); }
};
using Key = std::unique_ptr<std::remove_pointer<gpgme_key_t>::type, KeyDeleter>;
// ── passphrase handling ────────────────────────────────────────────────────
/** A passphrase handed to GPGME through its callback protocol. */
struct Passphrase {
std::string value;
bool used = false;
};
static gpgme_error_t passphraseCb(void* hook, const char* /*uid_hint*/, const char* /*passphrase_info*/,
int /*last_was_bad*/, int fd) {
auto* pass = static_cast<Passphrase*>(hook);
if (!pass->used) {
gpgme_io_writen(fd, pass->value.data(), pass->value.size());
pass->used = true;
}
gpgme_io_writen(fd, "\n", 1);
return 0;
}
// ── public API ─────────────────────────────────────────────────────────────
/** Detect key type from the armor header. */
static bool armorHoldsPrivateKey(const std::string& armored) {
return armored.find("-----BEGIN PGP PRIVATE KEY BLOCK-----") != std::string::npos ||
armored.find("-----BEGIN PGP SECRET KEY BLOCK-----") != std::string::npos;
}
/**
* Read a PGP key (public or private) and extract its metadata.
* Throws on invalid or unrecognized key material. `homeDir` must be an empty
* ephemeral directory (see the file header).
*/
PGPKeyInfo readKeyInfo(const std::string& armoredKey, const std::string& homeDir) {
if (armoredKey.empty()) throw std::runtime_error("Key input is empty or invalid.");
const bool isPrivateKey = armorHoldsPrivateKey(armoredKey);
try {
Gpg gpg;
gpg.setHomeDir(homeDir);
const std::string fpr = gpg.import(armoredKey);
// Import a private key as secret=true so the listing finds it.
const Key key(gpg.findKey(fpr, isPrivateKey));
const gpgme_subkey_t primary = key->subkeys;
if (primary == nullptr) throw std::runtime_error("Invalid PGP key: no key material");
PGPKeyInfo info;
info.userID = key->uids != nullptr && key->uids->uid != nullptr ? key->uids->uid : "unknown";
info.fingerprint = primary->fpr != nullptr ? primary->fpr : "";
for (char& c : info.fingerprint) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
const char* algoName = gpgme_pubkey_algo_name(primary->pubkey_algo);
info.algorithm = algoName != nullptr ? algoName : "unknown";
info.creationDate = isoDate(primary->timestamp).value_or("");
info.expiry = isoDate(primary->expires); // 0 → nullopt = never expires
info.isPrivate = isPrivateKey;
return info;
} catch (const std::runtime_error& e) {
throw std::runtime_error(std::string("Invalid PGP key: ") + e.what());
}
}
/**
* Encrypt a plaintext message for a recipient's public key.
* Returns an ASCII-armored PGP message. Optionally signs with the sender's
* private key (signingPrivateKeyArmored + passphrase).
*/
std::string pgpEncrypt(const std::string& message, const std::string& publicKeyArmored,
const std::string& signingPrivateKeyArmored = "",
const std::string& passphrase = "", const std::string& homeDir = "") {
if (message.empty()) throw std::runtime_error("Message must not be empty.");
if (publicKeyArmored.empty()) throw std::runtime_error("Recipient public key must not be empty.");
Gpg gpg;
gpg.setHomeDir(homeDir);
// Import the recipient public key and trust it for this one operation.
const std::string recipientFpr = gpg.import(publicKeyArmored);
gpgme_key_t recipientKeys[2] = {nullptr, nullptr};
const Key recipient(gpg.findKey(recipientFpr, false));
recipientKeys[0] = recipient.get();
// Optional signer: import + unlock the private key with the passphrase.
Key signer;
Passphrase pass{passphrase, false};
const bool signing = !signingPrivateKeyArmored.empty();
if (signing) {
signer.reset(gpg.findKey(gpg.import(signingPrivateKeyArmored), true));
if (!passphrase.empty()) {
gpgme_set_passphrase_cb(gpg.get(), passphraseCb, &pass);
}
}
const Data plain = dataFromString(message);
const Data cipher = dataForOutput();
gpgme_error_t err;
if (signing) {
gpgme_signers_clear(gpg.get());
gpgme_signers_add(gpg.get(), signer.get());
err = gpgme_op_encrypt_sign(gpg.get(), recipientKeys,
static_cast<gpgme_encrypt_flags_t>(GPGME_ENCRYPT_ALWAYS_TRUST),
plain.get(), cipher.get());
} else {
err = gpgme_op_encrypt(gpg.get(), recipientKeys,
static_cast<gpgme_encrypt_flags_t>(GPGME_ENCRYPT_ALWAYS_TRUST),
plain.get(), cipher.get());
}
check(err, "encryption failed");
return dataToString(cipher.get());
}
/**
* Decrypt an ASCII-armored PGP message with the recipient's private key.
* Returns the plaintext message.
*/
std::string pgpDecrypt(const std::string& armoredMessage, const std::string& privateKeyArmored,
const std::string& passphrase = "", const std::string& homeDir = "") {
if (armoredMessage.empty()) throw std::runtime_error("Armored message must not be empty.");
if (privateKeyArmored.empty()) throw std::runtime_error("Private key must not be empty.");
Gpg gpg;
gpg.setHomeDir(homeDir);
gpg.import(privateKeyArmored);
if (!passphrase.empty()) {
static thread_local Passphrase pass;
pass = Passphrase{passphrase, false};
gpgme_set_passphrase_cb(gpg.get(), passphraseCb, &pass);
}
const Data cipher = dataFromString(armoredMessage);
const Data plain = dataForOutput();
check(gpgme_op_decrypt(gpg.get(), cipher.get(), plain.get()), "decryption failed");
return dataToString(plain.get());
}
} // namespace pgpencrypt
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 →