DEV Community

Cover image for Encryption Isn't Enough — You Also Need to Know If Someone Tampered With Your Data
Bijan
Bijan

Posted on Originally published at linkedin.com AI-assisted

Encryption Isn't Enough — You Also Need to Know If Someone Tampered With Your Data

Most people think encryption's job is to keep data secret. That's only half the job — and the half it doesn't cover is exactly where a lot of otherwise-careful implementations quietly break.

This is part 4 of a series documenting what I'm learning building CryptoGraphy.

The gap plain encryption doesn't cover

Encryption without a way to detect tampering means an attacker who can intercept your ciphertext can flip bits in it without ever knowing the key. They can't read your plaintext, but they don't need to — depending on the cipher mode, they can sometimes predictably corrupt it, and your system will happily decrypt the tampered ciphertext into garbage, or in worse cases, into something that looks structurally valid but isn't what was actually sent.

Confidentiality answers "can someone read this." It says nothing about "can someone change this without me noticing." Those are two separate guarantees, and you need both.

Why AES-GCM instead of plain AES-CBC

crypto.py uses AESGCM specifically, not plain AES in CBC mode:

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def encrypt(plaintext: bytes, key: bytes) -> tuple[bytes, bytes]:
    nonce = os.urandom(12)
    aes = AESGCM(key)
    ciphertext = aes.encrypt(nonce, plaintext, None)
    return nonce, ciphertext


def decrypt(ciphertext: bytes, key: bytes, nonce: bytes) -> bytes:
    aes = AESGCM(key)
    return aes.decrypt(nonce, ciphertext, None)
Enter fullscreen mode Exit fullscreen mode

GCM stands for Galois/Counter Mode, and it belongs to a category called AEAD — Authenticated Encryption with Associated Data. In one pass, it does two things at once:

  1. Encrypts the data, so it stays confidential.
  2. Generates an authentication tag, proving the ciphertext wasn't modified after encryption.

That tag is embedded in the output AESGCM.encrypt() returns. When you call decrypt(), the library recomputes and checks that tag before returning any plaintext at all.

What happens when someone tampers with it

Change even a single bit of the ciphertext after encryption, and AESGCM.decrypt() doesn't return corrupted data — it raises an exception and refuses to return anything. That's the actual security property: failure is loud and immediate, not silent and exploitable. A system built on AES-GCM either gets the exact plaintext that was encrypted, or it gets nothing at all. There's no third outcome where it silently accepts tampered data.

Plain AES-CBC doesn't give you this. It'll happily decrypt tampered ciphertext into whatever the tampering produces — corrupted plaintext, or in some poorly-designed protocols, plaintext an attacker had meaningful influence over. CBC gives you confidentiality. It gives you nothing for integrity, and bolting integrity on afterward (encrypt-then-MAC, done correctly) is exactly the kind of extra step that's easy to get subtly wrong if you're rolling it yourself.

Why this distinction matters more than it sounds like it should

"This is private" and "this is private, and I can trust it wasn't touched" sound like a minor wording difference. In practice they're the difference between a system that's vulnerable to bit-flipping and ciphertext manipulation attacks, and one that simply isn't — for free, as a property of the cipher mode you picked, not something you have to remember to bolt on separately.

This is also why AEAD ciphers like AES-GCM (and alternatives like ChaCha20-Poly1305) have become the default recommendation over plain block cipher modes for new designs: they make the "did I remember to add integrity checking" question disappear by folding it into the encryption primitive itself.

The takeaway

Confidentiality and integrity are two different guarantees, and a lot of homegrown or legacy crypto quietly only provides one of them. Picking AES-GCM over plain AES-CBC isn't a performance or convenience choice — it's closing a specific, well-understood gap that shows up in real systems more often than it should.


Next in this series, the last one: why a nonce has to be unique every single time, and what actually happens if you reuse one under AES-GCM — it's worse than you'd expect.

Have you run into a system that had encryption but no integrity check? What ended up happening?

Top comments (0)