🔓 Analogy
A bank-vault door is unbreakable — but it’s useless if someone props it open, writes the combination on a sticky note, or uses the same key for every vault in the country. Cryptography is the same: AES and SHA-256 are not the weak point; how you use them is. Almost every real-world crypto break is a usage mistake — a reused nonce, a fast hash, a key in source control — not a broken algorithm. This page is a tour of the sticky notes.
The break is (almost) never the algorithm
graph TD ALGO["strong algorithm<br/>AES, SHA-256"] --> M1["fast hash for passwords"] ALGO --> M2["reused nonce"] ALGO --> M3["ECB mode"] ALGO --> M4["hardcoded key"] ALGO --> M5["math/rand"] ALGO --> M6["== on secrets"] M1 --> BROKEN["broken in practice"] M2 --> BROKEN M3 --> BROKEN M4 --> BROKEN M5 --> BROKEN M6 --> BROKEN
Dictionary attacks: why fast hashes lose
“Cracking” a hash isn’t reversing it — it’s guessing and re-hashing until a guess matches. Because SHA-256 is fast, an attacker tries an enormous wordlist cheaply. This runs here: it cracks unsalted SHA-256 hashes against a small wordlist, exactly as a real cracker does:
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
func sha(s string) string {
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:])
}
func main() {
// A "stolen database" of unsalted SHA-256 password hashes.
stolen := map[string]string{
"alice": sha("password123"),
"bob": sha("letmein"),
"carol": sha("S3cur3!Passphrase-No-One-Guesses"),
}
// The attacker's wordlist (real ones have millions of entries).
wordlist := []string{"123456", "password", "letmein", "password123", "qwerty"}
// Precompute hash -> word, then match the whole DB at once.
rainbow := make(map[string]string, len(wordlist))
for _, w := range wordlist {
rainbow[sha(w)] = w
}
for user, h := range stolen {
if pw, ok := rainbow[h]; ok {
fmt.Printf("%-6s CRACKED -> %s\n", user, pw)
} else {
fmt.Printf("%-6s safe (not in wordlist)\n", user)
}
}
}
alice and bob fall instantly; carol’s long, unusual passphrase survives this list but not a bigger one run at GPU speed. The fix isn’t a better hash function — it’s a slow, salted KDF (argon2id/bcrypt) so each guess costs ~100ms instead of nanoseconds, turning a seconds-long crack into millennia.
The other classic mistakes
- ECB mode leaks patterns. AES in ECB encrypts identical blocks identically, so structure shows through the ciphertext (the “ECB penguin”). Use AEAD (AES-GCM).
- Nonce reuse is fatal. Reuse a key+nonce with GCM/CTR and the keystream repeats:
C1 XOR C2 = P1 XOR P2leaks the plaintexts, and GCM additionally exposes its authentication key. One nonce, one message, forever. - Hardcoded keys. A secret in source is in git history and the binary (
strings ./appfinds it). Rotating it needs a rebuild. Secrets go in the environment or a secrets manager. - Weak randomness.
math/randis a predictable PRNG — fine for jitter, fatal for keys, tokens, and nonces. Usecrypto/rand. - Timing side-channels. Comparing a secret with
==returns early on the first mismatch, leaking the matching length through response time. Usecrypto/subtle.ConstantTimeCompare.
crypto/rand vs math/rand — the one-byte difference that matters
import "crypto/rand" // NOT math/rand — for any security value
token := make([]byte, 32)
rand.Read(token) // cryptographically secure, unpredictable
// math/rand would give predictable bytes an attacker can reproduce.
This single import choice is the difference between an unguessable session token and one an attacker can reproduce from the PRNG state. When in doubt for anything security-sensitive, it’s crypto/rand.
🐹 A weak-crypto audit checklist
Grep your codebase for the tells: md5/sha1 used for security, NewECBEncrypter, math/rand generating keys/tokens, a constant that looks like a key, bytes.Equal or == on a MAC/token, and any reused nonce. Run govulncheck for known crypto-library CVEs, and prefer high-level libraries (age, nacl/secretbox) that make the right choices for you. The algorithms are solid; your job is to not undermine them.
⚠️ 'It encrypts/looks random' is not a security review
The scariest weak-crypto bugs work perfectly — they encrypt, decrypt, and produce random-looking output, so they pass every functional test. A reused nonce, an ECB image, a math/rand token: the program behaves correctly while being completely broken. Functional correctness tells you nothing about cryptographic correctness. The only checks that matter are the specific properties above — and, for anything important, a real review by someone who knows crypto.
See also
- Hashing & passwords — the slow KDFs that defeat dictionary attacks.
- Symmetric encryption — AEAD, nonces, and why ECB is banned.
- Secrets management — keeping keys out of source and binaries.
- Supply-chain security — govulncheck for vulnerable crypto libraries.
Next: defensive engineering, starting with the bug class behind most breaches — input validation & injection defense.
Related topics
Storing secrets the right way — hashing vs encryption vs encoding, why fast hashes are wrong for passwords, salts, and the slow KDFs (bcrypt, scrypt, argon2id) that actually protect credentials.
cryptographySymmetric EncryptionKeeping data confidential with one shared key — why you reach for authenticated encryption (AES-GCM), the absolute rule of nonce uniqueness, and the broken modes (ECB) you must never use.
cryptographyTLS & PKIHow strangers establish trust on the internet — the TLS handshake, certificates and the chain of trust, certificate transparency, and mutual TLS — with runnable Go cert generation and verification.
Check your understanding
Score: 0 / 51. Why can a leaked database of unsalted SHA-256 password hashes be cracked so fast?
Cracking isn't reversing — it's guessing and hashing until a guess matches. Because SHA-256 is fast and unsalted hashes are identical for identical passwords, an attacker precomputes (or GPU-brute-forces) common passwords and matches the whole table at once. Salting + a slow KDF (argon2id/bcrypt) makes each guess expensive and unique, defeating this.
2. What goes catastrophically wrong if you reuse a nonce with AES-GCM (or any stream/CTR cipher)?
Stream ciphers XOR the plaintext with a keystream derived from key+nonce. Same key+nonce = same keystream, so C1 XOR C2 = P1 XOR P2 — the plaintexts leak. For GCM specifically, nonce reuse also lets an attacker recover the GHASH authentication key and forge messages. Nonce uniqueness per key is not optional.
3. Why is math/rand unsuitable for generating keys, tokens, or nonces?
math/rand is a statistical PRNG: fast and fine for simulations, but its output is fully determined by its seed and is predictable. If you generate a session token or key with it, an attacker who learns or guesses the state can predict future values. Always use crypto/rand (CSPRNG) for anything security-sensitive.
4. What's wrong with hardcoding an encryption key or API secret in source code?
A secret in source is committed to git forever (even if later 'removed', it's in history) and is trivially recoverable from the binary with `strings`. Private repos get leaked, forked, and cloned to laptops. Secrets belong in environment variables, a secrets manager, or a mounted file — never in code. See secrets management.
5. What is a timing side-channel, and how do you prevent it when comparing secrets?
A naive byte comparison (== or bytes.Equal) stops at the first differing byte, so a matching prefix takes measurably longer. An attacker measures response times to recover a MAC or token byte-by-byte. crypto/subtle.ConstantTimeCompare always examines all bytes, taking the same time regardless of where (or whether) they differ.
Comments
Sign in with GitHub to join the discussion.