AES-GCM vs AES-CBC: Encrypting Text with a Password Properly

Block cipher modes, IVs and nonces, why authenticated encryption matters, deriving keys from passwords with PBKDF2, and how openssl enc lays out its output.

· 12 min read

Illustration for the AES Encryption and Decryption Online guide

"Encrypt it with AES-256" sounds like a complete instruction, but it leaves out most of the decisions that make encryption safe or broken: which mode, where the IV comes from, how a password becomes a key, and how the pieces are stored so the other side can decrypt. This guide covers each decision in the order you meet them, with the layout used by the AES Encrypt & Decrypt tool and by openssl enc as concrete examples.

AES is a block cipher, not a complete scheme

AES (FIPS 197, 2001) is the Rijndael cipher selected by NIST after a public competition. On its own it does one thing: it takes a 16-byte block and a key of 128, 192 or 256 bits and produces another 16-byte block. With the same key and the same input block you always get the same output. Everything else (messages longer than 16 bytes, randomisation, integrity) comes from the mode of operation wrapped around it.

The key size sets the number of rounds (10, 12 or 14) and the security margin. No practical attack on full AES-128 is known; the best published attacks are only slightly better than brute force. AES-256 is chosen for a larger margin, including against Grover's algorithm on a hypothetical large quantum computer, which would halve the effective key length. Both are fine; when you derive the key from a password, as the tool does, using 256 bits costs nothing extra.

Why ECB is never the answer

The simplest mode, ECB, encrypts each block separately. Identical plaintext blocks give identical ciphertext blocks, so patterns survive encryption; the classic demonstration is a bitmap of the Linux penguin that is still recognisable after ECB encryption. No modern design should use ECB, and Web Crypto does not even offer it.

CBC: chaining and padding

Cipher Block Chaining (NIST SP 800-38A) XORs each plaintext block with the previous ciphertext block before encrypting it. The first block is XORed with an initialisation vector (IV), 16 random bytes that make each encryption different even for identical messages.

CBC has two properties to be aware of:

  • Padding. Messages must be a multiple of 16 bytes, so the last block is padded. The standard scheme, PKCS#7, adds N bytes each with value N; a message that already fills its last block gets a whole extra block of sixteen 0x10 bytes. That is why encrypting exactly 16 bytes with CBC gives 32 bytes of ciphertext.
  • No integrity. CBC hides the content but does not detect changes. Flipping a bit in one ciphertext block flips the same bit in the next plaintext block. Worse, if a server reveals whether decrypted padding was valid (through an error message or even response timing), an attacker can decrypt the whole message one byte at a time. This padding-oracle attack (Vaudenay, 2002) broke ASP.NET, Java Server Faces and parts of TLS.

CBC is safe only when combined with a MAC computed over the ciphertext (encrypt-then-MAC, for example AES-CBC plus HMAC-SHA256) and verified before any decryption. Most people who use CBC do not do this, which is the main reason to prefer GCM.

Diagram contrasting AES-CBC and AES-GCM: CBC chains each block through the previous ciphertext starting from a 16-byte IV and needs padding with no tag; GCM encrypts a counter stream from a 12-byte nonce, XORs it with the plaintext without padding, and appends a 16-byte authentication tag
Diagram contrasting AES-CBC and AES-GCM: CBC chains each block through the previous ciphertext starting from a 16-byte IV and needs padding with no tag; GCM encrypts a counter stream from a 12-byte nonce, XORs it with the plaintext without padding, and appends a 16-byte authentication tag

GCM: encryption and authentication in one

Galois/Counter Mode (NIST SP 800-38D) does two jobs:

  1. Counter mode encryption. AES encrypts a sequence of counter blocks derived from a 12-byte IV (often called a nonce), producing a keystream that is XORed with the plaintext. No padding is needed, so the ciphertext is exactly as long as the plaintext.
  2. Authentication. A polynomial hash called GHASH runs over the ciphertext (and any additional authenticated data), and the result is encrypted to form a tag, usually 16 bytes.

On decryption the tag is recomputed and compared. If the key is wrong or a single bit of ciphertext, IV or tag has changed, decryption fails and returns nothing. That is what "authenticated encryption" means, and it removes padding oracles and silent tampering in one step. TLS 1.3 uses AES-GCM (or ChaCha20-Poly1305) for all its traffic.

GCM's price is a strict rule: never use the same IV twice with the same key. Reusing an IV makes the keystream repeat, so the XOR of two ciphertexts equals the XOR of their plaintexts, and it also leaks the authentication key, allowing forgeries. Random 12-byte IVs are safe up to about 2^32 messages per key, the limit NIST SP 800-38D sets for randomly generated IVs. Beyond that, use a counter as the IV or rotate the key.

AES-CBCAES-GCM
IV size16 bytes, unpredictable12 bytes, unique per key
PaddingPKCS#7, up to 16 bytesnone
Integritynone without a separate MAC16-byte tag built in
Wrong keypadding error or garbagealways fails cleanly
Dangerpadding oracles, tamperingIV reuse
Use todaylegacy compatibilitydefault choice

Turning a password into a key

AES needs a key of exactly 16, 24 or 32 bytes with full randomness. A password is neither the right length nor random. A key derivation function fixes both, and the important property is that it is deliberately slow, so each guess costs an attacker real work.

PBKDF2 (RFC 8018) runs HMAC many times: the derived key depends on the password, a random salt and an iteration count. The salt ensures two people with the same password get different keys and prevents precomputed tables. The iteration count multiplies the cost of every guess. The OWASP Password Storage Cheat Sheet recommends 600,000 iterations for PBKDF2-HMAC-SHA256, and that is the default in the AES tool. On a laptop it takes a fraction of a second, which is fine for one encryption and painful for an attacker trying billions of passwords.

Memory-hard functions such as Argon2id and scrypt resist GPU cracking better than PBKDF2 and are preferred for new systems. PBKDF2 is used here because it is built into the Web Crypto API and OpenSSL, so the output can be reproduced anywhere.

No KDF rescues a weak password. Against an attacker with a GPU rig, 600,000 iterations turns billions of guesses per second into thousands, which protects a strong passphrase indefinitely and a common password not at all.

Storing the pieces together

To decrypt, the other side needs the salt, the IV, the ciphertext and the tag, plus the settings. None of the first four is secret, so the usual approach is to concatenate them. The compact format of the AES tool is:

Byte layouts of three formats: compact password mode is 16-byte salt then 12-byte IV then ciphertext then 16-byte tag; raw-key mode omits the salt; OpenSSL enc writes the 8 bytes Salted__ then an 8-byte salt then CBC ciphertext, with key and IV both derived by PBKDF2
Byte layouts of three formats: compact password mode is 16-byte salt then 12-byte IV then ciphertext then 16-byte tag; raw-key mode omits the salt; OpenSSL enc writes the 8 bytes Salted__ then an 8-byte salt then CBC ciphertext, with key and IV both derived by PBKDF2
  • Password mode: Base64 of salt (16 bytes) ‖ IV (12 for GCM, 16 for CBC) ‖ ciphertext (with the 16-byte tag at the end for GCM).
  • Raw key mode: Base64 of IV ‖ ciphertext.
  • JSON bundle: the same parts as separate Base64 fields with the algorithm, KDF and iteration count written out, so another program needs no prior agreement.

For example, encrypting the three bytes abc with a password in GCM gives 16 + 12 + 3 + 16 = 47 bytes, which is 64 Base64 characters. The overhead is fixed, so longer messages hardly grow.

Decrypting in Python with the cryptography package looks like this:

import base64, hashlib
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

raw = base64.b64decode(blob)
salt, iv, ct = raw[:16], raw[16:28], raw[28:]
key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 600_000, 32)
plaintext = AESGCM(key).decrypt(iv, ct, None).decode()

AESGCM.decrypt expects the tag appended to the ciphertext, which is exactly how Web Crypto produces it.

How openssl enc does it

The OpenSSL command line has its own long-standing format. With -pbkdf2 (OpenSSL 1.1.1 and later):

printf '%s' 'hello' | openssl enc -aes-256-cbc -pbkdf2 -a -A -pass pass:secret
  • OpenSSL picks an 8-byte random salt and writes the 8 ASCII bytes Salted__ followed by the salt, then the ciphertext. In Base64, Salted__ always becomes U2FsdGVkX1, which is how you recognise the format.
  • PBKDF2-HMAC-SHA256 with the default 10,000 iterations (unless -iter is given) derives 48 bytes: the first 32 are the AES-256 key and the last 16 are the CBC IV. The IV is not stored.
  • -a writes Base64 and -A keeps it on one line.

With the salt fixed to 0102030405060708 for reproducibility, the command above gives U2FsdGVkX18BAgMEBQYHCAasjjrdVJoSaHOk81PBW6s= in both OpenSSL 3 and LibreSSL. The AES tool's OpenSSL mode produces and reads exactly this format, and its tests check that value.

Older scripts may use openssl enc without -pbkdf2, which derives the key with a single MD5 iteration (EVP_BytesToKey). OpenSSL now prints a deprecation warning for it, because a single fast hash offers almost no resistance to password guessing. Re-encrypt such data with -pbkdf2 -iter set high.

A checklist

  1. Use AES-GCM with a 256-bit key unless a counterpart forces CBC.
  2. Generate a fresh random IV for every message; never reuse one with the same key.
  3. Derive keys from passwords with PBKDF2 (600,000 iterations for SHA-256), scrypt or Argon2id, with a random salt per message or per key.
  4. Store salt, IV, ciphertext and tag together, and record the settings.
  5. If you must use CBC, add HMAC-SHA256 over IV and ciphertext and verify it before decrypting.
  6. Treat decryption failures as a single generic error, never revealing whether padding or the tag failed.
  7. For files and backups, prefer a maintained tool such as age, or the AES-256 in 7-Zip, over hand-assembled scripts.

For the public-key side, where one party encrypts or verifies and another holds the private key, see RSA vs ECDSA vs Ed25519.

Sources

  • FIPS 197, Advanced Encryption Standard (AES).
  • NIST SP 800-38A (CBC and other modes) and SP 800-38D (GCM), section 8 on IV uniqueness.
  • RFC 8018, PKCS #5: Password-Based Cryptography Specification Version 2.1 (PBKDF2).
  • OWASP Password Storage Cheat Sheet, PBKDF2 iteration recommendations.
  • S. Vaudenay, "Security Flaws Induced by CBC Padding", EUROCRYPT 2002.
  • OpenSSL enc manual page (options -pbkdf2, -iter, -a, -A, -S).

Related searches

  • aes gcm vs cbc
  • aes encryption explained
  • encrypt with password
  • pbkdf2 iterations
  • aes iv nonce
  • openssl enc pbkdf2
  • padding oracle
  • authenticated encryption
  • aes 128 vs 256

Open the AES Encryption and Decryption Online