Deep dive
How it works
In: message bytes of any length, optionally a key or context string · Out: 32-byte digest (BLAKE3: any length) · State: none (hasher state lives only within one streaming call) · Cost: O(n); order of 1-2 GB/s (SHA-256 with SHA extensions), several GB/s (BLAKE3)
- SHA-256 (Merkle-Damgård): pad the message, split into 64-byte blocks, feed each through a 64-round compression function updating a 32-byte state. The final state is the digest, which is why an attacker who knows H(m) can compute H(m || padding || x) without m (length extension).
- SHA-512/256, SHA-3, BLAKE2, BLAKE3 don't have length extension.
- BLAKE3: split input into 1 KiB chunks, compress each independently (SIMD lanes, threads), combine in a binary tree. Modes:
hash,keyed_hash(a MAC, 32-byte key),derive_key(KDF with a context string), and XOF (any output length). - HMAC(K, m) = H((K ⊕ opad) || H((K ⊕ ipad) || m)): a secure MAC for any of these hashes.
- HKDF: Extract (HMAC with salt over input key material) then Expand (HMAC chain with an
infolabel) to get independent subkeys.
Code
Rust: sha2 = "0.10", hmac = "0.12", hkdf = "0.12", blake3 = "1".
use hkdf::Hkdf;
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use std::io::Read;
type HmacSha256 = Hmac<Sha256>;
/// Streaming file digest; constant memory.
pub fn sha256_file(path: &str) -> std::io::Result<[u8; 32]> {
let mut f = std::fs::File::open(path)?;
let mut h = Sha256::new();
let mut buf = vec![0u8; 1 << 16];
loop {
let n = f.read(&mut buf)?;
if n == 0 { break; }
h.update(&buf[..n]);
}
Ok(h.finalize().into())
}
/// Webhook-style MAC: sign and verify in constant time.
pub fn mac(key: &[u8], msg: &[u8]) -> [u8; 32] {
let mut m = HmacSha256::new_from_slice(key).expect("any key length works");
m.update(msg);
m.finalize().into_bytes().into()
}
pub fn mac_ok(key: &[u8], msg: &[u8], tag: &[u8]) -> bool {
let mut m = HmacSha256::new_from_slice(key).expect("any key length works");
m.update(msg);
m.verify_slice(tag).is_ok() // constant-time compare
}
/// One master secret -> independent keys per purpose.
pub fn subkeys(master: &[u8; 32]) -> ([u8; 32], [u8; 32]) {
let hk = Hkdf::<Sha256>::new(Some(b"myapp-v1-salt"), master);
let (mut enc, mut auth) = ([0u8; 32], [0u8; 32]);
hk.expand(b"encryption", &mut enc).unwrap();
hk.expand(b"auth-tokens", &mut auth).unwrap();
(enc, auth)
}
/// BLAKE3: fastest option when you control both sides.
pub fn blake3_examples(data: &[u8], key: &[u8; 32], master: &[u8]) {
let digest = blake3::hash(data); // 32 B, .to_hex() for display
let tag = blake3::keyed_hash(key, data); // MAC
let k = blake3::derive_key("myapp 2026-10 session keys v1", master); // KDF, context is a fixed string
let _ = (digest, tag, k);
// big inputs: blake3::Hasher::new().update_rayon(data).finalize() uses all cores ("rayon" feature)
}
Complexity and constants
- SHA-256 with SHA-NI (x86) / ARMv8 SHA2: order of 1.5-2 GB/s per core; without, order of 300-500 MB/s, where SHA-512 is faster on 64-bit CPUs.
- BLAKE3: order of 3-7 GB/s single-threaded with AVX2/AVX-512, scaling near-linearly with cores on large inputs.
- Small inputs (< 64 B): fixed overhead of hundreds of ns; per-call cost, not throughput, dominates.
- Digest sizes: 32 B gives 128-bit collision resistance, which is enough. Truncating to 16 B leaves 64-bit collision resistance: fine for non-adversarial dedup keys, not for security.
Efficiency
- Cost: SHA-256 order of 1.5-2 GB/s per core with SHA extensions, 300-500 MB/s in software; BLAKE3 order of 3-7 GB/s single-threaded, scaling with cores; hundreds of ns fixed cost for tiny inputs. Bound: compute on long inputs, per-call overhead on short ones.
- Levers:
- Hardware SHA — order of 3-5x over software (the
sha2crate selects it at runtime) — SHA-256 on x86/ARM with the extensions. - Tree hashing — BLAKE3's README says
b3sumis an order of magnitude faster thansha256sumon typical desktop hardware (rayonfeature) — big inputs, both sides under your control. - Clone a keyed HMAC instance — saves the two pad-block compressions per message — many short MACs under one key.
- 64 KiB or larger read buffers — syscalls stop dominating — streaming files.
- Hardware SHA — order of 3-5x over software (the
- Naive → standard → state of the art:
- Naive · software SHA-256, small reads, rebuild HMAC state per message
- Standard · SHA-256 with hardware backend, streaming, cached keyed state
- State of the art · multithreaded SIMD BLAKE3; for non-adversarial dedup or hash tables a fast non-cryptographic hash
Implementation notes
- Hash the exact bytes: define canonical serialization before hashing structured data, and include a domain-separation prefix and length fields when hashing multiple fields (
H("v1" || len(a) || a || b)), otherwise("ab","c")collides with("a","bc"). - For MAC checks, compare in constant time (
verify_slice,subtle::ConstantTimeEq), never==. - Random tokens (session IDs, API keys) are already uniform: store SHA-256(token) in the DB; no slow KDF needed.
- Merkle trees: prefix leaves with 0x00 and inner nodes with 0x01 to prevent second-preimage tricks.
Pitfalls
- SHA-256(secret || message) as an authenticator: length-extendable.
- Using MD5/SHA-1 "only for integrity" against attackers (chosen-prefix collisions on SHA-1 are practical).
- Hashing passwords with SHA-256, even salted.
- Mixing up hex and raw bytes when comparing digests across systems.
- Assuming a hash hides low-entropy data: SHA-256(phone number) is reversible by enumeration.
Variants and alternatives
crypto_generichash) and Argon2.In production
- Ledgers: Bitcoin (double SHA-256, docs).
- Cloud and supply chain: OCI container digests (
sha256:, spec), Sigstore, TLS 1.3 (HKDF-SHA256/384). - Source control: Git (SHA-1 moving to SHA-256, doc).
- Networking and storage: WireGuard (BLAKE2s), Bao/iroh (BLAKE3 verified streaming).
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| blake3 | Rust | BLAKE3-team/BLAKE3 | cargo add blake3 | — | — | — | — | official; SSE2 to AVX-512, NEON, runtime detection, rayon threads; b3sum CLI |
| sha2 | Rust | RustCrypto/hashes | cargo add sha2 | — | — | — | — | SHA-256/512, runtime SHA-NI and AArch64 backends; SHA-3 and BLAKE2 in sibling crates |
| hmac, hkdf | Rust | RustCrypto/MACs, RustCrypto/KDFs | cargo add hmac hkdf | — | — | — | — | generic over any digest hash; constant-time verify_slice |
| aws-lc-rs | Rust | aws/aws-lc-rs | cargo add aws-lc-rs | — | — | — | — | SHA-2, HMAC, HKDF over AWS-LC; FIPS option via aws-lc-fips-sys |
| ring | Rust | briansmith/ring | cargo add ring | — | — | — | — | SHA-2, HMAC, HKDF; C/asm largely from BoringSSL |
Sources
- NIST, FIPS 180-4: Secure Hash Standard, 2015. spec; FIPS 202: SHA-3 Standard, 2015. spec
- O'Connor, Aumasson, Neves, Wilcox-O'Hearn, BLAKE3: one function, fast everywhere, 2020. paper · code
- Krawczyk, Bellare, Canetti, RFC 2104: HMAC, 1997. RFC; Krawczyk, Eronen, RFC 5869: HKDF, 2010. RFC
- Stevens et al., The first collision for full SHA-1, CRYPTO 2017. paper · site