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 →