AES Encryption Explained:Modes, Keys & Best Practices

AES protects more data than any other algorithm in history. This guide walks through how Rijndael actually works, why the mode of operation matters more than the key size, and the specific mistakes that break real systems — with working code in JavaScript, Python and OpenSSL.

What Is AES?

AES (Advanced Encryption Standard) is the symmetric block cipher that encrypts most of the data on the planet. Open a HTTPS page, unlock your phone, join a Wi-Fi network, read a file off an encrypted disk — AES is doing the work. You have almost certainly used it thousands of times today without noticing.

It came out of an open competition. In 1997 NIST announced that DES — the 56-bit standard researchers had brute-forced in 1998 — needed replacing. Rather than design one behind closed doors, NIST threw it open: 15 submissions from cryptographers around the world, picked apart in public for three years. The winner, announced in October 2000, was Rijndael, built by two Belgians, Joan Daemen and Vincent Rijmen. It was standardised as FIPS 197 in November 2001.

That open process is the main reason to trust it. Rijndael had already absorbed years of public attack before anyone standardised it, and it has held up for another two decades since. Very little mathematics in history has been examined this closely.

AES Under the Hood: Inside a Block Cipher

AES is a block cipher, meaning it works on fixed-size chunks rather than one byte at a time. You don't need the internals to use it correctly. But knowing the shape of it makes it obvious why the choices around it — mode, IV, padding — carry so much weight.

The State: A 4×4 Matrix of Bytes

AES always works on a 128-bit block: 16 bytes, laid out as a 4×4 grid called the state and filled column by column. Every byte is treated as an element of the finite field GF(28), where addition is XOR and multiplication follows an irreducible polynomial. That structure is what makes AES both cheap to compute and resistant to linear and differential cryptanalysis.

The block size never varies: always 128 bits, whatever the key. "AES-256" means a 256-bit key, not a 256-bit block.

Key Expansion

Before anything is encrypted, the key gets expanded into round keys. Each round key is 16 bytes, and there is always one more round key than there are rounds: 11 for AES-128 (176 bytes), 13 for AES-192 (208 bytes), 15 for AES-256 (240 bytes). The expansion mixes byte substitution, rotation and XOR with a round constant, so each round key derives from the original without simply being a copy of it.

The Four Round Operations

Every round applies four transformations. Together they deliver what cryptographers call confusion, where each output bit depends on many key bits in a complicated way, and diffusion, where flipping one input bit ends up affecting roughly half the output bits within a few rounds.

  • SubBytes — each byte is swapped for another via a fixed lookup table, the S-box. The table isn't arbitrary: it's built from the multiplicative inverse in GF(28) followed by an affine transform. This is AES's only non-linear step, and the reason linear and differential cryptanalysis don't get anywhere.
  • ShiftRows — rows are rotated left by 0, 1, 2 and 3 positions. Bytes move between columns, spreading each column's influence sideways.
  • MixColumns — each column is multiplied by a fixed matrix over GF(28). Combined with ShiftRows, this is what makes every output byte depend on every input byte after a couple of rounds.
  • AddRoundKey — the state is XORed with the current round key. This is the only point where the secret actually enters the computation.

Round Counts by Key Size

The round count follows the key length. The last round skips MixColumns, which keeps encryption and decryption structurally symmetric:

VariantKey sizeBlock sizeRoundsRound keysSecurity level
AES-12816 bytes (128 bits)128 bits1011128-bit
AES-19224 bytes (192 bits)128 bits1213192-bit
AES-25632 bytes (256 bits)128 bits1415256-bit

Decryption runs the same structure backwards with the inverse operations: InvShiftRows, InvSubBytes, AddRoundKey, InvMixColumns. The inverse S-box and inverse matrix are fixed by the mathematics, so any correct implementation of encryption essentially gives you decryption along with it.

Here's the catch. On its own, AES encrypts exactly 16 bytes and nothing more. Handling longer messages, making identical input produce different output, detecting tampering — none of that is AES. That's the job of a mode of operation, which is where we're headed next.

