Symmetric encryption
One key to lock, the same key to unlock - and why that simple idea runs most of the encrypted traffic on the internet
What symmetric encryption is
Symmetric encryption is any cipher where the same key both encrypts and decrypts - the toy XOR cipher from the last article is technically one, just not a safe one. "Symmetric" just names that the same key does both jobs - the opposite is asymmetric encryption, where a public key encrypts and a different private key decrypts.
It matters because it's fast - orders of magnitude faster than asymmetric encryption - which is why it does almost all the actual encrypting in a system like TLS. When you load an HTTPS page, the bulk of the data is encrypted symmetrically. Asymmetric encryption only shows up briefly at the start, to agree on a symmetric key.
What AES does differently to the toy cipher
The toy cipher broke because it reused a short, predictable key over and over, byte after byte. AES (Advanced Encryption Standard) is built to avoid exactly that:
- Instead of XOR-ing against a short repeated key, AES takes your key and, for each 128-bit block of data, runs it through many rounds of substitution and mixing to produce something that looks statistically indistinguishable from random - then XORs that against the data.
- That output changes completely if you flip even one bit of input, so there's no repeating pattern for an attacker to line up two ciphertexts against.
It was selected by NIST in 2001 after a public competition, has been intensively studied for over two decades, and has no known practical attack against a correctly implemented version. It comes in three key sizes - 128, 192, and 256 bits. AES-128 is already secure against any foreseeable brute-force attack; AES-256 is the safe default for new systems and keeps a security margin against quantum computers. The key size governs how hard the key is to guess, not how much data you can encrypt with it - a 256-bit (32 byte) key can encrypt a 10GB file just as easily as a one-line message.
Modes of operation - why AES-GCM is what you want
AES only knows how to encrypt one 128-bit block. Real messages are rarely exactly that size, so a mode of operation decides how to stitch many blocks together. This choice matters more than it sounds like it should - the wrong mode can make AES insecure even with a perfect, never-reused key.
ECB - never use this. Each block is encrypted independently, so identical plaintext blocks always produce identical ciphertext blocks. The structure of the data leaks straight through. The classic demo: encrypt a bitmap image block by block, and the outline of the original image is still visible in the ciphertext, because same-coloured regions encrypt to the same repeating pattern.
CBC - better, but fragile. Each block is XORed with the previous ciphertext block before encrypting, so identical plaintext no longer produces identical ciphertext. It needs an IV (a random starting value for the first block) and it only protects confidentiality, not integrity - an attacker who can manipulate ciphertext can cause predictable changes to the decrypted plaintext without ever knowing the key. This is what the POODLE and BEAST attacks exploited. Mostly superseded now.
GCM - use this. AES-GCM is authenticated encryption - confidentiality and integrity in one operation. Alongside the ciphertext it produces an authentication tag (similar in spirit to an HMAC). Decryption checks that tag first; if the ciphertext was tampered with even slightly, decryption fails outright and no plaintext comes out. There's no separate integrity step to forget - it's built in.
Term - AEAD
AES-GCM is an example of AEAD (Authenticated Encryption with Associated Data). "Authenticated" is the integrity guarantee above. "Associated data" means you can attach extra metadata (a header, a record ID) that stays unencrypted and readable, but is still covered by the authentication tag - so it can't be tampered with even though it's in plain sight.
Nonces - why you must never reuse them
AES-GCM needs a nonce: a 96-bit value that must be unique for every encryption done under the same key. It doesn't need to be secret - it's typically just prepended to the ciphertext so the decryptor has it.
This is the same reuse problem as the two-ciphertext XOR attack from the last article, just against AES's internal keystream instead of a repeated key. Encrypt two different messages under the same key and nonce, and an attacker who sees both ciphertexts can combine them to cancel out the keystream entirely - no key needed. In GCM specifically, nonce reuse also breaks the authentication tag, so an attacker can go further and forge ciphertexts that pass integrity checks. This has happened to real systems, not just in theory.
The standard fix: generate a fresh, cryptographically random 96-bit nonce for every single encryption. At that size, the odds of an accidental collision stay negligible up to roughly 232 encryptions under one key - past that, rotate the key.
The key distribution problem
Symmetric encryption has one fundamental limitation: both parties need the same key, which means getting the key to the other party somehow. If you already had a secure way to share that key, you could have just sent the message that way instead. Send the key over an insecure channel, and anyone who intercepts it can decrypt everything.
This was an unsolved problem for centuries. The 1970s answer - asymmetric encryption and key exchange protocols like Diffie-Hellman - lets two parties who've never spoken before agree on a shared secret key over a completely public channel, without ever transmitting the key itself. TLS uses exactly this: asymmetric cryptography to negotiate a symmetric key, then symmetric encryption for everything after.
For at-rest encryption - data stored on a server, in a database, or on disk - key distribution is simpler because you control both sides. The key lives in a secrets manager or hardware security module, and the application fetches it at startup. The real challenge shifts to key rotation, and making sure the key and the data it protects are never stored in the same place.
In practice
Most applications never call AES directly - TLS handles it for you, or you go through a library that wraps AES-GCM correctly. When you do need to encrypt data at rest - sensitive database fields, files, encrypted backups - the pattern looks like this:
# For each encryption: nonce = random_bytes(12) # 96-bit random nonce ciphertext, tag = AES_GCM_encrypt(key, nonce, plaintext) store(nonce + ciphertext + tag) # nonce is not secret # For decryption: nonce, ciphertext, tag = parse(stored) plaintext = AES_GCM_decrypt(key, nonce, ciphertext, tag) # fails loudly if tag doesn't match - data was tampered with
In Python, using the cryptography library:
import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM key = AESGCM.generate_key(bit_length=256) # store this securely aesgcm = AESGCM(key) # Encrypt nonce = os.urandom(12) # 96-bit random nonce ciphertext = aesgcm.encrypt(nonce, b"hello world", aad=None) stored = nonce + ciphertext # nonce is not secret # Decrypt nonce, ciphertext = stored[:12], stored[12:] plaintext = aesgcm.decrypt(nonce, ciphertext, aad=None) # raises InvalidTag if ciphertext was tampered with
Use your language's standard library or a well-audited package rather than assembling primitives yourself - in Node.js, the built-in node:crypto module provides the same primitives. Never implement AES yourself.
For envelope encryption - encrypting data with a data key, then encrypting the data key itself with a master key - AWS KMS, Google Cloud KMS, and HashiCorp Vault all handle the key management layer for you, which is the hard part.