zig-skills/references/std-crypto.md

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
var derived: [32]u8 = undefined;
Blake3.hash("material", &derived, .{ .context = "my app v1 key derivation" });

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 tag doesn't verify

Key constants:

  • key_length: 32 bytes (256 bits)
  • nonce_length: 12 bytes
  • tag_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 bytes
  • nonce_length: 12 bytes (IETF) or 24 bytes (XChaCha)
  • tag_length: 16 bytes

Available variants:

  • ChaCha20Poly1305 - Standard IETF
  • XChaCha20Poly1305 - 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();

// Sign message
const sig = kp.sign(message, null);

// Verify signature
try kp.public_key.verify(sig, message);
// 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();

// 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();
const bob = X25519.KeyPair.generate();

// 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();

// Encapsulation (sender)
const encaps = kp.public_key.encaps(null);
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
);

// Use preset parameters
try argon2.kdf(allocator, &hash, password, salt, argon2.Params.interactive_2id, .argon2id);

// PHC string format (for storage)
var buf: [128]u8 = undefined;
const encoded = try argon2.strHash(password, salt, .interactive_2id, .argon2id, &buf);
// Returns: "$argon2id$v=19$m=65536,t=3,p=4$..."

// Verify PHC-encoded hash
try argon2.strVerify(encoded, password, null);

Parameter presets:

  • interactive_2id: Fast verification (login forms)
  • moderate_2id: Balanced
  • sensitive_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: [bcrypt.hash_length]u8 = undefined;
try bcrypt.strHash(password, .{ .rounds = 10 }, &hash);

// Verify
try bcrypt.strVerify(hash_str, password);

PBKDF2

const pbkdf2 = std.crypto.pwhash.pbkdf2;
const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256;

var key: [32]u8 = undefined;
pbkdf2(HmacSha256, &key, password, salt, 100000);  // 100k iterations

Secure Random

Thread-local cryptographically secure PRNG:

const random = std.crypto.random;

// Random bytes
var key: [32]u8 = undefined;
random.bytes(&key);

// Random integers
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;
std.crypto.random.bytes(&key);

// For asymmetric keys
const kp = std.crypto.sign.Ed25519.KeyPair.generate();

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;
std.crypto.random.bytes(&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, null, .interactive_2id, .argon2id, &buf);
// Store hash_str in database

// Login: verify
argon2.strVerify(stored_hash, password, null) 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.eql for comparing secrets, not == or std.mem.eql
  • Zero secrets - always secureZero sensitive data when done