Encryption โ Security: System-Level Failures
1. Why Should You Care?
Your application uses AES-256-GCM. Your TLS configuration gets an A+ from SSL Labs. Your passwords are hashed with Argon2id.
Then you get breached anyway.
Encryption is a lock on a door. It doesnโt help if the window is open, the key is under the mat, or the wall is made of cardboard.
2. The Security Mindset Gap
What Developers Think
Developer mental model:
"I encrypted the data, so it's secure."
Reality:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Attack Surface โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Data in plaintext โโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ [ Encryption ] โ Key management โโโโโผโโ Key exposure โ
โ โ โ โ
โ Encrypted data โโโโโโโโโโโโโโโโโโโโโโผโโ Storage breach โ
โ โ โ โ
โ [ Decryption ] โ Access control โโโโโผโโ Auth bypass โ
โ โ โ โ
โ Data in plaintext โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Encryption only protects the middle part! โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโThe Real Threat Model
Most breaches don't break encryption. They:
- Steal keys
- Access data before/after encryption
- Bypass authentication
- Exploit misconfigurations
- Use social engineering
- Find logical flaws
Strong encryption with weak operations = Weak system3. Plaintext Exposure
Case 1: Logging Sensitive Data
# DISASTER: Logging plaintext before encryption
import logging
def process_payment(card_number, amount):
logging.info(f"Processing payment: card={card_number}, amount={amount}")
encrypted_card = encrypt(card_number)
# Card number now in log files forever
# Log aggregation services
# Developer laptops
# Backup systems
# The encrypted database is secure.
# The logs are in 47 different places, unencrypted.Case 2: Error Messages
# DISASTER: Exceptions containing sensitive data
def decrypt_user_data(encrypted_data, key):
try:
return decrypt(key, encrypted_data)
except DecryptionError as e:
# Error message includes the key!
raise Exception(f"Failed to decrypt with key {key}: {e}")
# Error tracking services (Sentry, Bugsnag) now have your keysCase 3: Memory Dumps and Core Dumps
When processes crash, the OS may dump memory to disk:
- /var/crash/
- Windows error reports
- Docker container logs
Memory contains:
- Decrypted data
- Encryption keys
- Passwords in transit
The "encrypted" data exists in plaintext in memory.
Crash = permanent plaintext record.Case 4: Swap and Hibernation
Operating systems may write memory to disk:
Swap space:
- RAM overflow goes to disk
- Includes decrypted secrets
- May persist after reboot
Hibernation:
- Entire RAM written to disk
- All secrets saved unencrypted
- Recoverable with disk access
Cloud VMs:
- Hypervisor can read guest memory
- Live migration copies all RAM
- Snapshots include memory state4. Key Management Failures
Case 1: Keys in Source Code
# GitHub search: "AES_KEY" or "encryption_key" or "secret_key"
# Millions of results
# Found in real repositories:
AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"
ENCRYPTION_KEY = "super_secret_key_12345"
DATABASE_PASSWORD = "admin123"
# Once committed, even if deleted:
# - Still in git history
# - Cached by GitHub search
# - Stored in developer machines
# - Backed up multiple timesCase 2: Keys in Environment Variables (Leaked)
# docker-compose.yml committed to repo
services:
app:
environment:
- ENCRYPTION_KEY=aGVsbG8gd29ybGQK
- DATABASE_URL=postgres://user:password@db/prod
# CI/CD logs often print environment
# Container inspection reveals env vars
# Process listings show environmentCase 3: Key Reuse Across Environments
Common anti-pattern:
- Same key for dev, staging, and production
- Developer laptops have production keys
- Test databases encrypted with production keys
Breach in dev = Breach in productionCase 4: No Key Rotation
Company uses same encryption key for 10 years:
- Multiple employees who left had access
- Key may have leaked without detection
- All historical data vulnerable if key compromised
- No way to limit blast radius
Key rotation provides:
- Limited exposure window
- Ability to revoke compromised keys
- Regulatory compliance
- Cryptographic hygiene5. Operational Security Failures
Case 1: Backups
Production database: Encrypted at rest โ
Database backups: Unencrypted, stored on S3 โ
Real incident (2019):
- Company encrypted their MongoDB properly
- Backup script dumped to unencrypted S3 bucket
- Bucket was public
- 250 million records exposed
The encryption was perfect.
The backup process bypassed it entirely.Case 2: Development and Debug Access
# Production code with debug endpoint
@app.route('/debug/user/<user_id>')
def debug_user(user_id):
if request.args.get('debug_key') == 'supersecret':
user = get_user(user_id)
return {
'encrypted_data': user.encrypted_data,
'encryption_key': user.encryption_key, # ๐ฑ
'decrypted_data': decrypt(user.encrypted_data, user.encryption_key)
}
# "We'll remove this before production"
# Narrator: They did not.Case 3: Support and Admin Access
Typical enterprise:
- 50+ people with production database access
- 200+ people with access to decryption keys
- 500+ people who could theoretically access data
Each person is:
- A potential insider threat
- A social engineering target
- A laptop theft risk
- An account compromise risk
Encryption doesn't help when authorized users are the threat.Case 4: Third-Party Integration
Your security:
- End-to-end encrypted storage
- HSM-backed key management
- Zero-trust architecture
Your vendor integration:
"Just POST the customer data to our webhook as JSON"
// Actual code found in production
async function sendToVendor(customer) {
await fetch('https://vendor.com/webhook', {
method: 'POST',
body: JSON.stringify({
ssn: customer.ssn, // ๐ฑ
bank_account: customer.bank_account, // ๐ฑ
password: customer.password // ๐ฑ๐ฑ๐ฑ
})
});
}6. Side-Channel Leaks
Timing Attacks in Practice
# Authentication timing leak
def authenticate(username, password):
user = database.get_user(username)
if user is None:
return False # Fast: user not found
if not verify_password(password, user.password_hash):
return False # Slow: bcrypt comparison
return True
# Attacker can enumerate valid usernames:
# Invalid username: 1ms response
# Valid username: 500ms response (bcrypt)Cache-Based Leaks
AES implementations may have timing variations:
- Table lookups depend on cache state
- Different key bytes = different cache patterns
- Measurable from same machine or VM
Research has demonstrated:
- Extracting AES keys from co-located VMs
- Cross-process key extraction
- JavaScript cache timing attacks
Your "encrypted" data's key leaked through CPU cache timing.Network-Based Leaks
HTTPS protects content, not metadata:
Observable by network:
- Request timing (when you access data)
- Request size (roughly what you access)
- Request frequency (how often you check)
- Endpoints accessed (which features you use)
Traffic analysis can reveal:
- What websites you visit (by packet sizes)
- What you're typing (by keystroke timing)
- What you're watching (by bandwidth patterns)7. Architectural Failures
Case 1: Client-Side โSecurityโ
// "Encrypted" password storage in browser
function savePassword(password) {
const encrypted = btoa(password); // This is base64, not encryption!
localStorage.setItem('password', encrypted);
}
// Even with real encryption:
const key = 'hardcoded_key_in_js'; // Visible in source
const encrypted = CryptoJS.AES.encrypt(password, key);
// Anyone can read the source and decryptCase 2: Security Through Obscurity
Real example from IoT device:
- Communication "encrypted" with XOR
- Key was the string "security"
- Repeated for longer messages
"Nobody will reverse engineer our protocol"
Narrator: It took 15 minutes.Case 3: Encrypt-Then-Authenticate vs Authenticate-Then-Encrypt
Wrong order (vulnerable to padding oracle):
1. Encrypt plaintext
2. Compute MAC of ciphertext
3. Attacker modifies ciphertext
4. Server decrypts first, checks MAC second
5. Decryption errors leak information!
Right order (or use AEAD):
1. Compute MAC
2. Encrypt (plaintext + MAC)
3. Attacker modifications detected before decryption
Or just use AES-GCM which handles this correctly.Case 4: The Confused Deputy
# Server correctly encrypts data per-user
def get_document(user_id, doc_id):
doc = database.get_document(doc_id)
# Decrypt with user's key
key = get_user_key(user_id)
return decrypt(doc.encrypted_content, key)
# But authorization check is wrong!
def get_document(user_id, doc_id):
doc = database.get_document(doc_id)
# Forgot to check if user_id owns doc_id!
key = get_user_key(user_id) # Gets WRONG user's key
return decrypt(doc.encrypted_content, key) # Decryption fails... or worse
# What if attacker replaces encrypted content with their own?
# What if keys are accidentally shared?8. Real-World Breach Examples
Capital One (2019)
What they had:
- AWS encryption at rest
- AWS encryption in transit
- Proper key management
What went wrong:
- SSRF vulnerability in WAF
- Attacker accessed instance metadata
- Got IAM credentials
- Used credentials to access S3
- Downloaded 100 million customer records
The encryption was irrelevant.
Access control failure = breach.Equifax (2017)
What they had:
- Encrypted databases
- Security team
- Compliance certifications
What went wrong:
- Unpatched Apache Struts (CVE known for months)
- Attackers got shell access
- Accessed data through application (which has decryption access)
- 147 million records exposed
The encryption was irrelevant.
The application was the authorized accessor.Adobe (2013)
What they had:
- Encrypted passwords (but with ECB mode)
- Same key for all passwords
What went wrong:
- Database breach (separate vulnerability)
- ECB mode: same password = same ciphertext
- Password hints stored in plaintext
- Cross-reference hints with ciphertext patterns
User 1: Ciphertext ABC, Hint: "my cat's name"
User 2: Ciphertext ABC, Hint: "rhymes with fiskers"
User 3: Ciphertext ABC, Hint: "whiskers"
Attackers decrypted millions without breaking encryption.9. What Actually Works
Defense in Depth
Layer 1: Network security
- Firewalls, segmentation, WAF
- Stops network-based attacks
Layer 2: Authentication & Authorization
- Strong authentication (MFA)
- Principle of least privilege
- Stops unauthorized access
Layer 3: Application security
- Input validation
- Secure coding practices
- Stops application-level attacks
Layer 4: Data security
- Encryption at rest and in transit
- Key management
- Stops data theft if other layers fail
Layer 5: Monitoring & Response
- Logging, alerting, incident response
- Detects and contains breaches
Each layer catches what others miss.Practical Checklist
Before deployment, verify:
[ ] Secrets not in source code or logs
[ ] Encryption keys rotated regularly
[ ] Access controls tested (try to bypass them!)
[ ] Backups encrypted with different keys
[ ] Debug endpoints removed
[ ] Third-party integrations secured
[ ] Monitoring and alerting configured
[ ] Incident response plan exists
[ ] All team members trained on security
[ ] Regular security audits scheduled10. Summary
Three things to remember:
Encryption protects data in one state. Data exists before encryption, after decryption, in memory, in logs, in backups, in error messages. Encryption only protects the encrypted form.
Access control failures bypass encryption. If attackers can access data through your application (which must decrypt it to use it), your encryption is irrelevant. Most breaches are access control failures, not cryptographic breaks.
Security is a system property, not a feature. You canโt add encryption and be โdoneโ with security. Security requires ongoing attention to operations, monitoring, access control, and human factors.
11. Whatโs Next
You understand why encryption alone isnโt enough. But how do you think like a security engineer? How do you build systems that resist attack?
In the next article: Building Security Judgmentโthinking like an attacker, threat modeling, and knowing when โgood enoughโ really is good enough.
