</>
Back to Blog
Crypto 2026-06-02 9 min read

AES, Properly Explained

AES is twenty-five years old. NIST selected Rijndael β€” Joan Daemen and Vincent Rijmen's submission β€” over four other finalists in October 2000, after a public competition that started in 1997. The result is not novel, and that is exactly why it's trustworthy. AES has been studied harder than any cipher in history, in the open, and is still standing.

AESRijndaelAES-GCMBlock CipherAuthenticated Encryption

A standing caveat for this whole topic: don't roll your own crypto. The block cipher and modes described below are correct in theory, and unbelievably easy to misuse in practice. Use libsodium, your platform's cryptographic API, or your TLS library. The notes below are for understanding what your library is doing, not for replacing it.

What it actually is

AES is a block cipher. It takes a 128-bit input block and a key, and produces a 128-bit output block. That's the entire primitive. Encrypt the same block twice with the same key, you get the same output. Decrypt the output, you get the input back.

The key can be 128, 192, or 256 bits. AES-128, AES-192, AES-256 β€” same algorithm, different number of rounds (10, 12, 14 respectively) and different key schedules. AES-128 is faster; AES-256 is slower but provides a margin of safety against future attacks. Both are considered secure today.

This block-by-block thing is the entire reason "modes of operation" exist. Real messages aren't 128 bits. To encrypt anything longer, you have to decide how to chain blocks together β€” and almost every AES vulnerability in the wild comes from that chaining decision, not from AES itself.

Why ECB is forbidden

The simplest mode is ECB: split your message into 128-bit blocks and encrypt each one independently with the same key.

The problem is immediate. Identical input blocks produce identical output blocks. If your data has any repeating structure β€” repeated headers, runs of the same byte, repeated dictionary words β€” that structure shows up in the ciphertext.

The canonical demonstration is encrypting a bitmap of the Tux Linux mascot in ECB. The pixel values repeat in regular patterns; the encrypted result still has Tux clearly visible, just with the colors scrambled. Search "ECB penguin" β€” it's unforgettable once you see it.

ECB hides nothing structural about the plaintext. It is unsafe for almost every real use. The only legitimate use is encrypting random or pseudorandom data with no structure, and you almost never have that.

If you find ECB in production code, it's a bug. Fix it.

CBC: legacy

CBC (Cipher Block Chaining) addresses ECB's structural leak by XORing each block with the previous ciphertext block before encrypting. The first block has no predecessor, so it's XORed with a per-message IV (initialization vector). The ciphertext is the IV followed by the encrypted blocks.

CBC mode requires:

  • A unique IV per message. Reusing the IV with the same key leaks information; it doesn't break the key, but two messages with shared prefixes show identical ciphertext for those prefixes.
  • An unpredictable IV for messages where the attacker can choose plaintext. Predictable IVs enabled the BEAST attack against TLS in 2011.
  • Padding. AES blocks are 128 bits; messages aren't always multiples. PKCS#7 padding is the standard.
  • A separate authentication tag. CBC by itself is malleable β€” an attacker can flip bits in the ciphertext and produce predictable changes in the plaintext after decryption. This enables the padding oracle attack class.

Because of those last two requirements, CBC is largely retired in modern designs in favor of authenticated modes. TLS 1.3 dropped it. New code shouldn't reach for it.

CTR: secure but unauthenticated

CTR (Counter mode) turns AES into a stream cipher. Encrypt a counter (incrementing per block) under the key, XOR the result with the plaintext. No padding required, parallelizable, decryption is identical to encryption.

CTR's hard requirement: never reuse the (key, counter) pair. If you do, an attacker who knows or guesses any plaintext can recover the keystream and decrypt every other message that reused that counter. The same property β€” XOR of two ciphertexts equals XOR of the plaintexts β€” that makes CTR fast also makes counter reuse catastrophic.

By itself, CTR is unauthenticated. It needs to be combined with a MAC (HMAC-SHA256, for example) for integrity. Doing this correctly β€” encrypt-then-MAC, separate keys, MAC over IV+ciphertext β€” is one of those "looks easy, actually subtle" things that's the reason "don't roll your own" exists.

GCM: the modern default

GCM (Galois/Counter Mode) is CTR mode plus a built-in authentication tag, giving you authenticated encryption with associated data (AEAD). It produces ciphertext, an IV (called a nonce in this context), and a 128-bit tag. Decryption verifies the tag before returning plaintext; tampered ciphertext fails to decrypt.

