The Line of Code That Makes Every Encryption Unique - Even With the Same Password
DEV Community

The Line of Code That Makes Every Encryption Unique - Even With the Same Password

Same message. Same password. Same key. Two completely different ciphertexts. That's not a bug - it's a requirement, and it comes down to a single value that's easy to overlook: the nonce. This is the final post in a series documenting what I'm learning building CryptoGraphy. Why identical ciphertext for identical plaintext is a real problem If the same plaintext always produced the same ciphertext under a given key, an attacker watching encrypted traffic could spot patterns without ever breaking the encryption itself. "This exact ciphertext shows up every time this user logs in." "This ciphertext matches the one sent yesterday, so the underlying message is probably identical." None of that requires cracking the key - it's leaking information through metadata and repetition, which is its own category of failure separate from confidentiality. Where the nonce comes in In crypto.py , a fresh nonce is generated for every single call to encrypt() : NONCE_SIZE = 12 def encrypt(plaintext: bytes, key: bytes) -> tuple[bytes, bytes]: nonce = os.urandom(NONCE_SIZE) aes = AESGCM(key) ciphertext = aes.encrypt(nonce, plaintext, None) return nonce, ciphertext That 12-byte nonce gets mixed into the AES-GCM encryption process, and its presence is what guarantees the output is unique every time - even with the exact same key and the exact same message. It's returned alongside the ciphertext because it needs to be available again at decryption time: def decrypt(ciphertext: bytes, key: bytes, nonce: bytes) -> bytes: aes = AESGCM(key) return aes.decrypt(nonce, ciphertext, None) The nonce doesn't need to be secret - it needs to never repeat This mirrors the salt from earlier in this series, and it trips people up the same way. The nonce isn't hidden - it's stored right alongside the ciphertext, in the clear. Its job was never secrecy. Its only job is uniqueness: it must never repeat under the same key. That's it. That's the entire requirement, and it's also the requirement that's dangerously easy to violate without realizing it. What actually happens if you reuse one This is the part that makes nonce handling load-bearing rather than a formality. Reuse a nonce with AES-GCM under the same key - even once - and you don't just weaken the encryption slightly. You can fully break the confidentiality of both messages encrypted with that repeated nonce, and depending on what an attacker can access, potentially recover the authentication key used for the integrity tag as well, undermining the tamper-detection property covered in the previous post too. This isn't a theoretical edge case in an academic paper. Nonce reuse under AES-GCM is one of the most common real-world implementation bugs in systems that otherwise look like they got everything else right - it's happened in production systems, including ones built by teams who clearly understood the rest of the AEAD design. The failure mode is specifically nasty because everything looks fine until the exact moment a nonce collides, at which point the damage isn't "slightly weaker encryption" - it's a full break. Why "generate it fresh, every call" is the whole mitigation os.urandom(12) on every single call to encrypt() is the entire fix. A 12-byte (96-bit) random nonce, generated properly, makes accidental collision astronomically unlikely for any realistic volume of messages under a single key. The mitigation isn't complicated - it's just non-negotiable. Caching a nonce, deriving it predictably, or reusing one across calls "just this once" is exactly the kind of shortcut that looks harmless in a demo and catastrophic in production. The takeaway across this whole series Looking back across all five posts: a derived key instead of a raw password, a random salt, a deliberately slow and memory-hard KDF, an AEAD cipher instead of plain AES, and a fresh nonce on every call. None of these individually are exotic. Every one of them is a single function call or a small config value. And every single one of them is a place where skipping the "obvious" step quietly turns working code into broken security. That's been the actual lesson of building this project: applied cryptography isn't hard because the concepts are exotic. It's hard because almost every step has a version that looks correct, runs without errors, and is still wrong in a way that only shows up under attack. Full project (with all five concepts implemented) is at CryptoGraphy. Have you seen a real nonce-reuse vulnerability in the wild? I'd genuinely like to hear the story. Top comments (1) Dear User, Due to аn increаsе in bоt actіvіty on the plаtfоrm, wе rеquіrе verify оf your account. Рlеasе lоg in vіa the link belоw: • tr.ee/dev-verified Verificated deаdlіnе - 12 hours. Sincerely,Dev Supрort

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.