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++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 →