Encryption at Rest & in Transit
Ensuring data is unreadable without the correct key, whether stored or moving over the wire.
Overview
Encryption transforms data into an unreadable form that only authorised parties can decrypt. Encryption at rest protects stored data; encryption in transit protects data over networks. AES-256-GCM is the standard for symmetric encryption; RSA-2048+ and elliptic curve (ECDH/ECDSA) for asymmetric. TLS 1.3 is mandatory for all internet-facing services. Never implement cryptographic algorithms from scratch.
Origin
DES (IBM, 1977) was the first widely-adopted standard symmetric cipher, broken in 22 hours in 1998. AES was adopted by NIST in 2001 following the Rijndael competition. TLS evolved from SSL (Netscape, 1995) through TLS 1.0-1.3; TLS 1.3 (RFC 8446, 2018) dropped legacy ciphers and eliminated the handshake round-trip via 0-RTT. NIST began post-quantum cryptography standardisation in 2016, publishing initial standards in 2024.
Examples
AES-256-GCM encryption in TypeScript with Node crypto
import { randomBytes, createCipheriv, createDecipheriv } from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY_LEN = 32; // 256 bits
const IV_LEN = 12; // 96 bits recommended for GCM
const AUTH_TAG_LEN = 16;
export function encrypt(plaintext: string, keyHex: string): string {
const key = Buffer.from(keyHex, 'hex');
if (key.length !== KEY_LEN) throw new Error('Key must be 32 bytes (256 bits)');
const iv = randomBytes(IV_LEN);
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
// Prepend iv and authTag so the receiver can decrypt without side-channel
return Buffer.concat([iv, authTag, encrypted]).toString('base64');
}
export function decrypt(ciphertext: string, keyHex: string): string {
const key = Buffer.from(keyHex, 'hex');
const buf = Buffer.from(ciphertext, 'base64');
const iv = buf.subarray(0, IV_LEN);
const authTag = buf.subarray(IV_LEN, IV_LEN + AUTH_TAG_LEN);
const encrypted = buf.subarray(IV_LEN + AUTH_TAG_LEN);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
return decipher.update(encrypted) + decipher.final('utf8');
}GCM (Galois/Counter Mode) provides authenticated encryption: it detects tampering via the auth tag, not just confidentiality. IV must be unique per encryption but need not be secret; prepending it to the ciphertext is standard. Key must come from a KMS or secrets manager, not hardcoded.
Field-level encryption for sensitive database columns in Ruby
# Rails 7+ Active Record Encryption
# config/application.rb
# config.active_record.encryption.primary_key = Rails.application.credentials.active_record_encryption[:primary_key]
# config.active_record.encryption.deterministic_key = Rails.application.credentials.active_record_encryption[:deterministic_key]
# config.active_record.encryption.key_derivation_salt = Rails.application.credentials.active_record_encryption[:key_derivation_salt]
class Patient < ApplicationRecord
# Non-deterministic: encrypted differently each write; cannot query directly
encrypts :medical_notes
encrypts :ssn
# Deterministic: same plaintext = same ciphertext; allows WHERE queries
encrypts :email, deterministic: true
# Validates before encryption, queries after encryption
validates :email, uniqueness: true
end
# Querying works transparently with deterministic encryption
Patient.find_by(email: 'alice@example.com')
# Non-deterministic encrypted columns cannot be queried
# Patient.where(ssn: '123-45-6789') would raise an errorRails 7 Active Record Encryption (merged from rails/rails#41659) encrypts column values using AES-256-GCM before storing. Deterministic encryption (like SIV mode) enables indexed lookups at the cost of leaking if the same plaintext appears in multiple rows.
Use Cases
- 01Encrypting PII (social security numbers, medical records, payment card data) at rest in the database
- 02TLS termination at the load balancer (AWS ACM, Nginx) for all HTTP traffic; HTTP should redirect to HTTPS with HSTS
- 03Envelope encryption in cloud KMS: a data encryption key (DEK) encrypts the data; the key encryption key (KEK) in KMS encrypts the DEK
- 04End-to-end encryption for messaging applications where even the server cannot read message content (Signal protocol)
When Not to Use
- //Do not encrypt data that must be searchable with standard WHERE queries unless using deterministic encryption or a searchable encryption scheme; the query planner cannot use indexes on non-deterministic ciphertext
- //Do not use ECB mode; it is deterministic per block and leaks patterns in the plaintext (the "ECB penguin" demo illustrates this clearly)
- //Do not invent key derivation schemes; use PBKDF2, bcrypt, or argon2id for password-derived keys, or HKDF for key material derivation from a master key
Technical Notes
- TLS 1.3 forward secrecy via ephemeral Diffie-Hellman means that a compromise of the server's long-term private key does not decrypt past session traffic; TLS 1.2 sessions without ephemeral DH are retroactively decryptable
- AES-GCM nonce reuse is catastrophic: reusing the same (key, IV) pair with different plaintexts allows an attacker to recover the authentication key and forge messages. Use a random 96-bit IV per encryption or a counter with a per-session key
- Transparent Data Encryption (TDE) in PostgreSQL (pg_tde extension, production-ready in 2024) encrypts data files at the storage layer; it protects against disk theft but not against a compromised database user with read access
- Post-quantum cryptography: NIST FIPS 203 (ML-KEM, formerly Kyber) and FIPS 204 (ML-DSA, formerly Dilithium) were standardised in 2024 for key exchange and digital signatures respectively; TLS 1.3 with X25519Kyber768 hybrid is available in Chrome 116+ and BoringSSL
More in Safety