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 →