Skip to content

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 →