15 KiB
std.crypto - Cryptography Library (Zig 0.16.0)
Comprehensive cryptographic primitives: hashing, encryption, signatures, key exchange, password hashing, and secure utilities.
Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html
Zig 0.16 Entropy Rule
Do not use std.crypto.random directly in new Zig 0.16 code. Entropy is owned by std.Io.
var key: [32]u8 = undefined;
io.random(&key);
const rng_source: std.Random.IoSource = .{ .io = io };
const rng = rng_source.interface();
Use io.randomSecure(&bytes) when fresh OS-backed secure entropy is required and failures should be reported.
Zig 0.16 also adds AES-SIV, AES-GCM-SIV, and Ascon AEAD/hash constructions.
Quick Reference
| Category | Types/Functions |
|---|---|
| Hash | hash.sha2.Sha256, hash.sha2.Sha512, hash.sha3.*, hash.Blake3, hash.blake2.*, hash.Md5, hash.Sha1 |
| AEAD | aead.aes_gcm.Aes256Gcm, aead.chacha_poly.ChaCha20Poly1305, aead.aegis.* |
| MAC | auth.hmac.*, auth.siphash.*, auth.cmac.* |
| Signatures | sign.Ed25519, sign.ecdsa.* |
| Key Exchange | dh.X25519 |
| KEM | kem.ml_kem.* (post-quantum) |
| Password | pwhash.argon2, pwhash.scrypt, pwhash.bcrypt, pwhash.pbkdf2 |
| KDF | kdf.hkdf.HkdfSha256, kdf.hkdf.HkdfSha512 |
| Random | Use io.random, io.randomSecure, or std.Random.IoSource |
| Utilities | secureZero, timing_safe.*, codecs.* |
Choosing Algorithms
Need encryption?
├─ With authentication → AEAD (Aes256Gcm, ChaCha20Poly1305)
└─ Stream only → stream.chacha.* (usually want AEAD instead)
Need hashing?
├─ General purpose → Sha256, Sha512, Blake3
├─ Password storage → argon2, scrypt, bcrypt
└─ Legacy compatibility → Md5, Sha1 (NOT secure for new designs)
Need signatures?
├─ Standard choice → Ed25519
└─ ECDSA compatibility → ecdsa.EcdsaP256Sha256
Need key exchange?
├─ Standard choice → X25519
└─ Post-quantum → ml_kem.* (Kyber)
Need MAC?
├─ With key → HmacSha256, HmacSha512
└─ Hash table keying → siphash
Hashing
SHA-2 Family
const std = @import("std");
const sha2 = std.crypto.hash.sha2;
// One-shot hashing
var digest: [sha2.Sha256.digest_length]u8 = undefined;
sha2.Sha256.hash("hello world", &digest, .{});
// Streaming (incremental)
var hasher = sha2.Sha256.init(.{});
hasher.update("hello ");
hasher.update("world");
hasher.final(&digest);
// Peek at intermediate digest without consuming state
const intermediate = hasher.peek();
Available: Sha224, Sha256, Sha384, Sha512, Sha512_224, Sha512_256
SHA-3 Family
const sha3 = std.crypto.hash.sha3;
var digest: [sha3.Sha3_256.digest_length]u8 = undefined;
sha3.Sha3_256.hash("data", &digest, .{});
// SHAKE (extendable output)
var shake = sha3.Shake128.init(.{});
shake.update("data");
var output: [64]u8 = undefined;
shake.squeeze(&output);
Available: Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256, Keccak256, Keccak512
Blake3
const Blake3 = std.crypto.hash.Blake3;
// Standard hashing
var digest: [Blake3.digest_length]u8 = undefined;
Blake3.hash("data", &digest, .{});
// Keyed hashing (MAC)
var keyed: [Blake3.digest_length]u8 = undefined;
Blake3.hash("data", &keyed, .{ .key = key });
// Key derivation uses the dedicated KDF mode, not keyed-hash options.
var derived: [32]u8 = undefined;
var kdf = Blake3.initKdf("my app v1 key derivation", .{});
kdf.update("material");
kdf.final(&derived);
Blake2
const blake2 = std.crypto.hash.blake2;
// Blake2b (64-byte output)
var digest: [blake2.Blake2b256.digest_length]u8 = undefined;
blake2.Blake2b256.hash("data", &digest, .{});
// With key
blake2.Blake2b256.hash("data", &digest, .{ .key = key });
Available: Blake2b128, Blake2b256, Blake2b384, Blake2b512, Blake2s128, Blake2s224, Blake2s256
AEAD (Authenticated Encryption)
AES-GCM
const Aes256Gcm = std.crypto.aead.aes_gcm.Aes256Gcm;
// Encryption
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [Aes256Gcm.tag_length]u8 = undefined;
Aes256Gcm.encrypt(&ciphertext, &tag, plaintext, associated_data, nonce, key);
// Decryption
var decrypted: [ciphertext.len]u8 = undefined;
try Aes256Gcm.decrypt(&decrypted, &ciphertext, tag, associated_data, nonce, key);
// Returns error.AuthenticationFailed if the tag doesn't verify. Treat the
// contents of `decrypted` as invalid and discard them on any failure.
Key constants:
key_length: 32 bytes (256 bits)nonce_length: 12 bytestag_length: 16 bytes
ChaCha20-Poly1305
const ChaCha20Poly1305 = std.crypto.aead.chacha_poly.ChaCha20Poly1305;
var ciphertext: [msg.len]u8 = undefined;
var tag: [ChaCha20Poly1305.tag_length]u8 = undefined;
ChaCha20Poly1305.encrypt(&ciphertext, &tag, msg, ad, nonce, key);
try ChaCha20Poly1305.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
Key constants:
key_length: 32 bytesnonce_length: 12 bytes (IETF) or 24 bytes (XChaCha)tag_length: 16 bytes
Available variants:
ChaCha20Poly1305- Standard IETFXChaCha20Poly1305- Extended nonce (24 bytes, better for random nonces)ChaCha12Poly1305,ChaCha8Poly1305- Reduced rounds (faster, lower security margin)
AEGIS
High-performance AEAD designed for modern CPUs with AES-NI:
const Aegis256 = std.crypto.aead.aegis.Aegis256;
var ciphertext: [msg.len]u8 = undefined;
var tag: [Aegis256.tag_length]u8 = undefined;
Aegis256.encrypt(&ciphertext, &tag, msg, ad, nonce, key);
try Aegis256.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
Message Authentication (MAC)
HMAC
const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256;
// One-shot
var mac: [HmacSha256.mac_length]u8 = undefined;
HmacSha256.create(&mac, message, key);
// Streaming
var hmac = HmacSha256.init(key);
hmac.update(data1);
hmac.update(data2);
hmac.final(&mac);
Available: HmacMd5, HmacSha1, HmacSha224, HmacSha256, HmacSha384, HmacSha512
SipHash
Fast MAC for hash table keying (not for general authentication):
const SipHash = std.crypto.auth.siphash.SipHash64(2, 4);
const hash = SipHash.hash(key, data);
Digital Signatures
Ed25519
const Ed25519 = std.crypto.sign.Ed25519;
// Generate key pair
const kp = Ed25519.KeyPair.generate(io);
// Sign message
const sig = kp.sign(message, null);
// Verify signature
try sig.verify(message, kp.public_key);
// Returns error.SignatureVerificationFailed on failure
// Incremental signing (large messages)
var signer = try kp.signer(null);
signer.update(chunk1);
signer.update(chunk2);
const sig2 = signer.finalize();
Key lengths:
- Secret key: 64 bytes
- Public key: 32 bytes
- Signature: 64 bytes
ECDSA
const EcdsaP256Sha256 = std.crypto.sign.ecdsa.EcdsaP256Sha256;
// Generate key pair
const kp = EcdsaP256Sha256.KeyPair.generate(io);
// Sign
const sig = try kp.sign(message, null);
// Verify
try sig.verify(message, kp.public_key);
Available: EcdsaP256Sha256, EcdsaP256Sha3_256, EcdsaP384Sha384, EcdsaP384Sha3_384, EcdsaSecp256k1Sha256
Key Exchange
X25519 (Diffie-Hellman)
const X25519 = std.crypto.dh.X25519;
// Generate key pairs for Alice and Bob
const alice = X25519.KeyPair.generate(io);
const bob = X25519.KeyPair.generate(io);
// Compute shared secret
const alice_shared = try X25519.scalarmult(alice.secret_key, bob.public_key);
const bob_shared = try X25519.scalarmult(bob.secret_key, alice.public_key);
// alice_shared == bob_shared
// IMPORTANT: Hash the shared secret before use
var key: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(&alice_shared, &key, .{});
ML-KEM (Post-Quantum)
const MLKem768 = std.crypto.kem.ml_kem.MLKem768;
// Key generation
const kp = MLKem768.KeyPair.generate(io);
// Encapsulation (sender)
const encaps = kp.public_key.encaps(io);
const shared_secret = encaps.shared_secret;
const ciphertext = encaps.ciphertext;
// Decapsulation (receiver)
const decaps_secret = try kp.secret_key.decaps(ciphertext);
// shared_secret == decaps_secret
Available: MLKem512, MLKem768, MLKem1024
Key Derivation
HKDF
const HkdfSha256 = std.crypto.kdf.hkdf.HkdfSha256;
// Extract: derive pseudorandom key from input keying material
const prk = HkdfSha256.extract(salt, input_key_material);
// Expand: derive output key from PRK
var output_key: [32]u8 = undefined;
HkdfSha256.expand(&output_key, context_info, prk);
// Streaming extract (large IKM)
var hkdf = HkdfSha256.extractInit(salt);
hkdf.update(ikm_part1);
hkdf.update(ikm_part2);
var prk2: [HkdfSha256.prk_length]u8 = undefined;
hkdf.final(&prk2);
Password Hashing
Argon2
Memory-hard password hashing (recommended for new applications):
const argon2 = std.crypto.pwhash.argon2;
// Hash password
var hash: [32]u8 = undefined;
try argon2.kdf(
allocator,
&hash,
password,
salt,
.{
.t = 3, // time cost (iterations)
.m = 65536, // memory cost (KiB)
.p = 4, // parallelism
},
.argon2id, // mode: argon2i, argon2d, or argon2id
io,
);
// Use preset parameters
try argon2.kdf(allocator, &hash, password, salt, argon2.Params.interactive_2id, .argon2id, io);
// PHC string format (for storage)
var buf: [128]u8 = undefined;
const encoded = try argon2.strHash(password, .{
.allocator = allocator,
.params = .interactive_2id,
.mode = .argon2id,
}, &buf, io);
// Returns: "$argon2id$v=19$m=65536,t=3,p=4$..."
// Verify PHC-encoded hash
try argon2.strVerify(encoded, password, .{ .allocator = allocator }, io);
Parameter presets:
interactive_2id: Fast verification (login forms)moderate_2id: Balancedsensitive_2id: High security (key derivation)owasp_2id: OWASP recommended
Scrypt
Memory-hard KDF:
const scrypt = std.crypto.pwhash.scrypt;
var hash: [32]u8 = undefined;
try scrypt.kdf(
allocator,
&hash,
password,
salt,
.{ .ln = 17, .r = 8, .p = 1 }, // N=2^17, r=8, p=1
);
// Presets
try scrypt.kdf(allocator, &hash, password, salt, scrypt.Params.interactive);
bcrypt
const bcrypt = std.crypto.pwhash.bcrypt;
// Hash password
var hash: [128]u8 = undefined;
const hash_str = try bcrypt.strHash(password, .{
.params = .owasp,
.encoding = .phc,
}, &hash, io);
// Verify
try bcrypt.strVerify(hash_str, password, .{ .silently_truncate_password = false });
PBKDF2
const pbkdf2 = std.crypto.pwhash.pbkdf2;
const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256;
var key: [32]u8 = undefined;
try pbkdf2(&key, password, salt, 100000, HmacSha256); // 100k iterations
Secure Random
Use the caller-provided std.Io entropy interface. io.random is the process CSPRNG and may use a less-secure seeding fallback on platforms where fresh system entropy is unavailable; use io.randomSecure when that fallback is unacceptable.
// Random bytes
var key: [32]u8 = undefined;
io.random(&key);
// Adapt std.Io to the general std.Random convenience API.
var source: std.Random.IoSource = .{ .io = io };
const random = source.interface();
const n = random.int(u64);
const bounded = random.intRangeLessThan(u32, 0, 100); // [0, 100)
// Random float [0, 1)
const f = random.float(f64);
// Shuffle
random.shuffle(u32, &items);
Secure Utilities
secureZero
Securely erase sensitive data (prevents optimizer from removing):
var secret: [32]u8 = undefined;
// ... use secret ...
std.crypto.secureZero(u8, &secret); // guaranteed to zero
Timing-Safe Operations
const timing_safe = std.crypto.timing_safe;
// Constant-time equality (for MACs, signatures)
const equal = timing_safe.eql([32]u8, mac1, mac2);
// Constant-time comparison
const order = timing_safe.compare(u8, &a, &b, .big); // .lt, .eq, .gt
// Constant-time arithmetic
const overflow = timing_safe.add(u8, &a, &b, &result, .big);
const underflow = timing_safe.sub(u8, &a, &b, &result, .big);
Codecs (Constant-Time)
const codecs = std.crypto.codecs;
// Hex encoding (constant-time)
var hex: [64]u8 = undefined;
try codecs.hex.encode(&hex, &binary, .lower);
// Hex decoding
var decoded: [32]u8 = undefined;
try codecs.hex.decode(&decoded, &hex);
// Base64
const base64 = codecs.base64;
// Similar API to hex
Elliptic Curve Primitives
Low-level curve operations (usually use higher-level APIs):
const ecc = std.crypto.ecc;
// Edwards25519
const point = ecc.Edwards25519.basePoint;
const result = try point.mul(scalar);
// P-256 (NIST)
const p256_point = ecc.P256.basePoint;
// Ristretto255 (prime-order group)
const ristretto = ecc.Ristretto255.basePoint;
Available: Curve25519, Edwards25519, Ristretto255, P256, P384, Secp256k1
Error Handling
const errors = std.crypto.errors;
// Common errors
error.AuthenticationFailed // MAC/tag verification failed
error.SignatureVerificationFailed
error.IdentityElement // Degenerate point in ECC
error.NonCanonical // Input not in canonical form
error.InvalidEncoding // Malformed input
error.WeakPublicKey // Unsafe public key
error.PasswordVerificationFailed
Common Patterns
Encrypt-then-MAC
// Use AEAD instead - it handles this correctly
const Aes256Gcm = std.crypto.aead.aes_gcm.Aes256Gcm;
Aes256Gcm.encrypt(&ct, &tag, pt, ad, nonce, key);
Key Generation
// For symmetric keys
var key: [32]u8 = undefined;
io.random(&key);
// For asymmetric keys
const kp = std.crypto.sign.Ed25519.KeyPair.generate(io);
Nonce Management
// Option 1: Counter (deterministic, never reuse)
var nonce: [12]u8 = undefined;
std.mem.writeInt(u64, nonce[0..8], counter, .big);
@memset(nonce[8..], 0);
counter += 1;
// Option 2: Random (safe with XChaCha's 24-byte nonce)
const XChaCha = std.crypto.aead.chacha_poly.XChaCha20Poly1305;
var nonce: [XChaCha.nonce_length]u8 = undefined;
io.random(&nonce);
Secure Password Storage
const argon2 = std.crypto.pwhash.argon2;
// Registration: hash and store
var buf: [128]u8 = undefined;
const hash_str = try argon2.strHash(password, .{
.allocator = allocator,
.params = .interactive_2id,
.mode = .argon2id,
}, &buf, io);
// Store hash_str in database
// Login: verify
argon2.strVerify(stored_hash, password, .{ .allocator = allocator }, io) catch |err| {
if (err == error.PasswordVerificationFailed) {
// Invalid password
}
};
Side-Channel Protection
Configure side-channel mitigations:
const SideChannelsMitigations = std.crypto.SideChannelsMitigations;
// Available levels:
// .none - Fastest, no mitigations
// .basic - Protects against most practical attacks
// .medium - Default, good balance (increased resistance)
// .full - Highest protection, significant performance impact
// Default is .medium
const default = std.crypto.default_side_channels_mitigations;
Notes
- Never use MD5 or SHA1 for security - only for legacy compatibility
- AEAD over separate encrypt+MAC - AES-GCM or ChaCha20-Poly1305 handle this correctly
- Hash shared secrets - X25519 output should be passed through a KDF before use
- Use argon2id for passwords - it's the current best practice
- XChaCha for random nonces - 24-byte nonce has negligible collision probability
- Timing attacks - use
timing_safe.eqlfor comparing secrets, not==orstd.mem.eql - Zero secrets - always
secureZerosensitive data when done