Skip to content

Age File Encryption — C++ source

Encrypt and decrypt files with age — a modern, simple alternative to PGP. Password-based encryption runs entirely in your browser.

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

// Age File Encryption — passphrase-based file encryption in the spirit of the
// age format (age-encryption.org/v1).
//
// Language: C++17 (standard library + OpenSSL EVP)
// Ported from src/lib/age-encryption.ts (the canonical TypeScript
// implementation). display source — part of CosmoDev's polyglot tool pages.
//
// age's passphrase mode wraps a file key with an scrypt-derived key; this
// implementation delivers the same security properties with OpenSSL
// primitives: PBKDF2-SHA256 (100k iterations) key stretching, a fresh random
// salt per encryption, and AES-256-GCM authenticated encryption. Nothing
// leaves the process.
//
// Wire format (age-style header + body):
//   "cosmodev-age-v1" (15 B ASCII magic) || salt (16 B) || IV (12 B)
//   || AES-256-GCM ciphertext + tag (16 B)
// The header makes the format self-describing and detectable; the 256-bit key
// is derived from the passphrase, so the same file + passphrase never encrypts
// to the same bytes and the passphrase is never derivable from the output.

#include <array>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <vector>

#include <openssl/evp.h>
#include <openssl/rand.h>

namespace age {

using Bytes = std::vector<uint8_t>;

constexpr const char* AGE_HEADER = "cosmodev-age-v1";
constexpr size_t SALT_LENGTH = 16;
constexpr size_t IV_LENGTH = 12;
constexpr size_t ITERATIONS = 100000;

constexpr size_t HEADER_LENGTH = 15; // strlen(AGE_HEADER)

/// GCM appends a 16-byte auth tag to the ciphertext; the smallest possible
/// encrypted payload is therefore header + salt + IV + tag = 59 bytes.
constexpr size_t TAG_LENGTH = 16;
constexpr size_t OVERHEAD = HEADER_LENGTH + SALT_LENGTH + IV_LENGTH + TAG_LENGTH;

static const std::array<uint8_t, HEADER_LENGTH>& headerBytes() {
  static const std::array<uint8_t, HEADER_LENGTH> HEADER = {'c', 'o', 's', 'm', 'o', 'd', 'e', 'v',
                                                            '-', 'a', 'g', 'e', '-', 'v', '1'};
  return HEADER;
}

/** True when `data` starts with the cosmodev-age-v1 magic header. */
bool isAgeEncrypted(const Bytes& data) {
  if (data.size() < HEADER_LENGTH) return false;
  const auto& header = headerBytes();
  for (size_t i = 0; i < HEADER_LENGTH; i++) {
    if (data[i] != header[i]) return false;
  }
  return true;
}

/** PBKDF2-SHA256(100k) -> 32-byte AES-256 key. */
static Bytes deriveKey(const std::string& passphrase, const uint8_t* salt, size_t saltLen) {
  Bytes key(32);
  if (PKCS5_PBKDF2_HMAC(passphrase.data(), static_cast<int>(passphrase.size()), salt,
                        static_cast<int>(saltLen), static_cast<int>(ITERATIONS),
                        EVP_sha256(), static_cast<int>(key.size()), key.data()) != 1) {
    throw std::runtime_error("PBKDF2 key derivation failed.");
  }
  return key;
}

/** AES-256-GCM seal (ciphertext || 16-byte tag appended, the Web Crypto shape). */
static Bytes gcmEncrypt(const Bytes& key, const uint8_t* iv, const uint8_t* data, size_t len) {
  EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
  if (ctx == nullptr) throw std::runtime_error("OpenSSL context allocation failed.");

  Bytes out(len + TAG_LENGTH);
  int outLen = 0;
  int total = 0;
  bool ok = EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) == 1 &&
            EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, static_cast<int>(IV_LENGTH), nullptr) == 1 &&
            EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), iv) == 1 &&
            (len == 0 || EVP_EncryptUpdate(ctx, out.data(), &outLen, data, static_cast<int>(len)) == 1) &&
            ([&] { total = outLen; return EVP_EncryptFinal_ex(ctx, out.data() + total, &outLen) == 1; }()) &&
            ([&] {
              total += outLen;
              return EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, static_cast<int>(TAG_LENGTH),
                                         out.data() + total) == 1;
            }());
  EVP_CIPHER_CTX_free(ctx);
  if (!ok) throw std::runtime_error("AES-256-GCM encryption failed.");
  total += static_cast<int>(TAG_LENGTH);
  out.resize(static_cast<size_t>(total));
  return out;
}