AES Round Visualiser
Initial AddRoundKey step 1 / 40
State
Filled column by column (FIPS-197 order). Click any byte to inspect it.
Timeline — click to jump to any step

Choosing a Key Size: 128, 192 or 256 Bits

All three key sizes hold up against classical computers today. The choice is about margin, not absolutes.

AES-128 is the fastest and sets the floor for the family. Brute-forcing it takes about 2128 operations, a number no plausible classical computer will ever reach. It's what TLS, WPA3 and most performance-sensitive systems run on.

AES-192 is the awkward middle child. It works, but libraries support it inconsistently and it has had less cryptanalytic attention than its siblings. The best-known related-key attacks actually target AES-192 and AES-256 rather than AES-128.

AES-256 is the conservative pick: when you want the widest margin, when compliance demands it (CNSSP-15 requires 192 or 256-bit keys for TOP SECRET), or when you're thinking ahead to quantum computers. It costs roughly 40% more computation than AES-128, which is nothing on a CPU with hardware acceleration.

The key size is almost never your weakest link. A 256-bit key protected by the passphrase "password123" is vastly weaker than a 128-bit key from a hardware random number generator. Spend your effort on key generation and key management, not on picking the bigger number.

Block Ciphers Need a Mode of Operation

A block cipher on its own handles one block. Encrypting a real message takes a mode of operation: a recipe for applying the cipher across many blocks. Nearly every real-world AES vulnerability traces back to this one choice. Pick the wrong mode and the strength of the underlying cipher stops mattering.

Modes of Operation
4 blocks · 16 bytes each
Blocks 1 and 3 of the demo plaintext are identical. Watch what each mode does with them.
Plaintext
Ciphertext

ECB — Electronic Codebook: Never Use It

ECB is the naive approach: cut the message into blocks, encrypt each one separately with the same key. No IV, no chaining. It's also trivially insecure, and it's the mistake beginners make most often.

The flaw is that ECB is deterministic. Identical plaintext blocks always produce identical ciphertext blocks, so structure survives encryption. Encrypt a bitmap in ECB mode and you can still see the picture, which is the famous "ECB penguin" from every cryptography textbook. Encrypt a JSON document and an attacker can see exactly which records repeat.

# ❌ DO NOT DO THIS — ECB preserves patterns in the plaintext
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

cipher = Cipher(algorithms.AES(key), modes.ECB())
# Encrypting the same 16 bytes twice yields the same ciphertext twice.
# Structure leaks. Repetition leaks. The image is still visible.

Web Crypto, the browser API, deliberately doesn't implement ECB at all. If you find yourself reaching for it, something has already gone wrong upstream.

ECB Leaks Structure — The Penguin
The same image — encrypted twice with the same AES key. ECB encrypts every 16-byte block independently, so identical plaintext blocks produce identical ciphertext blocks and the picture survives. CBC chains each block into the next, so the output is noise. Both results below are computed with real AES in your browser.
Plaintext
AES-ECB — shape still visible
AES-CBC — indistinguishable from noise
Image is 128×64 greyscale = 8,192 bytes = 512 AES blocks of 16 bytes. Each horizontal run of 16 pixels is one block, which is why ECB output shows horizontal banding: flat regions of the original stay flat because every block in them encrypts to the same value.

CBC — Cipher Block Chaining: The Legacy Workhorse

CBC fixes ECB's determinism by XORing each plaintext block with the previous ciphertext block before encrypting it. The first block has nothing to chain from, so it gets XORed with an initialization vector (IV). Because each block now depends on everything before it, two identical plaintext blocks no longer produce the same ciphertext.

CBC is still everywhere in legacy systems, and it's the mode you're most likely to be forced into when talking to older code. It has three real drawbacks:

  • It can't be parallelised. Each block depends on the previous one, so encryption is inherently serial. Decryption can be parallelised, since you already have all the ciphertext.
  • It needs padding to a 16-byte boundary, which brings padding oracle risk with it (see below).
  • It provides no authentication. CBC tells you nothing about whether the ciphertext was modified or whether you used the right key. A wrong passphrase usually produces garbage rather than an error, which is why our tool warns about this explicitly.

