~/learn / cryptography / common-mistakes

Common Crypto Mistakes

The real-world patterns behind cryptographic failures, from weak randomness and hardcoded keys to missing integrity, nonce reuse, key reuse across purposes, and homegrown schemes, each with the mechanism and the fix.

advancedupdated

Almost every real-world cryptographic failure is a correct algorithm used incorrectly. The primitives (AES, SHA-256, RSA, the curves) are not the weak point and have not been for a long time. The weak point is the wiring: how keys are made and stored, how randomness is sourced, whether integrity is present, and whether someone decided to be clever. These are the patterns to hunt in code review and assessment.

Insecure randomness

Math.random() in JavaScript, rand() in C, and random.random() in Python are pseudorandom but not cryptographically secure. They are seeded from predictable state and their internal generator can be reconstructed after observing a handful of outputs, letting an attacker predict every future value. Anything that must be unguessable (session ids, reset tokens, API keys, nonces, salts, IVs) needs a CSPRNG.

// Wrong: predictable, an attacker who sees a few tokens can predict the rest
const token = Math.random().toString(36).slice(2);

// Right: drawn from the OS cryptographic generator
const bytes = crypto.getRandomValues(new Uint8Array(32));
const token = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");

Hardcoded and shared keys

A key in source is a key in every clone of the repository, every build artifact, every developer laptop, and every backup. Mobile apps and front-end bundles can always be unpacked, so a key shipped to the client is already public. A key shared across all customers or environments means one leak exposes everyone, including all historical data. Keys belong in a secrets manager or KMS, scoped per environment and per purpose, with a rotation plan so a leak has a bounded lifetime.

Encryption without integrity

AES-CBC or AES-CTR with no MAC is malleable: an attacker who cannot read the ciphertext can still modify it and change the decrypted plaintext in predictable ways. The classic outcome is flipping role=user to role=admin inside an encrypted cookie without knowing the key, exactly as shown in symmetric encryption. Use an AEAD mode (AES-GCM, ChaCha20-Poly1305). If you must assemble parts by hand, encrypt-then-MAC with HMAC, never MAC-then-encrypt.

Nonce and IV mistakes

Reusing a nonce with the same key in CTR, GCM, or ChaCha20 leaks the XOR of the plaintexts and, for GCM, the authentication subkey, enabling forgery. Using a predictable IV for CBC enabled the BEAST attack on TLS. Common causes are a hardcoded nonce constant, a counter that resets to zero on process restart, or deriving the IV from something an attacker can predict. Nonces must be unique per key (and for CBC, IVs must be unpredictable).

Reusing a key across purposes

One key used both to encrypt and to MAC, or the same key across two different protocols, creates interactions the primitives were never analyzed for and has produced real breaks. Derive separate subkeys for separate purposes from a single master using a KDF like HKDF, with a distinct context string per use. One key, one job.

ECB mode

Any library call that defaults to ECB, or code that explicitly selects it, leaks plaintext structure. Repeated 16-byte blocks in ciphertext are the giveaway. There is almost no legitimate use; reach for an AEAD instead.

Comparing secrets with ==

Ordinary comparison short-circuits at the first differing byte, so response time reveals how many leading bytes of a token or MAC were correct, which an attacker amplifies into a full forgery one byte at a time. Use the constant-time comparison your platform provides.

Fast hashes for passwords

Unsalted MD5 and SHA-1 password hashes still turn up in breaches constantly, and salted-but-fast SHA-256 is barely better against a GPU. Passwords need Argon2id, scrypt, bcrypt, or high-iteration PBKDF2, as covered in hashing and password storage.

Trusting the algorithm field

Any protocol that lets the attacker choose the algorithm invites a downgrade. The JWT alg: none and RSA-to-HMAC key-confusion bugs are the canonical examples, but TLS downgrade attacks (FREAK, Logjam) are the same idea at the transport layer. The receiver must decide which algorithms it accepts, not the sender.

Disabling certificate validation

verify=False in Python requests, rejectUnauthorized: false in Node, a trust-all TrustManager in Java, or curl -k turn TLS into encryption to whoever happens to answer, with no guarantee of who that is. These almost always begin as a quick fix for a self-signed cert in a test environment and quietly ship to production. Grep for them in every review, including in internal service-to-service calls, which attackers reach after a foothold.

Logging or caching secrets

Keys, tokens, and plaintext passwords end up in application logs, crash reports, error trackers, and URL query strings (which land in server logs, browser history, and Referer headers). Encryption is pointless if the plaintext is also written to a log aggregator with laxer access control.

Homegrown schemes

XOR with a fixed key, Base64 relabeled as “encryption,” custom ciphers, and inventive stacks of hashes all ship in real products. Treat any custom scheme as broken until proven otherwise by people whose job is breaking them, which is essentially never worth it when vetted libraries exist. Many homegrown schemes fall to the techniques in the cipher workbench. The overarching rule, Kerckhoffs’s principle, is that security must rest on the key alone, never on the secrecy of the design.

Field notes: spotting broken crypto in code and on the wire

The tells that something is wrong, and how to confirm it:

  • Predictable randomness. Math.random(), rand(), mt_rand(), or anything seeded with the time produces guessable tokens. If password-reset tokens or session ids come from a non-CSPRNG, you can predict the next one. Collect several and look for a pattern or a short period; Math.random state can be recovered from a handful of outputs. Secrets must come from crypto.getRandomValues, secrets, or SecureRandom.
  • Hardcoded keys and IVs. grep -rEi '(secret|key|iv|password)\s*=\s*["\x27]' . across the repo, and scan git history with gitleaks. A fixed IV reused across messages reintroduces the ECB and nonce-reuse weaknesses even when the mode is otherwise fine.
  • Encryption without integrity. Ciphertext the server accepts after you tamper with it has no MAC, which means bit-flipping and padding-oracle attacks are on the table. Edit a byte and see if you get a crypto error or just different behavior.
  • ECB in disguise. Upload an image or a long repetitive string and look for repeating 16-byte blocks in the output. The “ECB penguin” is visible precisely because structure survives encryption.
  • Non-constant-time comparison. token == expected or === on secrets is a timing oracle. Look for hmac.compare_digest / secure_compare and flag its absence.
  • Rolled-your-own. Any custom cipher, custom KDF, or XOR-with-a-repeating-key is broken by definition. XOR with a reused keystream falls to cribs and frequency analysis (the cipher workbench shows why).
  • Downgrade and version tells. SSLv3, TLS 1.0, RC4, DES, or MD5 in a handshake or a signature are immediate findings. testssl.sh enumerates them against a host.

Assess only systems and code you own or are authorized to review.

A review checklist

  • Where do keys come from, where are they stored, who can read them, and how are they rotated?
  • Is every secret random value generated by a CSPRNG?
  • Is every encrypted value also authenticated (AEAD or encrypt-then-MAC)?
  • Are nonces unique per key, and IVs unpredictable where required?
  • Is each key used for exactly one purpose, with subkeys derived by a KDF?
  • Are passwords hashed with Argon2id, scrypt, bcrypt, or high-iteration PBKDF2?
  • Is TLS certificate validation enabled everywhere, including internal calls?
  • Are accepted algorithms pinned by the receiver rather than chosen by input?
  • Do secrets stay out of logs, error reports, and URLs?