Skip to content

Argon2 Hash & Verify — Zig source

Hash passwords with Argon2id — the winner of the Password Hashing Competition. Configure memory, iterations, and parallelism. WASM-powered, client-side.

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

//! argon2 — Argon2id password hashing (PHC string format).
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/argon2.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! The TS reference drives the reference C implementation compiled to WASM
//! (argon2-browser). Zig's std.crypto ships the same Argon2 v1.3 natively
//! (`std.crypto.pwhash.argon2`), so this port uses it directly - same
//! algorithm, same PHC output format, no external dependency.
//!
//! PHC string format (what `encoded` holds - the string you store in a DB):
//!   $argon2id$v=19$m=65536,t=3,p=1$<b64 salt>$<b64 digest>
//! Salt and digest are unpadded standard Base64.
//!
//! Defaults follow the OWASP-recommended Argon2id profile (64 MiB, 3 passes).

const std = @import("std");

/// The native Argon2 v1.3 implementation from std.crypto.
const argon2 = std.crypto.pwhash.argon2;
const B64Encoder = std.base64.standard_no_pad.Encoder;
const B64Decoder = std.base64.standard_no_pad.Decoder;

/// Defaults follow the OWASP-recommended Argon2id profile (64 MiB, 3 passes).
pub const argon2_defaults = struct {
    pub const memory: u32 = 65_536; // KiB
    pub const iterations: u32 = 3;
    pub const parallelism: u3 = 1;
    pub const hash_length: usize = 32;
};

/// Random salt size in bytes (128 bits - the PHC recommendation).
pub const salt_bytes: usize = 16;

/// Argon2 variant ids as the C library encodes them.
pub const Argon2Type = enum(u8) {
    argon2d = 0,
    argon2i = 1,
    argon2id = 2,

    pub fn name(self: Argon2Type) []const u8 {
        return switch (self) {
            .argon2d => "argon2d",
            .argon2i => "argon2i",
            .argon2id => "argon2id",
        };
    }
};

pub const Argon2Options = struct {
    /// Memory cost in KiB (default 65536 = 64 MiB). Must be >= 1024.
    memory: u32 = argon2_defaults.memory,
    /// Time cost - passes over memory (default 3). Must be >= 1.
    iterations: u32 = argon2_defaults.iterations,
    /// Parallelism - lanes (default 1). Must be >= 1.
    parallelism: u3 = argon2_defaults.parallelism,
    /// Digest length in bytes (default 32). Must be 16..64.
    hash_length: usize = argon2_defaults.hash_length,
    /// Explicit salt bytes; a random 16-byte salt is generated when null.
    salt: ?[salt_bytes]u8 = null,
};

pub const Argon2Result = struct {
    /// Raw digest, lowercase hex (hash_length bytes). Caller-owned.
    hash: []u8,
    /// Self-contained PHC string - store this, verify against it. Caller-owned.
    encoded: []u8,
    /// Salt used, lowercase hex (16 bytes). Caller-owned.
    salt: []u8,
};

/// Parameters extracted from a PHC string (parseArgon2's return type).
pub const Argon2Params = struct {
    type: Argon2Type,
    version: u16,
    memory: u32,
    iterations: u32,
    parallelism: u32,
    /// Salt, decoded from the embedded Base64. Caller-owned.
    salt: []u8,
    /// Digest, decoded from the embedded Base64. Caller-owned ('' if absent).
    hash: []u8,
};

pub const Error = error{
    InvalidArgon2String,
    MemoryTooLow,
    IterationsTooLow,
    ParallelismTooLow,
    HashLengthOutOfRange,
    OutOfMemory,
};

/// Unpadded standard Base64 -> bytes (the PHC encoding).
fn phcBase64ToBytes(allocator: std.mem.Allocator, b64: []const u8) Error![]u8 {
    const len = B64Decoder.calcSizeForSlice(b64) catch return Error.InvalidArgon2String;
    const out = allocator.alloc(u8, len) catch return Error.OutOfMemory;
    B64Decoder.decode(out, b64) catch return Error.InvalidArgon2String;
    return out;
}

fn bytesToHex(allocator: std.mem.Allocator, bytes: []const u8) Error![]u8 {
    const out = allocator.alloc(u8, bytes.len * 2) catch return Error.OutOfMemory;
    const hex = std.fmt.bytesToHex(bytes, .lower);
    if (out.len != hex.len) return Error.OutOfMemory;
    @memcpy(out, &hex);
    return out;
}