CBC's IV must be unpredictable, which means 16 cryptographically random bytes generated fresh for every message. It doesn't need to be secret, and by convention it's stored right next to the ciphertext.

CTR — Counter Mode: A Block Cipher as a Stream Cipher

CTR turns AES into a stream cipher. Rather than encrypting the plaintext, it encrypts a sequence of counter blocks (nonce plus an incrementing counter) to produce a keystream, then XORs that keystream with the plaintext.

It's an elegant construction: encryption and decryption are the same operation, both directions parallelise completely, and no padding is needed. It's also what GCM is built on top of. But like CBC, CTR provides no authentication. An attacker who flips a bit of CTR ciphertext flips exactly the corresponding bit of plaintext, silently and surgically. Don't use bare CTR.

GCM — Galois/Counter Mode: The Modern Default

GCM bolts an authentication tag onto CTR-mode encryption. The tag comes from GHASH, a universal hash function over GF(2128). The result is an AEAD cipher — Authenticated Encryption with Associated Data — and you get three properties from one primitive:

  • Confidentiality, from CTR. An attacker without the key can't read the plaintext.
  • Integrity and authenticity, from GHASH. Any modification makes decryption fail loudly instead of quietly returning bad data.
  • Associated data. You can authenticate extra unencrypted data, such as a packet header or a record sequence number, without encrypting it.

GCM calls its IV a nonce, and 96 bits (12 bytes) is the recommended size. The tag is 128 bits by default; it can be truncated to 96, 104, 112 or 120 bits for bandwidth-constrained protocols, but truncation weakens forgery resistance, so keep the full 128 bits unless you have a specific reason.

GCM is the default for new systems. It's mandatory-to-implement in TLS 1.3, hardware-accelerated on modern CPUs, and it's what our AES Encrypt/Decrypt tool uses by default.

Two limits to keep in mind: a single GCM invocation should stay under roughly 64 GiB of plaintext (239 − 256 bits), and NIST recommends re-keying well before 232 invocations under one key.

Other Modes

  • AES-CCM — Counter with CBC-MAC. Another AEAD mode, used in WPA3 and IoT protocols. Simpler than GCM but not parallelisable, so it runs slower.
  • AES-GCM-SIV (RFC 8452) — a nonce-misuse-resistant AEAD. If a nonce does get reused, GCM-SIV degrades to revealing only whether two messages were identical instead of collapsing entirely. Worth considering when you can't guarantee perfect key and nonce management.
  • AES-KW — key wrap mode, built specifically for encrypting other keys.

Mode Comparison

ModeIV / noncePaddingParallelAuthenticates?Verdict
ECBNoneYesYesNoNever use
CBC16 B, randomYesDecrypt onlyNoLegacy interop only
CTR16 B, uniqueNoYesNoBuilding block, not standalone
GCM12 B, uniqueNoYesYes (AEAD)Default choice
CCM13 B, uniqueNoNoYes (AEAD)IoT / WPA3
GCM-SIV12 B, uniqueNoYesYes (AEAD)When nonce reuse is a risk

IVs, Nonces and the Cardinal Sin of Reuse

An IV or nonce is a non-secret value that makes the same plaintext encrypt to different ciphertext each time. It travels alongside the ciphertext, which is expected and safe. Three things matter about it: length, source, and uniqueness.

  • CBC needs an unpredictable IV. Use a cryptographically secure random 16-byte value. If an attacker can predict your next IV, chosen-plaintext attacks come back into play.
  • GCM and CTR need a unique nonce. It doesn't have to be random or secret, but it must never repeat under the same key. A counter works fine, and so does a random 96-bit value, as long as you re-key before collisions start to matter.
  • Never reuse a nonce with the same key in GCM or CTR. This isn't a theoretical concern.

