Symmetric encryption

Use authenticated encryption

Choose a high-level AEAD construction so ciphertext confidentiality and integrity are verified together before plaintext is accepted.

Encryption without authentication can allow controlled ciphertext changes. Attackers do not need to read your data to hurt you; sometimes flipping bits in ciphertext is enough to change what it decrypts to.

The failure looks like this: an application encrypts an account role but does not authenticate the ciphertext. A controlled bit change can alter decrypted data or produce distinguishable errors before the application notices. The data stayed confidential the whole time. It just stopped being trustworthy.

Authenticated encryption (AEAD) solves both problems in one construction. It encrypts and produces an authentication tag over the ciphertext, and decryption verifies the tag before releasing any plaintext. Use it by default. AES-GCM and ChaCha20-Poly1305 are the two standard choices, and a high-level library exposes them through a safe API. Do not select raw cipher modes yourself.

AES-GCM in Node

import { createCipheriv, randomBytes } from 'node:crypto'

const key = randomBytes(32)          // 256-bit key
const nonce = randomBytes(12)        // 96-bit nonce, unique per encryption

const cipher = createCipheriv('aes-256-gcm', key, nonce)
const ciphertext = Buffer.concat([cipher.update('role=admin'), cipher.final()])
const tag = cipher.getAuthTag()      // 16 bytes

Store the nonce, ciphertext, and tag together. None of them is secret; only the key is.

Decryption verifies before it trusts:

import { createDecipheriv } from 'node:crypto'

const decipher = createDecipheriv('aes-256-gcm', key, nonce)
decipher.setAuthTag(tag)
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()])
// throws "Unsupported state or unable to authenticate data"
// if ciphertext, tag, or nonce was modified

Flip one byte of the ciphertext or the tag and decipher.final() throws. That throw is the security feature.

Fail closed, and fail uniformly

Decryption must fail closed when authentication fails and must not release partial plaintext. In stream-style APIs, do not act on any decrypted output until the final tag check passes.

The application must also treat every authentication failure the same. One generic error, one log path, no fallback decryption with a weaker mode or an older code path. Distinguishable failures are how attackers turn a small crack into an oracle.

Practice

Encrypt a short account record with a maintained AEAD API and save the complete versioned output fields. Decrypt it successfully, then flip one byte in the ciphertext and tag. Prove both failures return no plaintext and follow the same public error path.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →