Skip to content

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++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-keygen.ts (the canonical TypeScript implementation).
// 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 empty GNUPGHOME (gpg writes the revocation certificate it
// generates into <GNUPGHOME>/openpgp-revocs.d/, which we re-read and armor),
// mirroring the TS island's guarantee that generation happens client-side.
//
// Build: c++ -std=c++17 pgp-keygen.cpp $(gpgme-config --cflags --libs)

#include <cctype>
#include <fstream>
#include <memory>
#include <regex>
#include <stdexcept>
#include <string>
#include <vector>

#include <gpgme.h>

namespace pgpkeygen {

/** Algorithm choices: ECC Curve25519 (default), RSA-2048, or RSA-4096. */
enum class PGPKeyGenAlgorithm { ECC, RSA2048, RSA4096 };

struct PGPKeyGenOptions {
  std::string name; // goes into the key's user ID packet
  std::string email; // goes into the key's user ID packet
  std::string passphrase; // optional; the private key is encrypted with it
  PGPKeyGenAlgorithm algorithm = PGPKeyGenAlgorithm::ECC;
};

struct PGPKeyPair {
  std::string publicKey; // ASCII-armored public key (BEGIN PGP PUBLIC KEY BLOCK)
  std::string privateKey; // ASCII-armored private key (BEGIN PGP PRIVATE KEY BLOCK)
  std::string fingerprint; // 40 hex chars (v4), lowercase
  std::string revocationCertificate; // ASCII-armored revocation certificate
};

// ── validation ─────────────────────────────────────────────────────────────

static std::string trim(const std::string& s) {
  const size_t first = s.find_first_not_of(" \t\r\n");
  if (first == std::string::npos) return "";
  const size_t last = s.find_last_not_of(" \t\r\n");
  return s.substr(first, last - first + 1);
}

/** Accepts foo@bar.tld-style addresses: one @, non-empty local + domain, a dot in the domain. */
static bool validEmail(const std::string& email) {
  static const std::regex EMAIL_RE(R"(^[^\s@]+@[^\s@]+\.[^\s@]+$)");
  return std::regex_match(email, EMAIL_RE);
}

/** Validate the identity that goes into the key's user ID. Throws on invalid input. */
void validateKeyGenIdentity(const std::string& name, const std::string& email) {
  if (trim(name).empty()) throw std::runtime_error("Name is required.");
  if (trim(email).empty()) throw std::runtime_error("Email is required.");
  if (!validEmail(trim(email))) throw std::runtime_error("Invalid email address.");
}

// ── GPGME 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>;

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));
  }
}

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);
}

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);
}

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

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

/**
 * Export one key (by fingerprint) as ASCII armor. `secret` selects the
 * private-key export mode.
 */
static std::string exportKey(gpgme_ctx_t ctx, const std::string& fpr, bool secret) {
  const Data out = dataForOutput();
  gpgme_export_mode_t mode = secret
                                 ? static_cast<gpgme_export_mode_t>(GPGME_EXPORT_MODE_SECRET)
                                 : static_cast<gpgme_export_mode_t>(0);
  check(gpgme_op_export(ctx, fpr.c_str(), mode, out.get()), "key export failed");
  const std::string armored = dataToString(out.get());
  if (armored.empty()) throw std::runtime_error("key export produced no data");
  return armored;
}

/** Read gpg's generated revocation certificate for `fpr` from the GNUPGHOME
 *  and re-armor it via an import/export round-trip through GPGME. */
static std::string revocationCertificate(gpgme_ctx_t ctx, const std::string& homeDir,
                                         const std::string& fpr) {
  std::string upper;
  for (char c : fpr) upper += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
  const std::string path = homeDir + "/openpgp-revocs.d/" + upper + ".rev";
  std::ifstream file(path, std::ios::binary);
  if (!file) throw std::runtime_error("Revocation certificate not found at " + path);
  const std::string raw((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
  if (raw.empty()) throw std::runtime_error("Revocation certificate is empty.");

  const Data importData = dataFromString(raw);
  // A lone revocation signature packet imports as an update to the public key.
  check(gpgme_op_import(ctx, importData.get()), "revocation import failed");
  return exportKey(ctx, fpr, false);
}

// ── generation ─────────────────────────────────────────────────────────────

/**
 * Generate an ASCII-armored PGP key pair. ECC (Curve25519) is fast; RSA-4096
 * can take a few seconds. `homeDir` must be an empty ephemeral directory.
 */
PGPKeyPair generatePGPKeyPair(const PGPKeyGenOptions& options, const std::string& homeDir) {
  validateKeyGenIdentity(options.name, options.email);

  if (gpgme_check_version(nullptr) == nullptr) {
    throw std::runtime_error("GPGME is not available.");
  }
  gpgme_ctx_t rawCtx = nullptr;
  check(gpgme_new(&rawCtx), "gpgme_new failed");
  Context ctx(rawCtx);
  check(gpgme_set_protocol(ctx.get(), GPGME_PROTOCOL_OpenPGP), "protocol selection failed");
  gpgme_set_armor(ctx.get(), 1);
  gpgme_ctx_set_engine_info(ctx.get(), GPGME_PROTOCOL_OpenPGP, nullptr, homeDir.c_str());

  // An empty-string passphrase would still encrypt the key; only a real
  // passphrase should.
  Passphrase pass{options.passphrase, false};
  if (!options.passphrase.empty()) {
    gpgme_set_passphrase_cb(ctx.get(), passphraseCb, &pass);
  }

  // Algorithm selector: "future-default" is GPGME's ECC Curve25519/Ed25519
  // profile (the TS reference's type 'curve25519'); RSA uses explicit bit sizes.
  const char* algo = options.algorithm == PGPKeyGenAlgorithm::ECC      ? "future-default"
                     : options.algorithm == PGPKeyGenAlgorithm::RSA4096 ? "rsa4096"
                                                                        : "rsa2048";
  const unsigned flags =
      GPGME_CREATE_SIGN | GPGME_CREATE_ENC |
      (options.passphrase.empty() ? GPGME_CREATE_NOPASSWD : 0u);

  const std::string userID = trim(options.name) + " <" + trim(options.email) + ">";
  check(gpgme_op_createkey(ctx.get(), userID.c_str(), algo, 0 /*reserved*/, 0 /*never expires*/,
                           static_cast<gpgme_keygen_mode_t>(0), flags),
        "key generation failed");

  const gpgme_genkey_result_t result = gpgme_op_genkey_result(ctx.get());
  if (result == nullptr || result->primary_fpr == nullptr) {
    throw std::runtime_error("key generation produced no fingerprint");
  }
  const std::string fpr = result->primary_fpr;

  return {
      exportKey(ctx.get(), fpr, false),
      exportKey(ctx.get(), fpr, true),
      fpr,
      revocationCertificate(ctx.get(), homeDir, fpr),
  };
}

} // namespace pgpkeygen

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 →