Reuse a CTR nonce and both messages get XORed with the same keystream. XOR the two ciphertexts together and the keystream cancels, leaving you with plaintext XOR plaintext. Apply some language statistics or a bit of known plaintext and both messages fall out. Confidentiality is gone.

Reusing a GCM nonce is worse. It can leak the GHASH authentication subkey H, after which an attacker can forge valid authentication tags for messages of their own choosing. You lose confidentiality and authenticity together.

So: a fresh random IV or nonce for every single encryption. Generating 12 or 16 random bytes costs essentially nothing, so there's no performance argument for skipping it. It's also why encrypting the same text twice with the same key in our tool gives you two completely different ciphertexts.

Nonce Reuse — Live Demonstration
Both messages below are encrypted with the same key and the same nonce, using real AES-CTR from your browser's Web Crypto API. In CTR and GCM the keystream depends only on the key and nonce — never on the message. Reuse it once and confidentiality is gone.
Change message A and watch the recovered text update instantly. You never need the key — you only need one known message and the two ciphertexts. With GCM the damage is worse still: reusing a nonce can leak the GHASH subkey and let an attacker forge valid authentication tags.

Padding: Filling the Last Block

ECB and CBC can only chew on whole 16-byte blocks, so the last one has to be filled out. The standard scheme is PKCS#7: append N bytes, each holding the value N, where N is between 1 and 16. If the plaintext already fills a block exactly, you add a whole extra block of padding. That looks wasteful but it's deliberate — otherwise the receiver couldn't tell padding apart from real data.

The size consequence catches people out: CBC output is always longer than the input, by 1 to 16 bytes. GCM adds exactly the tag length, 16 bytes by default. CTR output matches the input exactly. So an encrypted file being slightly larger than the original is normal, not a bug.

CTR and GCM need no padding at all, which is one of several reasons to prefer them.

PKCS#7 Padding
CBC and ECB can only process whole 16-byte blocks, so the last one is padded with PKCS#7: append N bytes, each with the value N. When the data already fills a block exactly, a whole extra block of padding is added — otherwise the receiver could not tell padding from real data.
Try:

Padding Oracle Attacks

Padding sounds like a formatting detail. Historically it has been one of the most productive attack surfaces in the field. A padding oracle exists whenever a system behaves differently depending on whether the padding of a decrypted ciphertext was valid. That difference could be a distinct error message, a different HTTP status code, or just a few milliseconds of timing.

Given such an oracle, an attacker can decrypt ciphertext one byte at a time without ever learning the key. Serge Vaudenay formalised this in 2002, and the attack has a long and painful record against real systems: POODLE (2014) against SSL 3.0, and Lucky Thirteen (2013) against TLS implementations that leaked timing through MAC-then-padding handling.

The fix is to use an AEAD mode like AES-GCM, which authenticates the ciphertext before any padding or parsing logic runs. When authentication fails, the data is rejected wholesale and nothing about the padding ever reaches the attacker. This is a large part of why TLS 1.3 dropped every non-AEAD cipher.

From Passphrase to Key: Key Derivation

AES wants a key of exactly 16, 24 or 32 bytes of near-uniform random data. Humans remember things like "correct horse battery staple", which is neither the right length nor remotely uniform. A key derivation function (KDF) bridges that gap.

Why You Cannot Just Use the Password

Padding a password with zeros, or running it through SHA-256 once to reach 32 bytes, is a serious mistake. Human-chosen passwords carry maybe 20 to 40 bits of entropy, and a single SHA-256 is extremely fast. An attacker who steals your ciphertext can test billions of candidates per second on commodity GPUs. It doesn't matter how strong the resulting 256-bit key is if the secret behind it is guessable.

A proper KDF contributes two things: a salt, and deliberate slowness.

Salt: Defeating Precomputation

A salt is a random value mixed into the derivation. With a unique random salt per encryption, the same passphrase yields a different key every time, so an attacker can't build one rainbow table and reuse it across all your data. They have to attack each encryption from scratch.