/// Parse a PHC-format Argon2 string into its parameters. Fails on any
/// malformed input. Caller owns `salt` and `hash`.
pub fn parseArgon2(allocator: std.mem.Allocator, encoded: []const u8) Error!Argon2Params {
    const s = std.mem.trim(u8, encoded, " \t\r\n");
    // $argon2id$v=19$m=65536,t=3,p=1$SALT[$HASH]
    if (s.len == 0 or s[0] != '$') return Error.InvalidArgon2String;
    var it = std.mem.splitScalar(u8, s[1..], '$');
    const type_str = it.next() orelse return Error.InvalidArgon2String;
    const ty: Argon2Type = if (std.mem.eql(u8, type_str, "argon2d"))
        .argon2d
    else if (std.mem.eql(u8, type_str, "argon2i"))
        .argon2i
    else if (std.mem.eql(u8, type_str, "argon2id"))
        .argon2id
    else
        return Error.InvalidArgon2String;

    const version_field = it.next() orelse return Error.InvalidArgon2String;
    if (!std.mem.startsWith(u8, version_field, "v=")) return Error.InvalidArgon2String;
    const version = std.fmt.parseInt(u16, version_field[2..], 10) catch
        return Error.InvalidArgon2String;

    const mtp = it.next() orelse return Error.InvalidArgon2String;
    // m=…,t=…,p=…
    var pit = std.mem.splitScalar(u8, mtp, ',');
    const m_field = pit.next() orelse return Error.InvalidArgon2String;
    const t_field = pit.next() orelse return Error.InvalidArgon2String;
    const p_field = pit.next() orelse return Error.InvalidArgon2String;
    if (!std.mem.startsWith(u8, m_field, "m=") or
        !std.mem.startsWith(u8, t_field, "t=") or
        !std.mem.startsWith(u8, p_field, "p=")) return Error.InvalidArgon2String;
    const memory = std.fmt.parseInt(u32, m_field[2..], 10) catch return Error.InvalidArgon2String;
    const iterations = std.fmt.parseInt(u32, t_field[2..], 10) catch return Error.InvalidArgon2String;
    const parallelism = std.fmt.parseInt(u32, p_field[2..], 10) catch return Error.InvalidArgon2String;

    const salt_b64 = it.next() orelse return Error.InvalidArgon2String;
    const salt = try phcBase64ToBytes(allocator, salt_b64);
    errdefer allocator.free(salt);
    const hash_b64 = it.next();
    const hash = if (hash_b64) |h| try phcBase64ToBytes(allocator, h) else try allocator.dupe(u8, "");

    return .{
        .type = ty,
        .version = version,
        .memory = memory,
        .iterations = iterations,
        .parallelism = parallelism,
        .salt = salt,
        .hash = hash,
    };
}

/// Validate + normalise hashing parameters, failing with a clear error.
fn normalizeOptions(options: Argon2Options) Error!Argon2Options {
    if (options.memory < 1024) return Error.MemoryTooLow;
    if (options.iterations < 1) return Error.IterationsTooLow;
    if (options.parallelism < 1) return Error.ParallelismTooLow;
    if (options.hash_length < 16 or options.hash_length > 64)
        return Error.HashLengthOutOfRange;
    return options;
}

/// Hash a password with Argon2id (hybrid of Argon2i's side-channel resistance
/// and Argon2d's GPU resistance - the Password Hashing Competition winner and
/// the recommended mode for password storage). Returns the digest (hex), the
/// salt used (hex), and the self-contained PHC string. A fresh random 16-byte
/// salt is generated per call unless `options.salt` is given.
/// Caller owns all three returned buffers.
pub fn argon2Hash(
    allocator: std.mem.Allocator,
    password: []const u8,
    options_in: Argon2Options,
) (Error || argon2.Error || std.mem.Allocator.Error)!Argon2Result {
    const options = try normalizeOptions(options_in);
    const salt = options.salt orelse blk: {
        var s: [salt_bytes]u8 = undefined;
        std.crypto.random.bytes(&s);
        break :blk s;
    };

    // Raw Argon2id digest via the native std.crypto implementation.
    const digest = try allocator.alloc(u8, options.hash_length);
    errdefer allocator.free(digest);
    try argon2.kdf(digest, password, &salt, .{
        .allocator = allocator,
        .params = .{ .t = options.iterations, .m = options.memory, .p = options.parallelism },
        .encoding = .phc,
        .version = 0x13, // Argon2 version 1.3 (v=19)
    });

    const hash_hex = try bytesToHex(allocator, digest);
    errdefer allocator.free(hash_hex);
    const salt_hex = try bytesToHex(allocator, &salt);
    errdefer allocator.free(salt_hex);

    // $argon2id$v=19$m=…,t=…,p=…$<b64 salt>$<b64 digest>
    const salt_b64 = try allocator.alloc(u8, B64Encoder.calcSize(salt.len));
    defer allocator.free(salt_b64);
    _ = B64Encoder.encode(salt_b64, &salt);
    const digest_b64 = try allocator.alloc(u8, B64Encoder.calcSize(digest.len));
    defer allocator.free(digest_b64);
    _ = B64Encoder.encode(digest_b64, digest);

    const encoded = try std.fmt.allocPrint(
        allocator,
        "$argon2id$v=19$m={d},t={d},p={d}${s}${s}",
        .{ options.memory, options.iterations, options.parallelism, salt_b64, digest_b64 },
    );

    return .{ .hash = hash_hex, .encoded = encoded, .salt = salt_hex };
}

/// Verify a password against a PHC-format encoded hash (as produced by
/// argon2Hash). Returns true on match, false on mismatch; fails only on a
/// malformed encoded string or a runtime error. Any Argon2 type (d/i/id) is
/// accepted - the type is read from the string itself.
pub fn argon2Verify(
    allocator: std.mem.Allocator,
    encoded: []const u8,
    password: []const u8,
) (Error || std.mem.Allocator.Error)!bool {
    var params = parseArgon2(allocator, encoded) catch |e| switch (e) {
        Error.OutOfMemory => return Error.OutOfMemory,
        else => return e, // malformed encoded string
    };
    defer allocator.free(params.salt);
    defer allocator.free(params.hash);

    argon2.strVerify(encoded, password, .{ .allocator = allocator }) catch |e| switch (e) {
        error.PasswordVerificationFailed => return false,
        else => return e, // runtime error (weak parameters, decode failure, ...)
    };
    return true;
}

Also available in 9 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 →