Encryption #
Encryption is the foundation of trust in digital systems. Without encryption, data sent over networks can be read by anyone sniffing traffic. Data stored in a leaked database can be read directly by attackers. Credentials stored without encryption become time bombs waiting for incidents.
Understanding encryption from a web developer’s perspective isn’t about understanding the math behind it — that’s the cryptographer’s domain. What needs to be understood: when encryption is needed, which algorithms are safe to use today, how to use available libraries correctly, and how to manage encryption keys securely. Incorrect encryption usage — outdated algorithms, weak keys, flawed implementations — can be more dangerous than no encryption at all because it provides a false sense of security.
The Two Encryption Categories: Symmetric and Asymmetric #
graph LR
subgraph Symmetric["Symmetric Encryption"]
A1[Plaintext] -->|Encrypt with key| B1[Ciphertext]
B1 -->|Decrypt with the SAME key| C1[Plaintext]
D1[One key for encrypting and decrypting]
end
subgraph Asymmetric["Asymmetric Encryption"]
A2[Plaintext] -->|Encrypt with Public Key| B2[Ciphertext]
B2 -->|Decrypt with Private Key| C2[Plaintext]
D2["Public key can be shared\nPrivate key stays secret"]
end
subgraph UseCases["When to Use"]
E1["Symmetric: data encryption,\nencription at rest, high speed"]
E2["Asymmetric: TLS handshake,\ndigital signatures, key exchange"]
endSymmetric vs Asymmetric comparison:
┌─────────────────┬──────────────────────────┬──────────────────────────┐
│ Aspect │ Symmetric (AES) │ Asymmetric (RSA/ECC) │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ Keys │ One key for everything │ Public/private key pairs │
│ Speed │ Very fast │ Slow (1000x slower) │
│ Key distribution│ Must exchange securely │ Public keys freely shared│
│ Key size │ 128/256 bits │ 2048/4096 bits (RSA) │
│ Use cases │ Bulk data encryption │ Key exchange, digital sig│
│ Examples │ AES-256-GCM │ RSA, ECDSA, X25519 │
└─────────────────┴──────────────────────────┴──────────────────────────┘
In practice, both are used together:
→ Asymmetric for key exchange (TLS handshake)
→ Symmetric for actual data encryption (faster)
→ This is how TLS works: a hybrid approach
TLS/HTTPS: Encrypting Data in Transit #
Encrypting data being sent over networks is a non-negotiable basic requirement. Plain HTTP allows anyone on the same network to read and modify traffic.
# Nginx configuration for secure TLS
server {
listen 443 ssl http2;
server_name app.example.com;
# Certificate and private key
ssl_certificate /etc/ssl/certs/app.example.com.crt;
ssl_certificate_key /etc/ssl/private/app.example.com.key;
# Only TLS 1.2 and 1.3 — TLS 1.0 and 1.1 are deprecated
ssl_protocols TLSv1.2 TLSv1.3;
# Strong cipher suites — prioritize forward secrecy
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off; # TLS 1.3 handles this
# Session resumption — performance without sacrificing security
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # disable session tickets (forward secrecy)
# OCSP Stapling — faster certificate validation
ssl_stapling on;
ssl_stapling_verify on;
# HSTS — force HTTPS for the next 1 year
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Redirect HTTP to HTTPS
# (in a separate port 80 server block)
}
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
What to ensure for secure TLS:
✓ TLS 1.2 minimum, TLS 1.3 recommended
✗ TLS 1.0 and 1.1 are deprecated and must be disabled
✓ Forward Secrecy: use ECDHE or DHE ciphers
If private keys leak in the future, old traffic can't be decrypted
✗ RSA key exchange without forward secrecy — if keys leak, all historical traffic is decryptable
✓ Certificates from trusted CAs
✓ Certificates renewed before expiry (Let's Encrypt: 90 days)
✓ HSTS to prevent SSL stripping attacks
✓ OCSP stapling for faster certificate validation
Verification tools:
→ SSL Labs (ssllabs.com/ssltest): an A+ rating is the target
→ Mozilla SSL Configuration Generator for recommended configs
Encrypting Data at Rest: Protecting Stored Data #
Stored data — in databases, on disks, in backups — needs protection if storage media are compromised.
Symmetric Encryption with AES-256-GCM #
AES-256-GCM is the recommended symmetric encryption standard. GCM (Galois/Counter Mode) provides authenticated encryption — it doesn’t just encrypt data but also verifies its integrity.
// AES-256-GCM encryption and decryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
)
func encryptData(plaintext []byte, key []byte) (map[string]string, error) {
if len(key) != 32 {
return nil, errors.New("key must be 256 bits (32 bytes)")
}
// Generate a new nonce for every encryption
// CRITICAL: never reuse a nonce with the same key!
nonce := make([]byte, 12) // 96-bit nonce — the GCM standard
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Encrypt + authenticate
// GCM produces ciphertext + an authentication tag (16 bytes)
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
return map[string]string{
"nonce": base64.StdEncoding.EncodeToString(nonce),
"ciphertext": base64.StdEncoding.EncodeToString(ciphertext),
"algorithm": "AES-256-GCM",
}, nil
}
func decryptData(encrypted map[string]string, key []byte) ([]byte, error) {
nonce, err := base64.StdEncoding.DecodeString(encrypted["nonce"])
if err != nil {
return nil, err
}
ciphertext, err := base64.StdEncoding.DecodeString(encrypted["ciphertext"])
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// GCM automatically verifies the authentication tag
// If data was modified, an error is returned
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, errors.New("decryption failed: data may have been modified")
}
return plaintext, nil
}
// Usage:
// key := make([]byte, 32) // 256-bit key — store it securely!
// plaintext := []byte("Sensitive data that needs encryption")
// encrypted, _ := encryptData(plaintext, key)
// decrypted, _ := decryptData(encrypted, key)
Important principles for AES-GCM:
✓ Use AES-256 (256-bit keys) — not AES-128
✓ Use GCM mode — provides encryption + authentication
✗ Don't use ECB mode — insecure, data patterns visible in the ciphertext
✗ Don't use CBC without a MAC — vulnerable to padding oracle attacks
Nonces (IVs):
✓ Generate a new random nonce for EVERY encryption
✗ Never reuse a nonce with the same key — this fatally breaks GCM security
✓ Store nonces with ciphertexts — nonces aren't secret, but must be unique
Authenticated Encryption:
✓ GCM provides an authentication tag verifying integrity
→ Modified data fails decryption
→ Protects against tampering and bit flipping attacks
Envelope Encryption: Best Practice for Key Management #
Envelope encryption separates the data encryption key (DEK) from the key encryption key (KEK). The DEK encrypts data, the KEK encrypts the DEK. This enables key rotation without re-encrypting all data.
// Envelope encryption pattern:
// KEK (Key Encryption Key) → encrypts the DEK
// DEK (Data Encryption Key) → encrypts data
//
// The KEK is stored in a secure location (HSM, KMS)
// The DEK is stored with the encrypted data (in encrypted form)
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
)
type EnvelopeEncryption struct {
kekCipher cipher.AEAD
}
// NewEnvelopeEncryption takes a 256-bit Key Encryption Key from a KMS or HSM.
func NewEnvelopeEncryption(kek []byte) (*EnvelopeEncryption, error) {
if len(kek) != 32 {
return nil, errors.New("KEK must be 256-bit")
}
block, err := aes.NewCipher(kek)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return &EnvelopeEncryption{kekCipher: aead}, nil
}
// Encrypt data using envelope encryption.
// Every call generates a new DEK.
func (e *EnvelopeEncryption) Encrypt(plaintext []byte) (map[string]string, error) {
// 1. Generate a new Data Encryption Key (DEK)
dek := make([]byte, 32) // 256-bit DEK
if _, err := rand.Read(dek); err != nil {
return nil, err
}
// 2. Encrypt the data with the DEK
dataNonce := make([]byte, 12)
if _, err := rand.Read(dataNonce); err != nil {
return nil, err
}
dataBlock, _ := aes.NewCipher(dek)
dataCipher, _ := cipher.NewGCM(dataBlock)
encryptedData := dataCipher.Seal(nil, dataNonce, plaintext, nil)
// 3. Encrypt the DEK with the KEK (wrapping)
keyNonce := make([]byte, 12)
if _, err := rand.Read(keyNonce); err != nil {
return nil, err
}
encryptedDek := e.kekCipher.Seal(nil, keyNonce, dek, nil)
return map[string]string{
"encrypted_dek": base64.StdEncoding.EncodeToString(encryptedDek),
"key_nonce": base64.StdEncoding.EncodeToString(keyNonce),
"encrypted_data": base64.StdEncoding.EncodeToString(encryptedData),
"data_nonce": base64.StdEncoding.EncodeToString(dataNonce),
}, nil
}
// Decrypt data from an envelope.
func (e *EnvelopeEncryption) Decrypt(envelope map[string]string) ([]byte, error) {
// 1. Unwrap the DEK using the KEK
encryptedDek, _ := base64.StdEncoding.DecodeString(envelope["encrypted_dek"])
keyNonce, _ := base64.StdEncoding.DecodeString(envelope["key_nonce"])
dek, err := e.kekCipher.Open(nil, keyNonce, encryptedDek, nil)
if err != nil {
return nil, err
}
// 2. Decrypt the data using the DEK
encryptedData, _ := base64.StdEncoding.DecodeString(envelope["encrypted_data"])
dataNonce, _ := base64.StdEncoding.DecodeString(envelope["data_nonce"])
dataBlock, _ := aes.NewCipher(dek)
dataCipher, _ := cipher.NewGCM(dataBlock)
return dataCipher.Open(nil, dataNonce, encryptedData, nil)
}
// Rotate the KEK: decrypt with the old KEK, re-encrypt the DEK with the new KEK.
// Data doesn't need to be decrypted and re-encrypted!
func (e *EnvelopeEncryption) RotateKek(newKek []byte, envelope map[string]string) (map[string]string, error) {
oldEncryptedDek, _ := base64.StdEncoding.DecodeString(envelope["encrypted_dek"])
oldKeyNonce, _ := base64.StdEncoding.DecodeString(envelope["key_nonce"])
dek, err := e.kekCipher.Open(nil, oldKeyNonce, oldEncryptedDek, nil)
if err != nil {
return nil, err
}
// Re-encrypt the DEK with the new KEK
newKekBlock, _ := aes.NewCipher(newKek)
newKekCipher, _ := cipher.NewGCM(newKekBlock)
newKeyNonce := make([]byte, 12)
if _, err := rand.Read(newKeyNonce); err != nil {
return nil, err
}
newEncryptedDek := newKekCipher.Seal(nil, newKeyNonce, dek, nil)
// encrypted_data and data_nonce stay unchanged
envelope["encrypted_dek"] = base64.StdEncoding.EncodeToString(newEncryptedDek)
envelope["key_nonce"] = base64.StdEncoding.EncodeToString(newKeyNonce)
return envelope, nil
}
Encrypting Sensitive Fields in Databases #
Not all data needs encryption in databases — encryption adds complexity and reduces query capabilities. Focus on genuinely sensitive fields.
// Transparently encrypted database fields
// Go equivalent of SQLAlchemy's TypeDecorator: a type implementing
// database/sql's Scanner and Valuer interfaces
import (
"crypto/sha256"
"database/sql"
"database/sql/driver"
"encoding/hex"
"encoding/json"
"os"
)
type EncryptedString struct {
Value string
key []byte
}
// Encrypt before storing in the database.
func (e EncryptedString) Value() (driver.Value, error) {
if e.Value == "" {
return nil, nil
}
encrypted, err := encryptData([]byte(e.Value), e.key)
if err != nil {
return nil, err
}
return json.Marshal(encrypted)
}
// Decrypt when reading from the database.
func (e *EncryptedString) Scan(value interface{}) error {
if value == nil {
e.Value = ""
return nil
}
var encrypted map[string]string
if err := json.Unmarshal(value.([]byte), &encrypted); err != nil {
return err
}
plaintext, err := decryptData(encrypted, e.key)
if err != nil {
return err
}
e.Value = string(plaintext)
return nil
}
// Configuration
var fieldEncryptionKey = []byte(os.Getenv("FIELD_ENCRYPTION_KEY"))
type User struct {
ID int64
Email string
PhoneNumber EncryptedString // Sensitive fields that get encrypted
TaxID EncryptedString
DateOfBirth EncryptedString
// NOT encrypted — needed for search/filter
// For fields needing search, store a separate hash
PhoneHash string
}
type UserRepository struct{ db *sql.DB }
func (r UserRepository) CreateUser(email string, phone, taxID, dob *string) error {
user := User{Email: email}
if phone != nil {
// automatically encrypted by EncryptedString
user.PhoneNumber = EncryptedString{Value: *phone, key: fieldEncryptionKey}
// Store a hash for search purposes
sum := sha256.Sum256([]byte(*phone))
user.PhoneHash = hex.EncodeToString(sum[:])
}
// ... insert the user; encrypted fields are serialized by Value()
return nil
}
// Find a user by phone number.
func (r UserRepository) FindByPhone(phone string) (*User, error) {
sum := sha256.Sum256([]byte(phone))
hash := hex.EncodeToString(sum[:])
// ... SELECT * FROM users WHERE phone_hash = ? AND scan with EncryptedString
return nil, nil
}
Which fields need database encryption:
✓ Must encrypt:
→ Credit card numbers / payment instruments
→ ID card, passport, or other identity numbers
→ Medical or health data
→ Bank account numbers
→ PINs or secrets usable for transactions
✓ Highly recommended:
→ Phone numbers
→ Dates of birth
→ Full addresses
→ Biometric data
✓ Consider per regulation:
→ Emails (GDPR, some jurisdictions)
→ IP addresses (can count as PII)
✗ Usually unnecessary:
→ Usernames (usually public)
→ Application preferences
→ Timestamps and metadata
→ Data that's genuinely public
Hashing vs Encryption: Choosing Correctly #
Hashing and encryption are two different techniques with different purposes. Choosing the wrong one can break security.
Encryption: reversible with keys
Data → [Encrypt with key] → Ciphertext → [Decrypt with key] → Data
When to use: when the original data needs to be recovered
Examples: credit card numbers, PII needing processing
Hashing: one-way, can't be reversed
Data → [Hash] → Hash value (can't return to Data)
When to use: verification without storing original data
Examples: passwords, verification tokens
Wrong usage:
✗ Encrypting passwords (decryptable if keys leak)
✗ Hashing credit card numbers for storage (can't be processed)
✓ Hashing passwords with bcrypt/Argon2
✓ Encrypting credit card numbers with AES-256-GCM
// Password hashing — NOT encryption
// Uses golang.org/x/crypto/argon2 (a battle-tested password KDF)
import (
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/crypto/argon2"
)
// Hash a password — can't be reversed to the original.
func storePassword(password string) string {
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
panic(err)
}
// Argon2id with the same parameters as the Python example
hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 2, 32)
return fmt.Sprintf("$argon2id$v=19$m=65536,t=3,p=2$%s$%s",
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash))
}
// Verify a stored password hash.
func verifyPassword(storedHash string, password string) bool {
// ... parse the hash, recompute argon2.IDKey with the stored salt,
// and compare in constant time (e.g. subtle.ConstantTimeCompare)
return false
}
// Sensitive data encryption — NOT hashing
// Encrypt — decryptable for processing payments.
func storeCreditCard(cardNumber string, key []byte) (map[string]string, error) {
return encryptData([]byte(cardNumber), key)
}
Key Management: The Biggest Challenge #
Correct encryption can fail completely due to poor key management. Encryption keys are the “keys” to the entire security system.
// ANTI-PATTERN: hardcoding keys in source code
// const secretKey = "mysecretkey123" // DON'T — source code can leak in git
// ANTI-PATTERN: keys too short or weak
// key := []byte("password") // 64-bit — easily brute-forced
// ANTI-PATTERN: the same key for every purpose
// masterKey := make([]byte, 32) // one key for everything → one leak, everything leaks
// CORRECT: keys from environment variables or secret managers
import (
"encoding/hex"
"fmt"
"os"
)
// Get the key from the environment or a secret manager.
func getEncryptionKey() ([]byte, error) {
keyHex := os.Getenv("ENCRYPTION_KEY")
if keyHex == "" {
return nil, fmt.Errorf("ENCRYPTION_KEY not configured")
}
key, err := hex.DecodeString(keyHex)
if err != nil {
return nil, err
}
if len(key) != 32 {
return nil, fmt.Errorf("ENCRYPTION_KEY must be 256-bit (64 hex chars)")
}
return key, nil
}
// Different keys for different purposes
func setupKeys() map[string][]byte {
return map[string][]byte{
"user_pii_key": []byte(os.Getenv("USER_PII_ENCRYPTION_KEY")),
"payment_key": []byte(os.Getenv("PAYMENT_ENCRYPTION_KEY")),
"session_key": []byte(os.Getenv("SESSION_ENCRYPTION_KEY")),
}
}
Key Management Best Practices:
Key storage:
✓ Environment variables (not in source code)
✓ Secret managers: AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager
✓ HSMs (Hardware Security Modules) for highly critical keys
✗ Hardcoded in source code
✗ Stored in the same database as the encrypted data
✗ Committed to git repositories
Key separation:
✓ Different keys for different purposes (PII, payment, session)
✓ Different keys for different environments (dev, staging, prod)
✓ Envelope encryption: DEKs for data, KEKs for DEKs
Key rotation:
✓ Periodic rotation (e.g. every 90 days for KEKs)
✓ Immediate rotation on compromise indications
✓ Envelope encryption eases rotation (just re-encrypt DEKs)
✗ Never-rotated keys are continuously growing risks
Access:
✓ Least privilege principles — only processes needing keys can access them
✓ Audit logs for all key access
✓ Periodic access credential rotation
Encryption in Different Contexts #
API Communication #
// Digital signatures with Ed25519 (proving authenticity, not confidentiality)
import (
"crypto/ed25519"
"crypto/rand"
)
// Generate an Ed25519 keypair for digital signatures.
func generateSigningKeypair() (ed25519.PrivateKey, ed25519.PublicKey) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
return privateKey, publicKey
}
// Sign a payload to prove authenticity.
func signPayload(payload []byte, privateKey ed25519.PrivateKey) []byte {
return ed25519.Sign(privateKey, payload)
}
// Verify a signature.
func verifySignature(payload []byte, signature []byte, publicKey ed25519.PublicKey) bool {
return ed25519.Verify(publicKey, payload, signature)
}
Backup Encryption #
# Encrypt backups before uploading to cloud storage
# Use GPG with symmetric encryption
# Encrypt the backup:
gpg --symmetric \
--cipher-algo AES256 \
--batch \
--passphrase-file /secure/location/backup.passphrase \
database_backup.sql.gz
# Upload to S3 (already encrypted):
aws s3 cp database_backup.sql.gz.gpg s3://backup-bucket/
# Verification: even if the S3 bucket is compromised,
# the file is still encrypted with AES-256
Algorithms No Longer Secure #
It’s important to know what to avoid:
DON'T use for new encryption:
DES / 3DES:
→ DES has been cracked since 1998
→ 3DES still used in legacy systems but deprecated
→ Replace with AES-256
RC4:
→ Many cryptographic weaknesses found
→ Already banned in TLS
MD5 and SHA-1 for security:
→ Collisions found (two different inputs producing the same hash)
→ Unsafe for digital signatures or certificates
→ Still OK for non-security checksums (like verifying downloads)
RSA < 2048 bits:
→ 1024-bit RSA is considered insecure
→ Use a minimum of 2048 bits, ideally 4096 bits
ECB mode:
→ Plaintext patterns visible in the ciphertext
→ Use GCM or CBC with HMAC
Currently recommended (2025):
Symmetric encryption: AES-256-GCM
Asymmetric encryption: RSA-4096, ECDSA (P-256 or P-384), Ed25519
Key exchange: X25519 (ECDH with Curve25519)
Hashing: SHA-256, SHA-384, SHA-512 (not MD5/SHA-1)
Password hashing: Argon2id, bcrypt, scrypt
Digital signatures: Ed25519, ECDSA P-256
TLS: 1.2 minimum, 1.3 recommended
Anti-Patterns to Avoid #
// ✗ Anti-pattern 1: encrypting passwords
// encryptedPassword := encrypt(password, key)
// If the key leaks, all passwords are decrypted
// ✓ Solution: hash with Argon2id, bcrypt, or scrypt
// ✗ Anti-pattern 2: reusing nonces/IVs
// nonce := make([]byte, 12) // static nonce — FATAL for GCM!
// Reusing a nonce with the same key destroys GCM security
// ✓ Solution: crypto/rand for every encryption
// ✗ Anti-pattern 3: keys derived from weak passwords without KDFs
// key := []byte(password)[:32] // not a good key
// Short passwords → weak keys
// ✓ Solution: use PBKDF2/Argon2 to derive keys from passwords
// ✗ Anti-pattern 4: storing keys in databases
// Keys and encrypted data in the same place
// If the database leaks, both leak
// ✓ Solution: keys in secret managers or environment variables
// ✗ Anti-pattern 5: CBC mode without authentication
// AES-CBC without HMAC is vulnerable to padding oracle attacks
// block, _ := aes.NewCipher(key)
// ciphertext := cipher.NewCBCEncrypter(block, iv).CryptBlocks(...)
// ✓ Solution: use AES-GCM which provides authenticated encryption
// ✗ Anti-pattern 6: implementing your own cryptographic algorithms
func myEncrypt(data, key []byte) []byte {
// Custom XOR or similar implementations
out := make([]byte, len(data))
for i := range data {
out[i] = data[i] ^ key[i%len(key)]
}
return out
}
// Highly insecure — use battle-tested libraries
// ✓ Solution: crypto/aes (Go), BouncyCastle (Java), WebCrypto API (JS)
Encryption Checklist #
DATA IN TRANSIT:
□ All endpoints use HTTPS
□ TLS 1.2 minimum, TLS 1.3 recommended
□ TLS 1.0 and 1.1 disabled
□ Forward secrecy (ECDHE or DHE ciphers) enabled
□ HSTS headers installed with adequate durations
□ Certificates renewed before expiry (automatic monitoring)
□ SSL Labs rating of A or A+
DATA AT REST:
□ Sensitive database fields encrypted (PII, payment data, etc.)
□ Backups encrypted before external storage
□ Disk encryption active on production servers
□ Algorithm used: AES-256-GCM
KEY MANAGEMENT:
□ No keys in source code or git repositories
□ Keys stored in environment variables or secret managers
□ Different keys for different environments (dev/staging/prod)
□ Different keys for different purposes (PII, payment, session)
□ Key rotation scheduled periodically
□ Audit logs for sensitive key access
ALGORITHMS:
□ No DES, 3DES, RC4, or MD5 in new code
□ No ECB mode
□ Nonces/IVs randomly generated for every encryption operation
□ No custom cryptographic implementations
HASHING:
□ Passwords hashed with Argon2id, bcrypt, or scrypt
□ Verification data (tokens) hashed with SHA-256
□ No MD5 or SHA-1 for security purposes
COMPLIANCE:
□ Encryption meets applicable regulatory requirements (PCI-DSS, GDPR, etc.)
□ Data retention and deletion policies defined
Summary #
- TLS is the minimum — all traffic must be HTTPS — plain HTTP lets anyone on the network read and modify data. TLS 1.3 with forward secrecy is the standard to achieve.
- Use AES-256-GCM for symmetric encryption — GCM provides authenticated encryption, protecting against tampering while providing confidentiality. Don’t use ECB mode.
- Nonces must be unique for every encryption — reusing a nonce with the same key fatally destroys GCM security. Always
os.urandom(12) for every encryption operation. - Encryption and hashing are two different things — passwords are hashed (one-way), sensitive data needing processing is encrypted (reversible). Using the wrong one opens security holes.
- Envelope encryption eases key rotation — DEKs encrypt data, KEKs encrypt DEKs. Rotating KEKs only requires re-encrypting DEKs, not all data.
- Encryption keys must never be in source code — use environment variables or secret managers. Keys hardcoded in git are already-leaked keys.
- Different keys for different purposes — one for PII, one for payments, one for sessions. Compromising one key doesn’t expose all data.
- Never implement your own cryptography — use battle-tested libraries (cryptography, BouncyCastle, libsodium). Custom cryptography almost always contains weaknesses.
- Outdated algorithms must be removed — DES, 3DES, RC4, MD5 for security, SHA-1 for digital signatures are all insecure. Migrate to AES-256-GCM and SHA-256/SHA-3.
- Sensitive database fields need encryption — card numbers, identities, and medical data must be encrypted even inside databases. Disk encryption isn’t enough if attackers gain database access.
#
- TLS is the minimum — all traffic must be HTTPS — plain HTTP lets anyone on the network read and modify data. TLS 1.3 with forward secrecy is the standard to achieve.
- Use AES-256-GCM for symmetric encryption — GCM provides authenticated encryption, protecting against tampering while providing confidentiality. Don’t use ECB mode.
- Nonces must be unique for every encryption — reusing a nonce with the same key fatally destroys GCM security. Always
os.urandom(12)for every encryption operation. - Encryption and hashing are two different things — passwords are hashed (one-way), sensitive data needing processing is encrypted (reversible). Using the wrong one opens security holes.
- Envelope encryption eases key rotation — DEKs encrypt data, KEKs encrypt DEKs. Rotating KEKs only requires re-encrypting DEKs, not all data.
- Encryption keys must never be in source code — use environment variables or secret managers. Keys hardcoded in git are already-leaked keys.
- Different keys for different purposes — one for PII, one for payments, one for sessions. Compromising one key doesn’t expose all data.
- Never implement your own cryptography — use battle-tested libraries (cryptography, BouncyCastle, libsodium). Custom cryptography almost always contains weaknesses.
- Outdated algorithms must be removed — DES, 3DES, RC4, MD5 for security, SHA-1 for digital signatures are all insecure. Migrate to AES-256-GCM and SHA-256/SHA-3.
- Sensitive database fields need encryption — card numbers, identities, and medical data must be encrypted even inside databases. Disk encryption isn’t enough if attackers gain database access.