Two rules follow directly:

  • The salt must be random and unique per encryption. A hardcoded salt, even a long and obscure-looking one, defeats the point entirely — it lets an attacker precompute a single table that works against every user of the system at once.
  • The salt is not secret. Store it next to the ciphertext. Its job is uniqueness, not secrecy.

Use at least 16 bytes (128 bits). Our tool generates exactly that, fresh on every encryption.

PBKDF2, scrypt and Argon2

PBKDF2 (NIST SP 800-132) is the most widely supported KDF. It applies a pseudorandom function, usually HMAC-SHA256, over and over, with the iteration count setting the cost. Double the iterations and you double the attacker's work per guess. PBKDF2 uses very little memory, which makes it relatively cheap to accelerate on GPUs, but it's available everywhere including every browser via Web Crypto, so it's the standard choice for browser-based encryption. OWASP currently recommends 600,000 iterations for PBKDF2-HMAC-SHA256.

scrypt and Argon2 are memory-hard: they deliberately consume significant RAM, which makes GPU and ASIC attacks far more expensive relative to CPU cost. Argon2id won the 2015 Password Hashing Competition and is the preferred choice for new server-side work, with OWASP suggesting roughly 19 MiB of memory and 2 iterations. Argon2 isn't available in the browser's Web Crypto API yet, which is why browser tools generally fall back to PBKDF2.

Choosing an Iteration Count

Pick the highest count your users' patience and hardware allow. The trade-off is direct: more iterations means slower derivation for everyone, including the legitimate user. That's the entire point. A few hundred milliseconds is fine for an interactive tool; a login server handling thousands of requests per second needs to budget carefully. Raise the number over time as hardware gets faster.

Our tool offers 250,000 (fast, still well above the old 100,000 default), 600,000 (the OWASP recommendation), and 1,000,000 (slow but about as much protection as is practical in a browser).

PBKDF2 Cost — Measured Live
Iterations are the dial that decides how expensive a single password guess is. Every doubling doubles the attacker's work too. These numbers are measured on your device right now with the Web Crypto API — press the button to run it.
Times are single runs on your CPU, so they vary with hardware and background load. An attacker does not use a browser: dedicated GPUs and ASICs compute PBKDF2 orders of magnitude faster per unit cost, which is why the iteration count has to keep rising and why memory-hard functions like Argon2 and scrypt hold an advantage on that terrain.

Authenticated Encryption: Confidentiality Isn't Enough

Confidentiality means an attacker can't read your data. Authenticity means they can't modify it without being detected. Most of the time you need both.

Take a bank transfer encrypted with AES-CBC. An attacker who can't read the message can still flip bits in the ciphertext, and because CBC has no integrity check, decryption succeeds and hands back modified plaintext. Maybe a different account number. Confidentiality without authenticity is worse than nothing in a lot of real systems, because it creates confidence that isn't earned.

AEAD modes like AES-GCM solve this with a single primitive. Decryption verifies the authentication tag first and either returns the correct plaintext or throws. There's no third outcome where you quietly receive tampered data. If you're stuck with a non-AEAD mode, the manual alternative is to compute an HMAC over the ciphertext and verify it before decrypting (encrypt-then-MAC), which you can try with the HMAC Generator. Never MAC-then-encrypt, and never MAC the plaintext.

Authenticated Encryption — Tamper Test
The same 32-byte message is encrypted twice — once with AES-256-GCM and once with AES-256-CBC — using real Web Crypto. Now flip a single bit of each ciphertext and decrypt. This is the practical difference between an AEAD mode and a confidentiality-only mode.
AES-256-GCM authenticated
AES-256-CBC confidentiality only

What AES Does Not Protect You From

