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"]
    end
Symmetric 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.
#

← Previous: Brute Force   Next: Firewall

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact