File Encryptor — C source
Encrypt or decrypt any file with AES-256-GCM in your browser. Password-based, zero server contact. Drag, drop, done.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/*
* file-encryptor — password-based AES-256-GCM file encryption.
*
* 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 File Encryptor tool, ported
* from src/lib/file-encryptor.ts (the canonical TypeScript
* implementation).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* Wire format, byte-identical to the TS reference:
* salt (16 B) || IV (12 B) || AES-256-GCM ciphertext || tag (16 B)
*
* The 256-bit key is derived from the password with PBKDF2-SHA256 (100 000
* iterations) and a fresh random salt per encryption, so the same file +
* password never encrypts to the same bytes, and the password itself is never
* stored in or 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 file-encryptor.c -lcrypto
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
/* --------------------------------------------------------------- constants --- */
enum {
SALT_LENGTH = 16,
IV_LENGTH = 12,
TAG_LENGTH = 16,
KEY_LENGTH = 32, /* AES-256 */
/* GCM appends a 16-byte auth tag; the smallest possible encrypted payload
* is therefore salt + IV + tag = 44 bytes. */
MIN_LENGTH = SALT_LENGTH + IV_LENGTH + TAG_LENGTH
};
static const int ITERATIONS = 100000;
/* A heap buffer plus its length. `data` is NULL only when `len` is 0. */
typedef struct {
uint8_t *data;
size_t len;
} fe_buf;
/* Error reporting mirrors the TS `throw new Error(...)` messages: every entry
* point writes one into `err` and returns false rather than aborting. */
typedef struct {
char message[160];
} fe_err;
static bool fe_fail(fe_err *err, const char *msg)
{
if (err != NULL) {
snprintf(err->message, sizeof err->message, "%s", msg);
}
return false;
}
void fe_buf_free(fe_buf *buf)
{
if (buf == NULL || buf->data == NULL) {
return;
}
/* Plaintext and keys may have passed through here — wipe before release. */
OPENSSL_cleanse(buf->data, buf->len);
free(buf->data);
buf->data = NULL;
buf->len = 0;
}
/* ------------------------------------------------------------ key material --- */
/* PBKDF2-SHA256(password, salt, 100 000) -> 32-byte AES key. */
static bool derive_key(const char *password, const uint8_t *salt, uint8_t out[KEY_LENGTH])
{
return PKCS5_PBKDF2_HMAC(password, (int) strlen(password),
salt, SALT_LENGTH,
ITERATIONS, EVP_sha256(),
KEY_LENGTH, out) == 1;
}
/* ---------------------------------------------------------------- encrypt --- */
/*
* Encrypt `data` under `password`. On success `out` owns salt || IV ||
* ciphertext || tag and the caller must fe_buf_free() it.
*/
bool encrypt_file(const uint8_t *data, size_t data_len,
const char *password,
fe_buf *out, fe_err *err)
{
EVP_CIPHER_CTX *ctx = NULL;
uint8_t key[KEY_LENGTH];
uint8_t *buf = NULL;
size_t total;
int part = 0, written = 0;
bool ok = false;
if (out == NULL) {
return fe_fail(err, "Output buffer must not be NULL.");
}
out->data = NULL;
out->len = 0;
if (password == NULL || password[0] == '\0') {
return fe_fail(err, "Password must not be empty.");
}
if (data_len == 0) {
return fe_fail(err, "Input data is empty - nothing to encrypt.");
}
total = SALT_LENGTH + IV_LENGTH + data_len + TAG_LENGTH;
buf = malloc(total);
if (buf == NULL) {
return fe_fail(err, "Out of memory.");
}
/* Fresh salt and IV per encryption — never reuse an (key, IV) pair. */
if (RAND_bytes(buf, SALT_LENGTH) != 1 ||
RAND_bytes(buf + SALT_LENGTH, IV_LENGTH) != 1) {
fe_fail(err, "Secure random source is not available.");
goto done;
}
if (!derive_key(password, buf, key)) {
fe_fail(err, "Key derivation failed.");
goto done;
}
ctx = EVP_CIPHER_CTX_new();
if (ctx == NULL) {
fe_fail(err, "Out of memory.");
goto done;
}
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 ||
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, IV_LENGTH, NULL) != 1 ||
EVP_EncryptInit_ex(ctx, NULL, NULL, key, buf + SALT_LENGTH) != 1) {
fe_fail(err, "Encryption failed to initialise.");
goto done;
}
if (EVP_EncryptUpdate(ctx, buf + SALT_LENGTH + IV_LENGTH, &part,
data, (int) data_len) != 1) {
fe_fail(err, "Encryption failed.");
goto done;
}
written = part;
if (EVP_EncryptFinal_ex(ctx, buf + SALT_LENGTH + IV_LENGTH + written, &part) != 1) {
fe_fail(err, "Encryption failed.");
goto done;
}
written += part;
/* Append the tag so the layout matches Web Crypto's ciphertext||tag. */
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, TAG_LENGTH,
buf + SALT_LENGTH + IV_LENGTH + written) != 1) {
fe_fail(err, "Encryption failed to produce an authentication tag.");
goto done;
}
out->data = buf;
out->len = SALT_LENGTH + IV_LENGTH + (size_t) written + TAG_LENGTH;
buf = NULL;
ok = true;
done:
EVP_CIPHER_CTX_free(ctx);
OPENSSL_cleanse(key, sizeof key);
free(buf);
return ok;
}
/* ---------------------------------------------------------------- decrypt --- */
/*
* Decrypt a payload produced by encrypt_file(). Fails when the password is
* wrong or the payload was corrupted/tampered (GCM auth-tag failure).
*/
bool decrypt_file(const uint8_t *data, size_t data_len,
const char *password,
fe_buf *out, fe_err *err)
{
EVP_CIPHER_CTX *ctx = NULL;
uint8_t key[KEY_LENGTH];
uint8_t *buf = NULL;
size_t cipher_len;
int part = 0, written = 0;
bool ok = false;
if (out == NULL) {
return fe_fail(err, "Output buffer must not be NULL.");
}
out->data = NULL;
out->len = 0;
if (password == NULL || password[0] == '\0') {
return fe_fail(err, "Password must not be empty.");
}
if (data_len < (size_t) MIN_LENGTH) {
char msg[160];
snprintf(msg, sizeof msg,
"Input is too short to be an encrypted file "
"(needs at least %d bytes: salt + IV + auth tag).",
MIN_LENGTH);
return fe_fail(err, msg);
}
/* data_len >= MIN_LENGTH, so this cannot underflow. */
cipher_len = data_len - SALT_LENGTH - IV_LENGTH - TAG_LENGTH;
/* malloc(0) may legitimately return NULL; ask for at least one byte so a
* zero-length plaintext is not mistaken for an allocation failure. */
buf = malloc(cipher_len > 0 ? cipher_len : 1);
if (buf == NULL) {
return fe_fail(err, "Out of memory.");
}
if (!derive_key(password, data, key)) {
fe_fail(err, "Key derivation failed.");
goto done;
}
ctx = EVP_CIPHER_CTX_new();
if (ctx == NULL) {
fe_fail(err, "Out of memory.");
goto done;
}
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 ||
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, IV_LENGTH, NULL) != 1 ||
EVP_DecryptInit_ex(ctx, NULL, NULL, key, data + SALT_LENGTH) != 1) {
fe_fail(err, "Decryption failed: wrong password or corrupted file.");
goto done;
}
if (cipher_len > 0 &&
EVP_DecryptUpdate(ctx, buf, &part,
data + SALT_LENGTH + IV_LENGTH, (int) cipher_len) != 1) {
fe_fail(err, "Decryption failed: wrong password or corrupted file.");
goto done;
}
written = part;
/* The tag sits at the very end of the payload, as Web Crypto writes it. */
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, TAG_LENGTH,
(void *) (data + data_len - TAG_LENGTH)) != 1) {
fe_fail(err, "Decryption failed: wrong password or corrupted file.");
goto done;
}
/* Non-positive return = the tag did not verify: the key did not match
* (wrong password) or the payload was modified after encryption. */
if (EVP_DecryptFinal_ex(ctx, buf + written, &part) <= 0) {
fe_fail(err, "Decryption failed: wrong password or corrupted file.");
goto done;
}
written += part;
out->data = buf;
out->len = (size_t) written;
buf = NULL;
ok = true;
done:
EVP_CIPHER_CTX_free(ctx);
OPENSSL_cleanse(key, sizeof key);
if (buf != NULL) {
OPENSSL_cleanse(buf, cipher_len);
free(buf);
}
return ok;
}
/* -------------------------------------------------------------------- demo --- */
int main(void)
{
const char *password = "correct horse battery staple";
const char *message = "Attack at dawn.";
fe_buf sealed = {0}, opened = {0};
fe_err err = {0};
if (!encrypt_file((const uint8_t *) message, strlen(message), password, &sealed, &err)) {
fprintf(stderr, "encrypt: %s\n", err.message);
return EXIT_FAILURE;
}
printf("plaintext: %zu bytes\n", strlen(message));
printf("ciphertext: %zu bytes (salt %d + IV %d + body + tag %d)\n",
sealed.len, SALT_LENGTH, IV_LENGTH, TAG_LENGTH);
if (!decrypt_file(sealed.data, sealed.len, password, &opened, &err)) {
fprintf(stderr, "decrypt: %s\n", err.message);
fe_buf_free(&sealed);
return EXIT_FAILURE;
}
printf("round-trip: %.*s\n", (int) opened.len, (const char *) opened.data);
/* A wrong password must fail the tag check rather than return garbage. */
fe_buf wrong = {0};
if (decrypt_file(sealed.data, sealed.len, "hunter2", &wrong, &err)) {
fprintf(stderr, "wrong password unexpectedly succeeded\n");
fe_buf_free(&wrong);
fe_buf_free(&sealed);
fe_buf_free(&opened);
return EXIT_FAILURE;
}
printf("wrong password: %s\n", err.message);
fe_buf_free(&sealed);
fe_buf_free(&opened);
return EXIT_SUCCESS;
}
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 →