AES does one job well: it scrambles data under a key. It does not do any of the following, and assuming otherwise is how systems actually get breached:

  • Key management. Where does the key live, who can read it, how is it rotated, how is it revoked? These questions are harder than the cryptography, and they're where most real failures happen.
  • Endpoint security. If the machine doing the encryption is compromised — malware, a malicious browser extension, XSS on the page — the attacker sees the plaintext before it's encrypted and the key right next to it.
  • Metadata. AES hides content, not context. Message timing, sizes, sender, recipient and frequency all stay visible.
  • Data existence. Encryption doesn't hide the fact that the data is there. Rubber-hose cryptanalysis, legal or physical, couldn't care less about your key length.
  • Weak passphrases. No KDF rescues "password123". A KDF raises the cost of each guess; it can't manufacture entropy that was never there.
  • Integrity of your code. A compromised application can just exfiltrate the key. Cryptography protects data in transit and at rest, not the program holding it.

So browser-based encryption carries an honest caveat however well it's built: the mathematics is sound and nothing leaves your device, but your key does sit in memory where a malicious extension or script could reach it. For genuinely sensitive data, use a dedicated, audited application.

Known Attacks and AES's Security Margin

No serious cryptanalytic attack threatens AES as it's actually deployed. That said, "no practical attack" describes today rather than proving anything about tomorrow.

  • Brute force is infeasible, full stop. The 2128 operations AES-128 needs is beyond any plausible classical computing effort.
  • Biclique attacks (Bogdanov, Khovratovich and Rechberger, 2011) recover an AES-128 key in about 2126.1 operations. That's marginally faster than brute force and still nowhere near practical. It's a theoretical improvement, not a break.
  • Related-key attacks against AES-192 and AES-256 (Biryukov and Khovratovich, 2009) need the attacker to obtain encryptions under several keys with a specific mathematical relationship they chose. Properly generated random keys have no such relationship, so these don't apply to real systems. They're still a reminder that key derivation has to be done right.
  • Side-channel attacks are the most productive line of attack against AES today. They skip the mathematics and go after the implementation, measuring power consumption, electromagnetic emissions, cache timing or branch behaviour to recover key material. The defences belong in the implementation (constant-time code, hardware isolation), not in the algorithm.

Quantum Computers and Grover's Algorithm

Grover's algorithm gives a quantum computer a quadratic speedup on unstructured search. Applied to key recovery, it halves the effective security level: AES-128 would offer roughly 264 quantum operations, AES-256 roughly 2128. Quadratic, not exponential, which is very different from Shor's algorithm breaking RSA and elliptic-curve cryptography outright.

What that means for you: AES-256 is comfortably quantum-resistant, and even AES-128 is probably fine, because 264 quantum operations is still an enormous and likely unreachable requirement. Grover's algorithm also resists parallelisation in ways that make large-scale attacks harder than the headline number suggests. NIST's own post-quantum guidance keeps AES-256 in the approved category. If you want the extra margin regardless, AES-256 costs you almost nothing.

Performance and Hardware Acceleration

AES's clean structure turned out to be unusually easy to implement in silicon, and modern CPUs ship dedicated instructions for it. Intel and AMD provide AES-NI (introduced around 2010) with the AESENC, AESDEC and AESKEYGENASSIST instructions, plus PCLMULQDQ for the carry-less multiplication GHASH depends on. ARM has equivalents in its Cryptography Extensions.

The effect is dramatic. Hardware-accelerated AES-GCM runs at multiple gigabytes per second on a desktop CPU, and encryption is rarely the bottleneck in a modern system. On platforms without hardware support, such as some embedded devices and older mobile chips, AES gets much slower and ChaCha20-Poly1305 is the better choice. That's exactly why TLS 1.3 offers both and lets the two peers negotiate.

Rule of thumb: prefer AES where hardware acceleration exists, which covers nearly all servers and desktop clients, and prefer ChaCha20-Poly1305 on low-end mobile and embedded targets. In the browser, Web Crypto is hardware-accelerated on virtually every platform you'll run into.

AES vs ChaCha20-Poly1305 vs RSA

