Skip to content

Text Encryptor — C source

Encrypt or decrypt text with a password using AES-256-GCM. Share the ciphertext safely - only someone with the password can read it.

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

/*
 * text-encryptor — password-based AES-256-GCM encryption of UTF-8 text, packed
 *                  into a single self-contained Base64 string.
 *
 * 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 Text Encryptor tool, ported
 *           from src/lib/text-encryptor.ts (the canonical TypeScript
 *           implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * Format: PBKDF2 (SHA-256, 100 000 iterations, 16-byte random salt) derives an
 * AES-256 key from the password; AES-GCM encrypts with a 12-byte random IV.
 * The Base64 output packs salt || IV || ciphertext || tag, so every encryption
 * is unique and self-contained — decrypt needs only the string and the password.
 *
 * The Base64 codec is written out by hand rather than pulled from a library,
 * mirroring the TS reference's own pure codec, so the wire format is provably
 * identical and the file has no dependency beyond libcrypto.
 *
 * Build: cc -std=c11 text-encryptor.c -lcrypto
 */

#include <ctype.h>
#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 {
    PBKDF2_ITERATIONS = 100000,
    SALT_BYTES = 16,
    IV_BYTES   = 12,
    TAG_BYTES  = 16,
    KEY_BYTES  = 32,
    /* Smallest valid packed payload: salt + IV + one AES-GCM block (tag). */
    MIN_BYTES  = SALT_BYTES + IV_BYTES + 16
};

