Most crypto bugs I have seen were not caused by someone inventing a new cipher.
They were caused by using a good cipher in the wrong mode, reusing an IV, skipping authentication, or copying a Stack Overflow snippet without understanding the contract.
If you are building a normal application in 2026, my default advice is simple:
Prefer authenticated encryption like AES-GCM. Avoid raw AES-CBC unless you know exactly how you will authenticate the ciphertext.
The Quick Map
| Mode | What it gives you | Main risk |
|---|---|---|
| AES-ECB | encryption only | leaks patterns, avoid it |
| AES-CBC | encryption only | needs padding and MAC |
| AES-CTR | stream-like encryption | nonce reuse is dangerous |
| AES-GCM | encryption + authentication | nonce must be unique |
The important word is authentication.
Encryption hides content. Authentication tells you whether the ciphertext was modified.
Never Use ECB
ECB encrypts equal plaintext blocks into equal ciphertext blocks.
That means patterns can leak.
same plaintext block -> same ciphertext block
same plaintext block -> same ciphertext block
This is why ECB is the classic "do not use this" example.
CBC Needs More Than Encryption
CBC uses an initialization vector:
plaintext + key + IV -> ciphertext
The IV must be unpredictable and unique for the message.
The bigger issue: CBC does not authenticate data by itself.
That means this is incomplete:
ciphertext = AES-CBC-Encrypt(key, iv, plaintext)
You also need a MAC, usually encrypt-then-MAC:
ciphertext = AES-CBC-Encrypt(encKey, iv, plaintext)
tag = HMAC(macKey, iv + ciphertext)
If you skip the MAC, attackers may be able to tamper with ciphertext and learn things from error behavior.
Why I Prefer GCM
AES-GCM gives encryption and authentication together:
plaintext + key + nonce + aad -> ciphertext + tag
You still need to manage nonces correctly, but the API nudges you toward the right model.
Node.js example:
import crypto from "node:crypto";
const key = crypto.randomBytes(32); // AES-256
const iv = crypto.randomBytes(12); // recommended GCM nonce size
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const ciphertext = Buffer.concat([
cipher.update("secret message", "utf8"),
cipher.final(),
]);
const tag = cipher.getAuthTag();
console.log({ iv, ciphertext, tag });
Decrypt:
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
const plaintext = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
console.log(plaintext.toString("utf8"));
If the ciphertext or tag is modified, decryption fails.
That is exactly what you want.
Practical Rules
- Do not use ECB.
- Do not hardcode keys.
- Do not reuse GCM nonces with the same key.
- Do not store encryption keys beside encrypted data.
- Do not invent your own padding or MAC scheme.
- Use high-level libraries when possible.
Where Block Ciphers Still Matter
You may hit this when building:
- encrypted backups
- secure tokens
- internal secrets tooling
- file encryption
- compliance-heavy backend systems
For normal password storage, do not use AES. Use a password hashing algorithm such as Argon2, bcrypt, or scrypt.
That distinction is important:
password storage -> hash
data secrecy -> encryption
Final Thought
Crypto code should be boring. If your encryption design feels clever, slow down.
For most application work, AES-GCM or a trusted high-level library is the safer path than hand-rolling AES-CBC with padding and MAC logic.
Have you ever had to review encryption code in an application backend?
Top comments (0)