AES-GCMChaCha20-Poly1305RSA
TypeSymmetric block cipher (AEAD)Symmetric stream cipher (AEAD)Asymmetric
Key128/192/256-bit shared secret256-bit shared secretKey pair, 2048–4096 bits
SpeedVery fast with AES-NIFast without hardware supportSlow; tiny payloads only
Best forBulk data, disk/TLS encryptionMobile, embedded, no-AES-NIKey exchange, signatures
Payload limitEffectively unlimitedEffectively unlimitedA few hundred bytes

The important relationship is that these are not competitors, they're collaborators. RSA is far too slow to encrypt a file, and AES has no way to establish a shared secret with someone you've never met. Real systems use both, in hybrid encryption: use RSA (or ECDH) to agree on or wrap a random AES key, then use AES for the actual data. TLS does exactly this. You can try the asymmetric half with the RSA Key Generator.

This pattern also gives you a useful operational trick called crypto shredding. Encrypt each user's data under a key of their own, then destroy that key when they ask to be deleted. The ciphertext becomes permanently unreadable without touching a single byte of storage, which is a practical way to satisfy data-deletion requirements.

Using AES: Code Examples

JavaScript — Web Crypto (Browser and Node.js)

This is the correct pattern, and it's essentially what our tool does internally. Note the random salt, the random IV, the high iteration count, and the fact that salt and IV travel with the ciphertext instead of being kept secret.

const enc = new TextEncoder();
const dec = new TextDecoder();

// 1. Derive a 256-bit key from a passphrase: random salt + slow KDF
const salt = crypto.getRandomValues(new Uint8Array(16));
const baseKey = await crypto.subtle.importKey(
  'raw', enc.encode(passphrase), 'PBKDF2', false, ['deriveKey']
);
const key = await crypto.subtle.deriveKey(
  { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
  baseKey,
  { name: 'AES-GCM', length: 256 },
  false,
  ['encrypt', 'decrypt']
);

// 2. Encrypt with a FRESH random IV, every single time
const iv = crypto.getRandomValues(new Uint8Array(12));
const ct = new Uint8Array(
  await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plaintext))
);

// 3. Store salt + iv + ciphertext together. None of them are secret.
const blob = new Uint8Array(salt.length + iv.length + ct.length);
blob.set(salt, 0);
blob.set(iv, salt.length);
blob.set(ct, salt.length + iv.length);

Decryption is the mirror image. The critical property is that decrypt() throws on either a wrong key or a modified ciphertext — there is no silent corruption.

const salt = blob.slice(0, 16);
const iv   = blob.slice(16, 28);
const ct   = blob.slice(28);

// re-derive the key from the passphrase and the stored salt, then:
const plaintext = dec.decode(
  await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct)
);
// Throws if the key is wrong OR the ciphertext was tampered with.

Python — the cryptography Library

The library's AESGCM class is an AEAD interface, which makes it hard to misuse by design: it won't let you encrypt without a nonce, and it refuses to return plaintext when the tag doesn't verify.

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

key = AESGCM.generate_key(bit_length=256)   # 32 random bytes
nonce = os.urandom(12)                      # fresh for every message

ct = AESGCM(key).encrypt(nonce, b"secret message", None)
pt = AESGCM(key).decrypt(nonce, ct, None)   # raises InvalidTag on tampering

Command Line — OpenSSL

OpenSSL's enc command defaults to a weak legacy KDF, so you have to pass -pbkdf2 explicitly. It's a classic case of a dangerous default catching people out.

# Encrypt — always pass -pbkdf2 (and a high -iter)
openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -in secret.txt -out secret.enc

# Decrypt
openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in secret.enc -out secret.txt

# AES-256-GCM is not available via 'enc'; use a library for AEAD instead.

One thing to watch: OpenSSL's output format, a Salted__ header plus its own KDF parameters, is not interchangeable with what our browser tool produces. Each format embeds its own parameter layout. If you need cross-tool compatibility, use a library where you control every parameter explicitly.

Common Mistakes Checklist