/** AES-256-GCM open. Throws on auth-tag failure (wrong key or tampered data). */
static Bytes gcmDecrypt(const Bytes& key, const uint8_t* iv, const uint8_t* data, size_t len) {
  if (len < TAG_LENGTH) throw std::runtime_error("Ciphertext is shorter than the GCM tag.");
  EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
  if (ctx == nullptr) throw std::runtime_error("OpenSSL context allocation failed.");

  const size_t bodyLen = len - TAG_LENGTH;
  Bytes out(bodyLen);
  int outLen = 0;
  int total = 0;
  uint8_t tag[TAG_LENGTH];
  std::copy_n(data + bodyLen, TAG_LENGTH, tag);

  bool ok = EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) == 1 &&
            EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, static_cast<int>(IV_LENGTH), nullptr) == 1 &&
            EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), iv) == 1 &&
            (bodyLen == 0 || EVP_DecryptUpdate(ctx, out.data(), &outLen, data, static_cast<int>(bodyLen)) == 1) &&
            ([&] { total = outLen; return true; }()) &&
            EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, static_cast<int>(TAG_LENGTH), tag) == 1 &&
            ([&] { return EVP_DecryptFinal_ex(ctx, out.data() + total, &outLen) == 1; }());
  EVP_CIPHER_CTX_free(ctx);
  if (!ok) throw std::runtime_error("GCM authentication failed.");
  total += outLen;
  out.resize(static_cast<size_t>(total));
  return out;
}

static Bytes randomBytes(size_t n) {
  Bytes out(n);
  if (RAND_bytes(out.data(), static_cast<int>(n)) != 1) {
    throw std::runtime_error("The system CSPRNG is unavailable.");
  }
  return out;
}

/** Encrypt `data` under `passphrase`. Returns header || salt || IV || ciphertext+tag. */
Bytes ageEncrypt(const Bytes& data, const std::string& passphrase) {
  if (passphrase.empty()) throw std::runtime_error("Passphrase must not be empty.");
  if (data.empty()) throw std::runtime_error("Input data is empty - nothing to encrypt.");

  const Bytes salt = randomBytes(SALT_LENGTH);
  const Bytes iv = randomBytes(IV_LENGTH);
  const Bytes key = deriveKey(passphrase, salt.data(), salt.size());
  const Bytes ciphertext = gcmEncrypt(key, iv.data(), data.data(), data.size());

  Bytes out;
  out.reserve(OVERHEAD + data.size());
  const auto& header = headerBytes();
  out.insert(out.end(), header.begin(), header.end());
  out.insert(out.end(), salt.begin(), salt.end());
  out.insert(out.end(), iv.begin(), iv.end());
  out.insert(out.end(), ciphertext.begin(), ciphertext.end());
  return out;
}

/**
 * Decrypt a payload produced by `ageEncrypt`. Throws when the data lacks the
 * cosmodev-age-v1 header, the passphrase is wrong, or the payload was
 * corrupted/tampered (GCM auth-tag failure).
 */
Bytes ageDecrypt(const Bytes& data, const std::string& passphrase) {
  if (passphrase.empty()) throw std::runtime_error("Passphrase must not be empty.");
  if (!isAgeEncrypted(data)) {
    throw std::runtime_error("Not an age-encrypted file (missing cosmodev-age-v1 header).");
  }
  if (data.size() < OVERHEAD) {
    throw std::runtime_error("Input is too short to be an age-encrypted file (needs at least " +
                             std::to_string(OVERHEAD) + " bytes: header + salt + IV + auth tag).");
  }
  const uint8_t* salt = data.data() + HEADER_LENGTH;
  const uint8_t* iv = data.data() + HEADER_LENGTH + SALT_LENGTH;
  const uint8_t* ciphertext = data.data() + HEADER_LENGTH + SALT_LENGTH + IV_LENGTH;
  const size_t ciphertextLen = data.size() - (HEADER_LENGTH + SALT_LENGTH + IV_LENGTH);
  const Bytes key = deriveKey(passphrase, salt, SALT_LENGTH);
  try {
    return gcmDecrypt(key, iv, ciphertext, ciphertextLen);
  } catch (const std::runtime_error&) {
    // A GCM auth-tag failure means the key did not match (wrong passphrase) or
    // the payload was modified after encryption.
    throw std::runtime_error("Decryption failed: wrong passphrase or corrupted file.");
  }
}

} // namespace age

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 →