⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.2122+3e15e99e6What's newOn an older Zig?

Crypto

const std = @import("std");
const expect = std.testing.expect;

test "sha256" {
    var digest: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash("abc", &digest, .{});

    // The well-known SHA-256 of "abc".
    var hex: [64]u8 = undefined;
    const text = try std.mem.print(&hex, "{x}", .{digest});
    try expect(std.mem.startsWith(u8, text, "ba7816bf"));
}

test "incremental hashing" {
    var hasher = std.crypto.hash.sha2.Sha256.init(.{});
    hasher.update("a");
    hasher.update("bc");

    var digest: [32]u8 = undefined;
    hasher.final(&digest);

    var one_shot: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash("abc", &one_shot, .{});
    try expect(std.mem.eql(u8, &digest, &one_shot));
}

test "compare secrets in constant time" {
    const a = [_]u8{ 1, 2, 3 };
    const b = [_]u8{ 1, 2, 3 };
    // `std.mem.eql` short-circuits, which leaks how much matched via timing.
    try expect(std.crypto.timing_safe.eql([3]u8, a, b));
}

test "hmac" {
    const Hmac = std.crypto.auth.hmac.sha2.HmacSha256;
    var mac: [Hmac.mac_length]u8 = undefined;
    Hmac.create(&mac, "message", "key");
    try expect(mac.len == 32);
}

test "password hashing is deliberately slow" {
    // bcrypt/scrypt/argon2 are the right tools for passwords; a bare hash
    // is not. They are omitted from this runnable example precisely because
    // they are designed to take a long time.
    try expect(@hasDecl(std.crypto.pwhash, "argon2"));
}

Zig ships a substantial crypto library in std.crypto (hashes, AEADs, key exchange, signatures) with no external dependency.

That last part matters more than it sounds. In most languages, doing anything cryptographic means linking OpenSSL or an equivalent, which is a build dependency, a version to track, and a large amount of C. Here it is in the standard library, written in Zig, and it cross-compiles wherever your program does.

Picking the right tool

The names are dense, so it is worth being clear about which problem each solves.

You wantUseNot
A fingerprint of some dataSha256, Blake3anything with “MD” or “SHA1” in it
To prove data came from someone with the keyhmac, or an AEADa plain hash of key ++ data
To encrypt and detect tamperingaead (ChaCha20Poly1305, Aes256Gcm)a bare cipher
To store a passwordpwhash (argon2)any of the above
Fast hashing for a hash mapstd.hashstd.crypto

The last row is a real distinction. std.hash holds non-cryptographic hashes like Wyhash, which are much faster and are what a hash table wants. std.crypto.hash is for when an attacker must not be able to find two inputs with the same output. Using the wrong one is slow in one direction and unsafe in the other.

One-shot and incremental

Sha256.hash(data, &digest, .{});          // one-shot

var hasher = Sha256.init(.{});            // incremental
hasher.update(part1);
hasher.update(part2);
hasher.final(&digest);

Both produce the same digest; use the incremental form when the input arrives in pieces or is too large to hold at once.

The digest is a fixed-size array, [32]u8 for SHA-256, and it is raw bytes rather than hex. Printing it needs {x} or std.fmt.bytesToHex, and comparing it against a hex string from elsewhere means converting one side, not eyeballing the two.

Constant-time comparison

std.crypto.timing_safe.eql([32]u8, a, b)

std.mem.eql returns as soon as it finds a difference, so how long it took reveals how many bytes matched. For MACs, tokens, and password hashes that is a real attack. Use the timing-safe form for anything secret.

The attack is easier to believe once you see the shape of it. An attacker who can submit guesses and measure the response time submits tokens differing in the first byte. When one takes marginally longer, they have learned that byte, and they move to the second. That turns 256^32 guesses into 32 × 256, which is a few thousand. The timing-safe version compares every byte every time and combines the results, so the duration says nothing.

Passwords are a different problem

Do not hash passwords with SHA-256. std.crypto.pwhash provides argon2, bcrypt, and scrypt, which are deliberately slow and salted. That slowness is the feature: it is what makes an offline guessing attack expensive.

SHA-256 is designed to be fast, and commodity hardware computes billions per second. A leaked table of SHA-256 password hashes is a table of passwords within hours. Argon2 is tuned so that one hash takes a measurable fraction of a second and a set amount of memory. That costs your login endpoint nothing. It costs an attacker running millions of guesses everything.

The salt is the other half. It is stored next to the hash, in the clear. Its job is to make sure two users with the same password get different hashes, so one precomputed table cannot crack both. pwhash handles generating and encoding it, which is the main reason to use it rather than assembling the pieces.

What this page is not

Enough to design a protocol. Knowing the API for an AEAD does not tell you how to choose a nonce, and choosing one badly breaks the whole construction. Use a reviewed protocol where one exists, and treat std.crypto as the implementation of primitives rather than as advice about how to combine them.