AES-GCM Nonce Reuse: Catastrophic Key Recovery
AES-GCM (Galois/Counter Mode) is the right choice for authenticated encryption. It provides both confidentiality and integrity with a single pass. But it has an architectural weakness: the nonce (initialisation vector) must be unique for every encryption operation under a given key. Reusing a nonce with the same key is not a degraded-security scenario β it is a complete cryptographic collapse.
The mathematics: GCM's keystream is produced by encrypting a counter initialised from the nonce. If two plaintexts P1 and P2 are encrypted with the same key and the same nonce, their ciphertexts satisfy C1 XOR C2 = P1 XOR P2. An attacker who sees both ciphertexts and knows either plaintext recovers the other immediately. Worse, the authentication tag β which is supposed to prevent tampering β is computed using the same keystream. A nonce-reuse attack can recover the authentication key, allowing the attacker to forge arbitrary authenticated ciphertexts that pass integrity checks.
Real-world occurrence: Nonce reuse happens when: a fixed or hardcoded nonce is used, a counter is reset after restart without re-keying, a random nonce generator is seeded predictably, or a 96-bit nonce is generated randomly in a high-volume system (birthday paradox: collision probability reaches 50% at approximately 2^48 messages).
For high-volume systems where random 96-bit nonces risk birthday collision, use AES-GCM-SIV (nonce-misuse resistant) which degrades gracefully on nonce reuse, or switch to a nonce scheme based on a monotonically increasing counter combined with instance-specific salt.
ECB Mode: The Mode That Leaks Structure
AES-ECB (Electronic Codebook) mode encrypts each 16-byte block independently with the same key. Identical plaintext blocks produce identical ciphertext blocks. The classic demonstration is the "ECB penguin" β a bitmap of Tux the Linux penguin encrypted with AES-ECB. The output is encrypted but the penguin shape is clearly visible because the repeated background pixels produce the same ciphertext block every time.
In practice, ECB leaks structural information about plaintext. For user data with predictable patterns β user IDs, status fields, repeated padding β an attacker observing ECB-encrypted ciphertext can deduce information about the plaintext without knowing the key. More critically, ECB blocks can be rearranged: if a record contains [account_id block][amount block][recipient block], an attacker can swap blocks from different records to produce a valid-seeming ciphertext that decrypts to a manipulated record.
ECB has no legitimate use case in application code. The correct substitution is AES-GCM (preferred) or AES-CBC with a random IV and a separate HMAC for authentication (legacy compatibility only).
Timing Attack Leaks: Comparing Secrets in Variable Time
A timing attack exploits the fact that operations take different amounts of time depending on their input. The canonical example is string comparison: most string equality functions in most languages return False as soon as they find a differing byte. A comparison against a 32-byte HMAC that fails on the first byte takes measurably less time than one that fails on the 31st byte.
This means an attacker can determine secret bytes one at a time by measuring response time. Submit a candidate MAC where the first byte is 0x00. Measure the response time. Submit with first byte 0x01. Compare times. When response time increases, the first byte is correct β the comparison went further before failing. Repeat for each subsequent byte. 256 attempts per byte, 32 bytes = 8192 requests to fully recover a 256-bit HMAC. This is entirely practical over a network with median response time measurements.
Constant-time comparison must be used for: HMAC verification, session token comparison, API key validation, password comparison (before the hash, if checking stored tokens), and any other comparison where the equality result is the authentication decision. In Go: subtle.ConstantTimeCompare(). In Java: MessageDigest.isEqual(). In Ruby: ActiveSupport::SecurityUtils.secure_compare(). Do not implement your own.
Padding Oracle Attacks
AES-CBC requires plaintext to be a multiple of 16 bytes. PKCS#7 padding extends the plaintext to the required length: if 5 bytes of padding are needed, 5 bytes of value 0x05 are appended. On decryption, the receiver strips padding β and if the padding is malformed, should return an error.
A padding oracle is any observable difference in server behaviour between "padding valid, message invalid" and "padding invalid". It does not need to be an explicit error message β a different HTTP status code, a different response time, or even a different error page is sufficient. Given a padding oracle, an attacker can decrypt any ciphertext block-by-block without the key, using roughly 128 queries per byte (32 bytes per block, 4 values on average per byte).
The attack exploits CBC's block chaining. Each decrypted plaintext block is XORed with the previous ciphertext block. An attacker who controls the previous ciphertext block can manipulate the XOR to produce arbitrary padding bytes in the decrypted output. By finding the byte value that produces valid padding, they recover the intermediate decryption output, and from that the plaintext.
ASP.NET ViewState (2010), OWASP's POODLE descendant (2014), JSON Web Encryption with AES-CBC (2017): Padding oracle attacks have broken production systems repeatedly. The pattern recurs because CBC + PKCS#7 is extremely common and the oracle condition is easy to introduce accidentally.
The correct fix is to use authenticated encryption (AES-GCM or ChaCha20-Poly1305). Authentication is verified before decryption β malformed ciphertext is rejected before the padding check ever runs, eliminating the oracle condition entirely. Never use CBC without an authentication tag verified in constant time before decryption.
IV Reuse in AES-CBC
AES-CBC requires a unique, unpredictable IV for each encryption. Unlike AES-GCM, reusing a CBC IV does not produce immediate keystream recovery β but it does leak information. If two messages are encrypted with the same IV and key, an attacker who knows one plaintext can determine whether and where the other plaintext matches it, up to the first differing block. This breaks semantic security: the ciphertext reveals information about the plaintext beyond its length.
The more dangerous form: using a predictable IV. If an attacker can predict the IV that will be used for the next encryption (e.g., it is the last ciphertext block of the previous message β which was the actual behaviour of TLS 1.0's CBC), they can mount a Chosen Plaintext Attack (CPA) to recover secrets. This was the BEAST attack against TLS 1.0.
A safe IV for AES-CBC is a 16-byte value generated with a cryptographically secure random number generator for each encryption operation. The IV is not secret and is transmitted alongside the ciphertext. Sequence numbers, timestamps, and counters are all wrong choices for IVs because they are predictable.
Detection in Code Review and SAST
These bugs are all detectable with targeted code review patterns:
- ECB mode: Search for
AES/ECB,Cipher.getInstance("AES")(Java default is ECB),AES.new(key, AES.MODE_ECB) - Fixed nonces/IVs: Any IV or nonce that is a literal byte string, a constant, or derived from a non-random source
- Non-constant-time comparisons:
==on HMAC/token strings,memcmp()on secrets,strcmp()on API keys - CBC without authentication: Any use of CBC mode without a corresponding HMAC-SHA256 verify-before-decrypt pattern
- Padding error differentiation: Error handlers that return different responses based on whether decryption failed vs. decryption succeeded but message parsing failed
Most SAST tools detect ECB mode and hardcoded IVs reliably. Non-constant-time comparisons and padding oracle conditions require data-flow analysis and are caught less consistently β manual review of authentication code paths remains important.
Safe Patterns
- New code: Use AES-256-GCM with a randomly generated 12-byte nonce per message. Use
libsodium/NaClabstractions where available β they make the correct choice unavoidable. - Secret comparison: Always
hmac.compare_digest(),subtle.ConstantTimeCompare(), or platform equivalents. Never==on authentication material. - CBC legacy: If CBC must be used, generate a random 16-byte IV per encryption, compute HMAC-SHA256 over IV + ciphertext, verify the HMAC in constant time before decryption, return a uniform error on any failure without differentiating the cause.
- High-volume nonce management: For AES-GCM at scale, use a counter-based nonce scheme with a per-instance random prefix, or use AES-GCM-SIV (XSalsa20-Poly1305 via libsodium is also nonce-misuse resistant and simpler to deploy correctly).
The rule: Cryptographic code should be boring. Use the highest-level abstraction available. If you find yourself constructing IVs, concatenating MACs, or writing XOR operations in application code, you are probably implementing something that has a higher-level safe API that you should use instead.