
The auth tag is not optional
On this page
I was reviewing a small encryption module, the kind that guards a handful of secrets in a database. It used AES-256-GCM from the standard library, which is the right choice. The primitives were all there. So I nearly moved on.
Then I checked whether it kept the auth tag. That is the whole ballgame.
GCM gives you two things, not one#
AES-256-GCM is authenticated encryption. That word “authenticated” is not decoration. It means the cipher produces two outputs, and you need both.
The first is the ciphertext, the scrambled secret. The second is a 16-byte authentication tag, a short fingerprint computed over the ciphertext during encryption. On decrypt you recompute the fingerprint and compare. If it does not match, the library refuses to hand you plaintext.
This module did it correctly. A fresh random initialisation vector for every encryption, so identical plaintext never encrypts to identical ciphertext. The auth tag captured on the way out and verified on the way back. A proper key-derivation function on the password path, so a passphrase becomes a key the slow, salted way rather than being hashed once and hoped over.
The tag is what makes tampering fail loudly#
Here is the failure I want you to feel, because it is silent.
GCM is really two things bolted together: a stream cipher that turns plaintext into ciphertext, and an authentication tag computed over that ciphertext. The stream cipher part does not need the tag to run. The tag is a separate integrity check that a correct decrypt verifies before it hands you any plaintext. The mistake is treating GCM like plain encryption: capturing the IV and ciphertext but dropping the tag, so there is nothing left to verify. What you have then is the stream cipher on its own, confidentiality with no integrity.
Now an attacker flips a few bytes of your stored ciphertext. Verify the tag and decryption fails loudly, so you know the data was touched. Drop it, and there is nothing to fail: the stream cipher turns the mangled bytes into different plaintext than you stored, and nothing complains.
Drop the auth tag and you have not built weaker encryption. You have built unauthenticated encryption, and a tampered ciphertext decrypts as if nothing happened.
The everyday version: a safe that scrambles whatever you lock inside but never notices if someone swaps the papers. Anyone who could reach the stored data could quietly rewrite your secrets, and the app would unlock the forgery as though you had put it there yourself.
Confidentiality without integrity is a trap, because it looks identical in every test you are likely to write. The only test that catches it is one that mutates the ciphertext and expects a thrown error.
One column, three parts#
The tidy trick in this module was serialisation. A GCM secret is really three values, and you do not want three columns for one secret.
So it packs them into a single colon-delimited hex string:
iv:tag:ciphertextEncrypt-and-serialise produces that string. Deserialise-and-decrypt splits it back into three parts, feeds the tag to the verifier, and returns plaintext or throws. One round-trip pair, one database column, nothing to forget. And if the key is unset, the module throws on the spot rather than encrypting under an empty key and pretending it worked.
The lesson#
With authenticated encryption, storing and verifying the auth tag is not an optional extra. It is the difference between encryption that detects tampering and encryption that quietly serves an attacker’s edits. Keep the tag, verify the tag, and write the one test that corrupts a byte and expects a failure.
The serialisation is the easy, satisfying part. An iv:tag:ciphertext string keeps
your schema to one column and travels anywhere a string travels.
One honest caveat, because it is the residual risk here: this scheme runs on a single master key with no rotation. That is fine for a first cut and worth naming, because the day you need to rotate, you will wish you had a key id sitting next to the ciphertext. Ship the tag today. Plan for rotation before you need it.


