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 →