Almost every real-world AES failure is one of these. Not one of them involves breaking the cipher.

  • Using ECB mode. It leaks plaintext structure. Use GCM.
  • Reusing an IV or nonce under the same key. In GCM and CTR this can enable forgery, not just decryption.
  • Using a static or hardcoded salt. Lets an attacker precompute one rainbow table that works against every user at once.
  • Deriving keys with a single hash (sha256(password)) instead of a real KDF with salt and iterations.
  • Skipping authentication, by using CBC or CTR alone with no MAC and no AEAD.
  • MAC-then-encrypt, or MAC-ing the plaintext. Always encrypt-then-MAC, or just use an AEAD.
  • Using Math.random() for keys, IVs or salts. It isn't cryptographically secure. Use crypto.getRandomValues() or os.urandom().
  • Treating encoding as encryption. Base64 is reversible by anyone and provides no confidentiality at all.
  • Rolling your own crypto. Use the standard library.
  • Hardcoding keys in source code or committing them to a repository.
  • Logging plaintext or keys, then wondering how the secret leaked.
  • Never rotating keys, and never planning for what happens after a suspected compromise.

Try It Yourself: AES in Your Browser

Reading about a cipher is one thing; watching it work is another. The AES Encrypt/Decrypt tool implements everything described here using the browser's native Web Crypto API, the same hardware-accelerated primitives that secure your TLS connections.

What it actually does under the hood:

  • A fresh 16-byte random salt on every encryption, so the same passphrase never produces the same key twice.
  • PBKDF2-HMAC-SHA256 at 250,000, 600,000 or 1,000,000 iterations, picked according to how much delay you can tolerate.
  • AES-128, AES-192 or AES-256, in GCM (authenticated, recommended) or CBC (for compatibility with older systems), with the IV sized correctly for each mode.
  • Base64 or Hex output, with the salt, IV and algorithm parameters packed into the blob. Decryption needs only the passphrase, so you don't have to remember which settings you picked.
  • A built-in key strength meter and a random key generator, because the key matters more than the key size.

Three things to try, each of which demonstrates a real property of the cipher:

  1. Encrypt the same text twice with the same key. You'll get two completely different ciphertexts. That's the random salt and IV doing their job, and it's exactly the property ECB lacks.
  2. Decrypt with the wrong key in GCM mode. You get a clean error. Switch to CBC and the same mistake may produce garbled nonsense instead, which shows concretely why authentication matters.
  3. Watch the output length. GCM adds exactly 16 bytes for the authentication tag; CBC rounds up to the next 16-byte boundary because of PKCS#7 padding.

Everything runs locally in your browser. No key, plaintext or ciphertext is transmitted anywhere, which you can verify by disconnecting from the network after the page loads.

Quick Reference

TaskRecommended approach
Encrypt arbitrary dataAES-256-GCM with a random 12-byte nonce
Derive a key from a passphrasePBKDF2-HMAC-SHA256, 600,000+ iterations, random 16-byte salt (or Argon2id server-side)
Generate a key or IVcrypto.getRandomValues() — never Math.random()
Encrypt on low-end mobile or embeddedChaCha20-Poly1305 when no AES hardware exists
Share a key with a strangerRSA or ECDH key exchange, then AES for the data
Verify a key without encrypting dataHMAC Generator
Store a passwordHash it with bcrypt or Argon2. Don't encrypt it.

One last distinction to carry with you: encryption is reversible, hashing is not. Encrypt a user's data when you need to read it back. Hash a user's password when you only ever need to verify it. Getting these two backwards is one of the most consequential mistakes in application security.

Related Tools

  • AES Encrypt/Decrypt — encrypt and decrypt text with AES-128/192/256 in GCM or CBC mode, with PBKDF2 key derivation.
  • HMAC Generator — add authentication when you cannot use an AEAD mode.
  • Hash Generator — SHA-256 and friends for integrity checking, not encryption.
  • RSA Key Generator — the asymmetric half of a hybrid encryption scheme.
  • Password Generator — create strong random keys instead of memorable passphrases.