This is the default for TLS 1.3, IPsec, SSH, and most application-level encryption today. AES-128-GCM and AES-256-GCM are what you should reach for in 2026.

GCM has one rule that you must not break: never reuse a nonce with the same key. GCM nonce reuse doesn't merely leak β€” it allows full authentication key recovery, which lets an attacker forge arbitrary messages under your key. This is much worse than CTR nonce reuse.

The standard nonce length is 96 bits. The conventional approach is to use a counter for nonces, or a random 96-bit value. Random has a birthday-bound problem: at ~2⁴⁸ messages with the same key, you have a meaningful chance of collision. For most applications this is fine; for very high-volume systems, use a deterministic counter or rotate keys.

If you're considering AES-GCM-SIV instead β€” that's a misuse-resistant variant where nonce reuse merely leaks (rather than catastrophically breaking authentication). Use it if your nonce discipline isn't airtight.

AES vs ChaCha20-Poly1305

AES has hardware acceleration on every modern CPU (AES-NI, ARMv8 crypto extensions). On those platforms it's stupendously fast β€” a few cycles per byte.

On hardware without acceleration β€” older mobile devices, embedded systems, some constrained environments β€” software AES is slow and timing-attack-vulnerable. ChaCha20-Poly1305 was designed to be fast and constant-time in pure software, no hardware support needed. TLS 1.3 supports both.

For client/server with modern hardware: AES-256-GCM. For unknown devices or pure software paths: ChaCha20-Poly1305. Both are AEAD, both are considered secure, performance is the only deciding factor on most platforms.

Key derivation: where most apps go wrong

You almost never have a 256-bit random key sitting around. You usually have a password, or a passphrase, or an API key. Using these directly as AES keys is wrong on multiple levels β€” they're not uniformly random and they're not the right length.

The fix is a key derivation function:

  • PBKDF2 β€” old but widely supported. Use SHA-256, at least 600,000 iterations as of 2025 OWASP guidance. Slow on modern GPUs by design.
  • scrypt β€” memory-hard, harder to attack on GPUs. Reasonable default.
  • Argon2id β€” current best practice, winner of the 2015 Password Hashing Competition. Use it if your platform has a reliable implementation.
  • HKDF β€” for deriving keys from existing high-entropy secrets (not passwords).

Storing a password and re-deriving the key per encryption is not the right shape. Derive once, cache the key in memory for the session, store nothing on disk. Different file β†’ different IV / nonce.

Common pitfalls

  • ECB. The penguin should be reason enough.
  • IV / nonce reuse. CBC: leaks. CTR: leaks badly. GCM: catastrophic.
  • Treating an IV as a secret. It's not. Send it in the clear alongside the ciphertext.
  • Confusing IV (for CBC) and nonce (for GCM/CTR). Same field, different requirements.
  • Encrypt-then-MAC vs MAC-then-encrypt. Encrypt-then-MAC is correct. The other order has caused real attacks (POODLE, Lucky 13).
  • Using ECB to "encrypt" small blocks because "the data is too small for a block cipher to need a mode." It's not.
  • Forgetting authentication. Symmetric encryption without authentication is an attack waiting to happen.
  • Storing keys in source code or environment variables that get logged. Use platform key management (AWS KMS, Cloud KMS, OS keychain).

Practical rules

  • AES-256-GCM for new code. Or ChaCha20-Poly1305.
  • 96-bit random nonces for GCM. Track them so you can prove uniqueness if asked.
  • Derive keys with Argon2id (or PBKDF2 with high iteration count if Argon2 isn't available).
  • Use a vetted library. libsodium / NaCl, your platform's crypto, your TLS stack. Not your own implementation.
  • Authenticated encryption only. If you find yourself reaching for "encrypt only," you're in territory where you shouldn't be.
  • Never reuse the same key for both encryption and signing. Different keys, derived separately.
  • Rotate keys. The cost is low and the upside is enormous if you ever need to recover from a leak.

Encrypt and decrypt locally

The AES tool on this site supports CBC, CTR, and GCM modes with PBKDF2 key derivation. Useful for understanding the trade-offs by example. All cryptographic operations happen client-side β€” no data leaves the browser.

Open the AES tool

Related guides

Keep the session useful with adjacent reading instead of exiting after one article.

View all guides

Cookie Consent

We use cookies to enhance your experience and show relevant ads. You can customize your preferences.