Skip to content

Text Encryptor — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! text-encryptor — password-based text encryption (AES-256-GCM + Base64).
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/text-encryptor.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! Format: PBKDF2 (SHA-256, 100k 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 (+ GCM tag), so every
//! encryption is unique and self-contained — decrypt needs only the string
//! and the password.
//!
//! The TS reference hand-rolls the Base64 codec (no btoa); Zig's std has a
//! standard Base64 encoder/decoder that serves the same canonical alphabet,
//! so this port uses it and keeps the identical packed layout.

const std = @import("std");

const pbkdf2_iterations: u32 = 100_000;
const salt_bytes: usize = 16;
const iv_bytes: usize = 12;
/// Smallest valid packed payload: salt + IV + one AES-GCM block (tag).
const min_bytes: usize = salt_bytes + iv_bytes + 16;

const Aes256Gcm = std.crypto.aead.aes_gcm.Aes256Gcm;
const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256;
const B64Encoder = std.base64.standard.Encoder;
const B64Decoder = std.base64.standard.Decoder;

pub const Error = error{
    NothingToEncrypt,
    NothingToDecrypt,
    EmptyPassword,
    InvalidBase64,
    PayloadTooShort,
    DecryptionFailed,
    OutOfMemory,
};

/// PBKDF2-SHA256 (100k iterations) -> AES-256-GCM key.
fn deriveKey(password: []const u8, salt: [salt_bytes]u8) [32]u8 {
    var key: [32]u8 = undefined;
    std.crypto.pwhash.pbkdf2(&key, password, &salt, pbkdf2_iterations, HmacSha256) catch
        unreachable; // fixed-size output can only fail on weak parameters
    return key;
}

/// Encrypt UTF-8 `plaintext` under `password` and return the Base64 string
/// packing salt + IV + AES-256-GCM ciphertext. Fails if either input is empty.
/// Caller owns the returned slice.
pub fn encryptText(
    allocator: std.mem.Allocator,
    plaintext: []const u8,
    password: []const u8,
) Error![]u8 {
    if (plaintext.len == 0) return Error.NothingToEncrypt;
    if (password.len == 0) return Error.EmptyPassword;

    var salt: [salt_bytes]u8 = undefined;
    var iv: [iv_bytes]u8 = undefined;
    std.crypto.random.bytes(&salt);
    std.crypto.random.bytes(&iv);

    const key = deriveKey(password, salt);

    // packed = salt || IV || ciphertext || tag
    const packed_len = salt_bytes + iv_bytes + plaintext.len + Aes256Gcm.tag_length;
    const packed = try allocator.alloc(u8, packed_len);
    defer allocator.free(packed);
    @memcpy(packed[0..salt_bytes], &salt);
    @memcpy(packed[salt_bytes .. salt_bytes + iv_bytes], &iv);

    var tag: [Aes256Gcm.tag_length]u8 = undefined;
    Aes256Gcm.encrypt(
        packed[salt_bytes + iv_bytes ..][0..plaintext.len],
        &tag,
        plaintext,
        "", // no associated data
        iv,
        key,
    ) catch return Error.DecryptionFailed;
    @memcpy(packed[packed_len - Aes256Gcm.tag_length ..], &tag);

    const b64 = try allocator.alloc(u8, B64Encoder.calcSize(packed_len));
    _ = B64Encoder.encode(b64, packed);
    return b64;
}

/// Decrypt a Base64 string produced by `encryptText` back to plaintext.
/// Fails on empty input/password, invalid Base64, a payload too short to be
/// one of ours, or a GCM authentication failure (wrong password / tampered
/// data). Caller owns the returned slice.
pub fn decryptText(
    allocator: std.mem.Allocator,
    ciphertext_b64: []const u8,
    password: []const u8,
) Error![]u8 {
    const trimmed = std.mem.trim(u8, ciphertext_b64, " \t\r\n");
    if (trimmed.len == 0) return Error.NothingToDecrypt;
    if (password.len == 0) return Error.EmptyPassword;

    const decoded_len = B64Decoder.calcSizeForSlice(trimmed) catch return Error.InvalidBase64;
    const packed = try allocator.alloc(u8, decoded_len);
    defer allocator.free(packed);
    B64Decoder.decode(packed, trimmed) catch return Error.InvalidBase64;

    if (packed.len < min_bytes) return Error.PayloadTooShort;

    var salt: [salt_bytes]u8 = undefined;
    @memcpy(&salt, packed[0..salt_bytes]);
    var iv: [iv_bytes]u8 = undefined;
    @memcpy(&iv, packed[salt_bytes .. salt_bytes + iv_bytes]);

    const ct = packed[salt_bytes + iv_bytes .. packed.len - Aes256Gcm.tag_length];
    var tag: [Aes256Gcm.tag_length]u8 = undefined;
    @memcpy(&tag, packed[packed.len - Aes256Gcm.tag_length ..]);

    const key = deriveKey(password, salt);

    const plain = try allocator.alloc(u8, ct.len);
    errdefer allocator.free(plain);
    Aes256Gcm.decrypt(plain, ct, tag, "", iv, key) catch return Error.DecryptionFailed;
    return plain;
}

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 →