static const char ALPHABET[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/* Every entry point writes a message here and returns NULL/false instead of
 * aborting — the C analogue of the TS `throw new Error(...)`. */
typedef struct {
    char message[128];
} te_err;

static void *te_fail(te_err *err, const char *msg)
{
    if (err != NULL) {
        snprintf(err->message, sizeof err->message, "%s", msg);
    }
    return NULL;
}

/* ------------------------------------------------------------ base64 codec --- */

/* Bytes -> canonical Base64. Returns a malloc'd NUL-terminated string. */
char *bytes_to_base64(const uint8_t *bytes, size_t len)
{
    size_t groups = (len + 2) / 3;
    char *out = malloc(groups * 4 + 1);
    size_t o = 0;

    if (out == NULL) {
        return NULL;
    }
    for (size_t i = 0; i < len; i += 3) {
        int b0 = bytes[i];
        bool has1 = i + 1 < len;
        bool has2 = i + 2 < len;
        int b1 = has1 ? bytes[i + 1] : 0;
        int b2 = has2 ? bytes[i + 2] : 0;

        out[o++] = ALPHABET[b0 >> 2];
        out[o++] = ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
        out[o++] = has1 ? ALPHABET[((b1 & 0x0F) << 2) | (b2 >> 6)] : '=';
        out[o++] = has2 ? ALPHABET[b2 & 0x3F] : '=';
    }
    out[o] = '\0';
    return out;
}

/* Index of `c` in the Base64 alphabet, or -1. */
static int alphabet_index(char c)
{
    const char *p = memchr(ALPHABET, c, 64);
    return (p != NULL) ? (int) (p - ALPHABET) : -1;
}

/*
 * Base64 -> bytes. Accepts surrounding whitespace; fails on any other
 * non-alphabet character, wrong length, or misplaced padding. On success
 * returns a malloc'd buffer and writes its length to *out_len.
 */
uint8_t *base64_to_bytes(const char *b64, size_t *out_len, te_err *err)
{
    size_t raw_len = (b64 != NULL) ? strlen(b64) : 0;
    char *clean = malloc(raw_len + 1);
    size_t n = 0, out_length, p = 0;
    uint8_t *bytes = NULL;

    if (clean == NULL) {
        return te_fail(err, "Out of memory.");
    }
    /* Strip all whitespace, as the TS reference's /\s+/g replace does. */
    for (size_t i = 0; i < raw_len; i++) {
        if (!isspace((unsigned char) b64[i])) {
            clean[n++] = b64[i];
        }
    }
    clean[n] = '\0';

    if (n == 0) {
        free(clean);
        return te_fail(err, "Invalid Base64: input is empty");
    }
    if (n % 4 != 0) {
        free(clean);
        return te_fail(err, "Invalid Base64: length must be a multiple of 4");
    }

    /* Padding is legal only as the last one or two characters. */
    for (size_t i = 0; i < n; i++) {
        char c = clean[i];
        bool is_pad_slot = (i >= n - 2);

        if (c == '=') {
            if (!is_pad_slot) {
                free(clean);
                return te_fail(err, "Invalid Base64: unexpected character \"=\"");
            }
            /* '=' may not be followed by a non-'=' character. */
            if (i + 1 < n && clean[i + 1] != '=') {
                free(clean);
                return te_fail(err, "Invalid Base64: unexpected character \"=\"");
            }
        } else if (alphabet_index(c) < 0) {
            char msg[128];
            snprintf(msg, sizeof msg, "Invalid Base64: unexpected character \"%c\"", c);
            free(clean);
            return te_fail(err, msg);
        }
    }

    out_length = (n / 4) * 3;
    if (n >= 2 && clean[n - 1] == '=' && clean[n - 2] == '=') {
        out_length -= 2;
    } else if (clean[n - 1] == '=') {
        out_length -= 1;
    }

    bytes = malloc(out_length > 0 ? out_length : 1);
    if (bytes == NULL) {
        free(clean);
        return te_fail(err, "Out of memory.");
    }

    for (size_t i = 0; i < n; i += 4) {
        int c0 = alphabet_index(clean[i]);
        int c1 = alphabet_index(clean[i + 1]);
        int c2 = (clean[i + 2] == '=') ? -1 : alphabet_index(clean[i + 2]);
        int c3 = (clean[i + 3] == '=') ? -1 : alphabet_index(clean[i + 3]);

        if (c0 < 0 || c1 < 0) {
            free(clean);
            free(bytes);
            return te_fail(err, "Invalid Base64: malformed quantum");
        }
        if (p < out_length) bytes[p++] = (uint8_t) ((c0 << 2) | (c1 >> 4));
        if (c2 != -1 && p < out_length) bytes[p++] = (uint8_t) (((c1 & 0x0F) << 4) | (c2 >> 2));
        if (c2 != -1 && c3 != -1 && p < out_length) bytes[p++] = (uint8_t) (((c2 & 0x03) << 6) | c3);
    }

    free(clean);
    *out_len = out_length;
    return bytes;
}

/* ------------------------------------------------------------ key material --- */

/* PBKDF2-SHA256 (100 000 iterations) -> 32-byte AES-256-GCM key. */
static bool derive_key(const char *password, const uint8_t *salt, uint8_t out[KEY_BYTES])
{
    return PKCS5_PBKDF2_HMAC(password, (int) strlen(password),
                             salt, SALT_BYTES,
                             PBKDF2_ITERATIONS, EVP_sha256(),
                             KEY_BYTES, out) == 1;
}

/* ---------------------------------------------------------------- encrypt --- */

/*
 * Encrypt UTF-8 `plaintext` under `password`; returns a malloc'd Base64 string
 * packing salt + IV + AES-256-GCM ciphertext + tag. NULL on failure.
 */
char *encrypt_text(const char *plaintext, const char *password, te_err *err)
{
    EVP_CIPHER_CTX *ctx = NULL;
    uint8_t key[KEY_BYTES];
    uint8_t *packed = NULL;
    char *encoded = NULL;
    size_t plain_len;
    int part = 0, written = 0;

    if (plaintext == NULL || plaintext[0] == '\0') {
        return te_fail(err, "Nothing to encrypt: input is empty");
    }
    if (password == NULL || password[0] == '\0') {
        return te_fail(err, "Password must not be empty");
    }
    plain_len = strlen(plaintext);

    packed = malloc(SALT_BYTES + IV_BYTES + plain_len + TAG_BYTES);
    if (packed == NULL) {
        return te_fail(err, "Out of memory.");
    }

    if (RAND_bytes(packed, SALT_BYTES) != 1 ||
        RAND_bytes(packed + SALT_BYTES, IV_BYTES) != 1) {
        te_fail(err, "Secure random source is not available.");
        goto done;
    }
    if (!derive_key(password, packed, key)) {
        te_fail(err, "Key derivation failed.");
        goto done;
    }

    ctx = EVP_CIPHER_CTX_new();
    if (ctx == NULL) {
        te_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_BYTES, NULL) != 1 ||
        EVP_EncryptInit_ex(ctx, NULL, NULL, key, packed + SALT_BYTES) != 1 ||
        EVP_EncryptUpdate(ctx, packed + SALT_BYTES + IV_BYTES, &part,
                          (const uint8_t *) plaintext, (int) plain_len) != 1) {
        te_fail(err, "Encryption failed.");
        goto done;
    }
    written = part;
    if (EVP_EncryptFinal_ex(ctx, packed + SALT_BYTES + IV_BYTES + written, &part) != 1) {
        te_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_BYTES,
                            packed + SALT_BYTES + IV_BYTES + written) != 1) {
        te_fail(err, "Encryption failed to produce an authentication tag.");
        goto done;
    }

    encoded = bytes_to_base64(packed, SALT_BYTES + IV_BYTES + (size_t) written + TAG_BYTES);
    if (encoded == NULL) {
        te_fail(err, "Out of memory.");
    }

