Skip to content

File Encryptor — Zig source

Encrypt or decrypt any file with AES-256-GCM in your browser. Password-based, zero server contact. Drag, drop, done.

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

//! file-encryptor — password-based AES-256-GCM file encryption.
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/file-encryptor.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! Wire format:  salt (16 B) || IV (12 B) || AES-256-GCM ciphertext + tag.
//! The 256-bit key is derived from the password with PBKDF2-SHA256 and a fresh
//! random salt per encryption, so the same data + password never encrypts to
//! the same bytes, and the password itself is never stored or derivable from
//! the output.
//!
//! The TS reference leans on the Web Crypto API; Zig's std.crypto provides the
//! same primitives natively (pbkdf2, Aes256Gcm, a CSPRNG), so this port keeps
//! the identical wire format without any third-party dependency.

const std = @import("std");

pub const salt_length: usize = 16;
pub const iv_length: usize = 12;
pub const iterations: u32 = 100_000;

/// GCM appends a 16-byte auth tag to the ciphertext; the smallest possible
/// encrypted payload is therefore salt + IV + tag = 44 bytes.
const tag_length: usize = 16;
pub const min_length: usize = salt_length + iv_length + tag_length;

const Aes256Gcm = std.crypto.aead.aes_gcm.Aes256Gcm;
const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256;

pub const Error = error{
    EmptyPassword,
    EmptyInput,
    InputTooShort,
    WrongPasswordOrCorrupted,
};

/// Derive an AES-256 key from `password` + `salt` with PBKDF2-SHA256.
fn deriveKey(password: []const u8, salt: [salt_length]u8) [32]u8 {
    var key: [32]u8 = undefined;
    std.crypto.pwhash.pbkdf2(&key, password, &salt, iterations, HmacSha256) catch
        unreachable; // fixed-size output can only fail on weak parameters
    return key;
}

/// Encrypt `data` under `password`. Returns salt || IV || ciphertext+tag.
/// Caller owns the returned slice.
pub fn encryptFile(
    allocator: std.mem.Allocator,
    data: []const u8,
    password: []const u8,
) Error![]u8 {
    if (password.len == 0) return Error.EmptyPassword;
    if (data.len == 0) return Error.EmptyInput;

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

    const key = deriveKey(password, salt);

    const out = allocator.alloc(u8, salt_length + iv_length + data.len + tag_length) catch
        return Error.WrongPasswordOrCorrupted; // allocation failure surface
    @memcpy(out[0..salt_length], &salt);
    @memcpy(out[salt_length .. salt_length + iv_length], &iv);

    // AES-256-GCM in-place: ciphertext follows the header, tag at the end.
    var tag: [tag_length]u8 = undefined;
    Aes256Gcm.encrypt(
        out[salt_length + iv_length ..][0..data.len],
        &tag,
        data,
        "",      // no associated data
        iv,      // npub
        key,
    ) catch return Error.WrongPasswordOrCorrupted;
    @memcpy(out[out.len - tag_length ..], &tag);
    return out;
}

/// Decrypt a payload produced by `encryptFile`. Fails when the password is
/// wrong or the payload was corrupted/tampered (GCM auth-tag failure).
/// Caller owns the returned slice.
pub fn decryptFile(
    allocator: std.mem.Allocator,
    data: []const u8,
    password: []const u8,
) Error![]u8 {
    if (password.len == 0) return Error.EmptyPassword;
    if (data.len < min_length) return Error.InputTooShort;

    var salt: [salt_length]u8 = undefined;
    @memcpy(&salt, data[0..salt_length]);
    var iv: [iv_length]u8 = undefined;
    @memcpy(&iv, data[salt_length .. salt_length + iv_length]);

    const ciphertext = data[salt_length + iv_length .. data.len - tag_length];
    var tag: [tag_length]u8 = undefined;
    @memcpy(&tag, data[data.len - tag_length ..]);

    const key = deriveKey(password, salt);

    const plain = allocator.alloc(u8, ciphertext.len) catch
        return Error.WrongPasswordOrCorrupted; // allocation failure surface
    errdefer allocator.free(plain);

    // A GCM auth-tag failure means the key did not match (wrong password) or
    // the payload was modified after encryption.
    Aes256Gcm.decrypt(plain, ciphertext, tag, "", iv, key) catch
        return Error.WrongPasswordOrCorrupted;
    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 →