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-encryption — passphrase-based file encryption in the spirit of the age
* format (age-encryption.org/v1).
*
* Language: C (C11, standard library + OpenSSL 3.x libcrypto — C has no crypto
* in its standard library; libcrypto is the de-facto native choice)
* Source: CosmoDev polyglot showcase port of the Age File Encryption tool,
* ported from src/lib/age-encryption.ts (the canonical TypeScript
* implementation).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* age's passphrase mode wraps a file key with an scrypt-derived key; this port
* delivers the same security properties with the same primitives the TS
* reference uses via Web Crypto: PBKDF2-SHA256 (100k iterations) key
* stretching, a fresh random salt per encryption, and AES-256-GCM
* authenticated encryption.
*
* Wire format (age-style header + body), byte-identical to the TS reference:
* "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.
*
* Note on tag placement: Web Crypto appends the GCM tag to the ciphertext.
* OpenSSL hands it back separately, so this port appends it explicitly — the
* output is interchangeable with the TS implementation's.
*
* Build: cc -std=c11 age.c -lcrypto
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
/* ---------------------------------------------------------------- format --- */
#define AGE_HEADER "cosmodev-age-v1"
#define HEADER_LENGTH 15u /* strlen(AGE_HEADER) */
#define SALT_LENGTH 16u
#define IV_LENGTH 12u
#define TAG_LENGTH 16u
#define KEY_LENGTH 32u /* AES-256 */
#define ITERATIONS 100000
/* GCM appends a 16-byte auth tag, so the smallest possible encrypted payload
* is header + salt + IV + tag = 59 bytes. */
#define OVERHEAD (HEADER_LENGTH + SALT_LENGTH + IV_LENGTH + TAG_LENGTH)
/* Owning byte buffer. `data` is heap-allocated; free with age_buf_free(). */
typedef struct {
uint8_t *data;
size_t len;
} age_buf;
/* Result of an encrypt/decrypt call. On failure `ok` is false and `error`
* holds a caller-facing message (never a raw OpenSSL string — the TS
* reference deliberately collapses crypto failures into one message so a
* decrypt oracle cannot distinguish "wrong key" from "tampered"). */
typedef struct {
bool ok;
age_buf out;
char error[160];
} age_result;
void age_buf_free(age_buf *b)
{
if (b == NULL || b->data == NULL) {
return;
}
OPENSSL_cleanse(b->data, b->len);
free(b->data);
b->data = NULL;
b->len = 0;
}
static age_result age_fail(const char *msg)
{
age_result r = { .ok = false, .out = { NULL, 0 } };
snprintf(r.error, sizeof r.error, "%s", msg);
return r;
}
/* ------------------------------------------------------------- detection --- */
/* True when `data` starts with the cosmodev-age-v1 magic header. */
bool is_age_encrypted(const uint8_t *data, size_t len)
{
if (data == NULL || len < HEADER_LENGTH) {
return false;
}
return memcmp(data, AGE_HEADER, HEADER_LENGTH) == 0;
}
/* --------------------------------------------------------------- key kdf --- */
/* PBKDF2-SHA256(passphrase, salt, 100_000) -> 32-byte AES-256 key. */
static bool derive_key(const char *passphrase,
const uint8_t salt[SALT_LENGTH],
uint8_t key[KEY_LENGTH])
{
return PKCS5_PBKDF2_HMAC(passphrase, (int)strlen(passphrase),
salt, (int)SALT_LENGTH,
ITERATIONS, EVP_sha256(),
(int)KEY_LENGTH, key) == 1;
}
/* ------------------------------------------------------------- encrypting --- */
/*
* Encrypt `data` under `passphrase`.
* Returns header || salt || IV || ciphertext || tag.
*/
age_result age_encrypt(const uint8_t *data, size_t len, const char *passphrase)
{
if (passphrase == NULL || *passphrase == '\0') {
return age_fail("Passphrase must not be empty.");
}
if (len == 0) {
return age_fail("Input data is empty - nothing to encrypt.");
}
uint8_t salt[SALT_LENGTH];
uint8_t iv[IV_LENGTH];
if (RAND_bytes(salt, (int)SALT_LENGTH) != 1 ||
RAND_bytes(iv, (int)IV_LENGTH) != 1) {
return age_fail("Secure random number generator is unavailable.");
}
uint8_t key[KEY_LENGTH];
if (!derive_key(passphrase, salt, key)) {
return age_fail("Key derivation failed.");
}
/* header || salt || IV || ciphertext (== plaintext length for GCM) || tag */
size_t out_len = HEADER_LENGTH + SALT_LENGTH + IV_LENGTH + len + TAG_LENGTH;
uint8_t *out = malloc(out_len);
if (out == NULL) {
OPENSSL_cleanse(key, sizeof key);
return age_fail("Out of memory.");
}
memcpy(out, AGE_HEADER, HEADER_LENGTH);
memcpy(out + HEADER_LENGTH, salt, SALT_LENGTH);
memcpy(out + HEADER_LENGTH + SALT_LENGTH, iv, IV_LENGTH);
uint8_t *body = out + HEADER_LENGTH + SALT_LENGTH + IV_LENGTH;
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (ctx == NULL) {
free(out);
OPENSSL_cleanse(key, sizeof key);
return age_fail("Out of memory.");
}
bool ok = EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) == 1 &&
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, (int)IV_LENGTH, NULL) == 1 &&
EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv) == 1;
int written = 0;
int total = 0;
if (ok) {
ok = EVP_EncryptUpdate(ctx, body, &written, data, (int)len) == 1;
total += written;
}
if (ok) {
ok = EVP_EncryptFinal_ex(ctx, body + total, &written) == 1;
total += written;
}
if (ok) {
/* Append the tag so the payload matches Web Crypto's layout. */
ok = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG,
(int)TAG_LENGTH, body + total) == 1;
}
EVP_CIPHER_CTX_free(ctx);
OPENSSL_cleanse(key, sizeof key);
if (!ok || (size_t)total != len) {
OPENSSL_cleanse(out, out_len);
free(out);
return age_fail("Encryption failed.");
}
age_result r = { .ok = true, .out = { out, out_len } };
r.error[0] = '\0';
return r;
}
/* ------------------------------------------------------------- decrypting --- */
/*
* Decrypt a payload produced by age_encrypt(). Fails when the data lacks the
* cosmodev-age-v1 header, the passphrase is wrong, or the payload was
* corrupted/tampered (GCM auth-tag failure).
*/
age_result age_decrypt(const uint8_t *data, size_t len, const char *passphrase)
{
if (passphrase == NULL || *passphrase == '\0') {
return age_fail("Passphrase must not be empty.");
}
if (!is_age_encrypted(data, len)) {
return age_fail("Not an age-encrypted file (missing cosmodev-age-v1 header).");
}
if (len < OVERHEAD) {
age_result r = age_fail("");
snprintf(r.error, sizeof r.error,
"Input is too short to be an age-encrypted file (needs at least "
"%u bytes: header + salt + IV + auth tag).",
(unsigned)OVERHEAD);
return r;
}
const uint8_t *salt = data + HEADER_LENGTH;
const uint8_t *iv = data + HEADER_LENGTH + SALT_LENGTH;
const uint8_t *body = data + HEADER_LENGTH + SALT_LENGTH + IV_LENGTH;
/* The trailing TAG_LENGTH bytes of the body are the GCM tag. */
size_t ct_len = len - OVERHEAD;
const uint8_t *tag = body + ct_len;
uint8_t key[KEY_LENGTH];
if (!derive_key(passphrase, salt, key)) {
return age_fail("Key derivation failed.");
}
/* malloc(0) may return NULL; keep one spare byte so `out` is always valid. */
uint8_t *out = malloc(ct_len + 1);
if (out == NULL) {
OPENSSL_cleanse(key, sizeof key);
return age_fail("Out of memory.");
}
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (ctx == NULL) {
free(out);
OPENSSL_cleanse(key, sizeof key);
return age_fail("Out of memory.");
}
bool ok = EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) == 1 &&
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, (int)IV_LENGTH, NULL) == 1 &&
EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv) == 1;
int written = 0;
int total = 0;
if (ok && ct_len > 0) {
ok = EVP_DecryptUpdate(ctx, out, &written, body, (int)ct_len) == 1;
total += written;
}
if (ok) {
/* EVP_CTRL_GCM_SET_TAG takes a non-const pointer even though it only
* reads; the cast keeps our own API honestly const-correct. */
ok = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, (int)TAG_LENGTH,
(void *)(uintptr_t)tag) == 1;
}
if (ok) {
/* Final is where GCM verifies the tag: != 1 means wrong key or tamper. */
ok = EVP_DecryptFinal_ex(ctx, out + total, &written) == 1;
total += written;
}
EVP_CIPHER_CTX_free(ctx);
OPENSSL_cleanse(key, sizeof key);
if (!ok) {
OPENSSL_cleanse(out, ct_len + 1);
free(out);
/* One message for both causes — do not leak which check failed. */
return age_fail("Decryption failed: wrong passphrase or corrupted file.");
}
age_result r = { .ok = true, .out = { out, (size_t)total } };
r.error[0] = '\0';
return r;
}
/* -------------------------------------------------------------- demo main --- */
#ifdef AGE_DEMO
int main(void)
{
const char *msg = "attack at dawn";
const char *pass = "correct horse battery staple";
age_result enc = age_encrypt((const uint8_t *)msg, strlen(msg), pass);
if (!enc.ok) {
fprintf(stderr, "encrypt: %s\n", enc.error);
return 1;
}
printf("encrypted %zu bytes -> %zu bytes (age header: %s)\n",
strlen(msg), enc.out.len,
is_age_encrypted(enc.out.data, enc.out.len) ? "yes" : "no");
age_result dec = age_decrypt(enc.out.data, enc.out.len, pass);
if (!dec.ok) {
fprintf(stderr, "decrypt: %s\n", dec.error);
age_buf_free(&enc.out);
return 1;
}
printf("round-trip: %.*s\n", (int)dec.out.len, (const char *)dec.out.data);
age_result bad = age_decrypt(enc.out.data, enc.out.len, "wrong");
printf("wrong passphrase: %s\n", bad.ok ? "ACCEPTED (bug!)" : bad.error);
age_buf_free(&enc.out);
age_buf_free(&dec.out);
age_buf_free(&bad.out);
return 0;
}
#endif
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 →