done:
    EVP_CIPHER_CTX_free(ctx);
    OPENSSL_cleanse(key, sizeof key);
    free(packed);
    return encoded;
}

/* ---------------------------------------------------------------- decrypt --- */

/*
 * Decrypt a Base64 string produced by encrypt_text() back to plaintext.
 * Returns a malloc'd NUL-terminated string, or NULL on empty input/password,
 * invalid Base64, a payload too short to be one of ours, or a GCM
 * authentication failure (wrong password / tampered data).
 */
char *decrypt_text(const char *ciphertext, const char *password, te_err *err)
{
    EVP_CIPHER_CTX *ctx = NULL;
    uint8_t key[KEY_BYTES];
    uint8_t *packed = NULL;
    char *plain = NULL;
    size_t packed_len = 0, body_len;
    const char *p;
    int part = 0, written = 0;

    if (ciphertext == NULL) {
        return te_fail(err, "Nothing to decrypt: input is empty");
    }
    /* Reject input that is empty once trimmed, as the TS `.trim() === ''` does. */
    for (p = ciphertext; *p != '\0' && isspace((unsigned char) *p); p++) {
        /* skip leading whitespace */
    }
    if (*p == '\0') {
        return te_fail(err, "Nothing to decrypt: input is empty");
    }
    if (password == NULL || password[0] == '\0') {
        return te_fail(err, "Password must not be empty");
    }

    /* base64_to_bytes() already ignores surrounding whitespace. */
    packed = base64_to_bytes(ciphertext, &packed_len, err);
    if (packed == NULL) {
        return NULL;
    }
    if (packed_len < (size_t) MIN_BYTES) {
        free(packed);
        return te_fail(err, "Ciphertext too short - not a valid salt + IV + AES-GCM payload");
    }

    body_len = packed_len - SALT_BYTES - IV_BYTES - TAG_BYTES;
    plain = malloc(body_len + 1);
    if (plain == NULL) {
        free(packed);
        return te_fail(err, "Out of memory.");
    }

    if (!derive_key(password, packed, key)) {
        te_fail(err, "Key derivation failed.");
        goto fail;
    }

    ctx = EVP_CIPHER_CTX_new();
    if (ctx == NULL) {
        te_fail(err, "Out of memory.");
        goto fail;
    }
    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 ||
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, IV_BYTES, NULL) != 1 ||
        EVP_DecryptInit_ex(ctx, NULL, NULL, key, packed + SALT_BYTES) != 1) {
        te_fail(err, "Decryption failed - wrong password or corrupted ciphertext");
        goto fail;
    }
    if (body_len > 0 &&
        EVP_DecryptUpdate(ctx, (uint8_t *) plain, &part,
                          packed + SALT_BYTES + IV_BYTES, (int) body_len) != 1) {
        te_fail(err, "Decryption failed - wrong password or corrupted ciphertext");
        goto fail;
    }
    written = part;

    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, TAG_BYTES,
                            packed + packed_len - TAG_BYTES) != 1 ||
        EVP_DecryptFinal_ex(ctx, (uint8_t *) plain + written, &part) <= 0) {
        /* A tag mismatch means the key did not match (wrong password) or the
         * ciphertext was modified after encryption. */
        te_fail(err, "Decryption failed - wrong password or corrupted ciphertext");
        goto fail;
    }
    written += part;
    plain[written] = '\0';

    EVP_CIPHER_CTX_free(ctx);
    OPENSSL_cleanse(key, sizeof key);
    free(packed);
    return plain;

fail:
    EVP_CIPHER_CTX_free(ctx);
    OPENSSL_cleanse(key, sizeof key);
    free(packed);
    free(plain);
    return NULL;
}

/* -------------------------------------------------------------------- demo --- */

int main(void)
{
    const char *password = "correct horse battery staple";
    const char *message  = "Meet me at the observatory at midnight.";
    te_err err = {0};
    char *sealed, *opened;

    sealed = encrypt_text(message, password, &err);
    if (sealed == NULL) {
        fprintf(stderr, "encrypt: %s\n", err.message);
        return EXIT_FAILURE;
    }
    printf("plaintext:  %s\n", message);
    printf("encrypted:  %s\n", sealed);

    opened = decrypt_text(sealed, password, &err);
    if (opened == NULL) {
        fprintf(stderr, "decrypt: %s\n", err.message);
        free(sealed);
        return EXIT_FAILURE;
    }
    printf("round-trip: %s\n", opened);

    /* A wrong password must fail the tag check rather than return garbage. */
    if (decrypt_text(sealed, "hunter2", &err) == NULL) {
        printf("wrong password: %s\n", err.message);
    }

    free(sealed);
    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 →