No modern cryptographic breach happened because someone broke AES or factored an RSA key. What fails is everything around the algorithm: the code that calls it, the system that manages the keys, the humans who configure it all.
The math works. The engineering around it does not. Every real-world cryptographic failure is an implementation, protocol, or operational error.
Code-level errors: nonce reuse, wrong cipher mode, timing leaks, buffer overflows. The algorithm is correct. The code that calls it is not.
Missing authentication, wrong composition of primitives, encrypting without signing. The pieces are sound. The assembly is flawed.
Key management disasters, unpatched libraries, debug logging that leaks secrets. The system works. The people running it make mistakes.
A side channel leaks information through something other than the algorithm's output: execution time, power consumption, electromagnetic emissions. Timing is the most common side channel in software, and the easiest to exploit remotely.
A naive string comparison returns false on the first mismatched byte. That means the comparison takes different amounts of time depending on how many leading bytes are correct. An attacker who can measure response time can recover a secret token byte by byte. For an -byte secret with possible values per byte, brute force requires attempts. A timing attack requires only . Exponential vs. linear.
4.3 billion guesses for an 8-character hex token.
128 guesses maximum. Test each position independently.
A vulnerable server compares API tokens byte by byte and returns early on the first mismatch. Measure the response time to figure out the secret, one character at a time.
A constant-time comparison checks every byte regardless of where the first mismatch occurs. XOR each byte pair, OR all results into an accumulator, then check if the accumulator is zero.
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0; // same time regardless of match positionA padding oracle is any system that tells an attacker whether a decrypted ciphertext has valid padding. This seemingly minor information leak lets an attacker decrypt an entire message without knowing the key, one byte at a time.
Block ciphers operate on fixed-size blocks (16 bytes for AES). When the plaintext is not a multiple of the block size, PKCS#7 padding fills the remaining bytes. The value of each padding byte equals the number of padding bytes needed.
The server decrypts, checks padding, and returns either "padding valid" or "padding invalid." This one-bit oracle is devastating.
The attacker targets the last byte of a ciphertext block. They modify the corresponding byte in the preceding block, trying all 256 possible values.
When the server responds "valid padding," the attacker knows the decrypted last byte is 0x01 (valid single-byte padding). From this, they derive the intermediate decryption value: .
The original plaintext byte is recovered: . One byte decrypted without the key.
Repeat for the second-to-last byte, now targeting 0x02 0x02 padding. Then the third byte with 0x03 0x03 0x03. Byte by byte, right to left, the entire block is recovered.
The POODLE attack (2014) exploited exactly this oracle in SSL 3.0. The Lucky Thirteen attack (2013) exploited timing differences in TLS padding validation. These attacks are why TLS 1.3 removed all CBC cipher suites entirely. The only cipher modes allowed in TLS 1.3 are AEAD constructions that cannot produce padding oracles.
You can use AES-256-GCM with perfect nonce handling and constant-time code. None of it matters if the key is hardcoded in your source code, committed to Git, logged in plaintext, or never rotated.
Each of these has caused a real-world breach. They are listed roughly in order of how often they occur.
Found by static analysis, leaked via open-source repositories, visible in decompiled binaries.
Visible in process listings, crash dumps, container inspection, and CI/CD logs.
Debug logging that prints request bodies, connection strings, or configuration objects containing key material.
A single key compromise exposes all data ever encrypted with that key. Without rotation, the blast radius is unlimited.
Using the same key for encryption and authentication, or for different services. One compromised context exposes everything.
Six code snippets, six cryptographic bugs. Can you find them all?
const ENCRYPTION_KEY = "super-secret-key-12345678901234";
const cipher = crypto.createCipheriv("aes-256-gcm", ENCRYPTION_KEY, iv);GitHub's secret scanning has found millions of private keys, API tokens, and encryption keys committed to public repositories. Key management is not a feature you add later. It is a requirement from day one.
Every failure in this chapter has happened in production, at scale, affecting millions of users. These are not theoretical attacks. They are engineering mistakes made by skilled teams under real-world constraints.
A maintainer commented out two lines that seeded the random number generator, reducing all key generation to roughly 32,000 possible keys for two years. Every SSL certificate, SSH key, and VPN key generated on Debian during this period was trivially breakable.
Sony used a constant value for the random nonce k in every ECDSA signature. Two signatures with the same k and basic algebra were enough to extract the private key. Hackers used it to sign their own code and run it on any PS3.
A buffer over-read in OpenSSL's heartbeat extension leaked up to 64 KB of server memory per request, including private keys and session tokens. Not a cryptographic flaw. A bounds-checking bug in C code.
Apple's TLS implementation had a duplicated "goto fail;" line that skipped certificate signature verification entirely. Any certificate was accepted as valid. The fix was a single line deletion.
A padding oracle in SSL 3.0's CBC implementation allowed decryption of HTTPS session cookies. The attack required roughly 256 requests per byte. This led to the deprecation of SSL 3.0 entirely.
Replaying message 3 of Wi-Fi's four-way handshake forced the client to reinstall an already-used key with a reset nonce counter. This turned WPA2 encryption into a nonce-reuse attack, allowing decryption of Wi-Fi traffic.
Notice the pattern. Not a single one of these was caused by someone breaking AES, RSA, or SHA-256. The algorithms work. The implementations do not.
You cannot eliminate all bugs, but you can build systems that are hard to misuse. Make the right thing easy and the wrong thing impossible.
Use libsodium, Web Crypto, or your language's standard crypto library. Never implement primitives yourself.
Use AEAD modes (AES-GCM, ChaCha20-Poly1305). Never use a cipher mode without authentication.
Use constant-time comparison for MACs, tokens, and passwords. Your language's == operator is not constant-time.
Use a KMS. Rotate keys. Separate keys by purpose. Never log, hardcode, or commit keys.
If decryption fails, reject the data entirely. Do not return partial results or detailed error messages that reveal why.
Subscribe to CVE feeds for your crypto libraries. Heartbleed persisted on patched systems for months because operators did not update.
The common thread: reduce the surface area for mistakes. Use AEAD instead of separate encrypt-and-MAC. Use a KMS instead of manual key files. Use constant-time comparison by default. The best defense against cryptographic failure is an API that makes misuse difficult.