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 →