b3a0c4bfd8
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/71f26285-5584-42b2-8255-8ad2c9e9ecb4
5.3 KiB
5.3 KiB
ADR 002: Use Fernet Encryption for Mail Credentials
Status: Accepted
Date: 2026-01-20
Deciders: Security Team, Development Team
Context
The application stores POP3/IMAP credentials for user mail accounts. These credentials must be:
- Encrypted at rest in the database
- Decryptable when needed for mail operations
- Protected with industry-standard encryption
- Simple to implement and maintain
Security requirements:
- Symmetric encryption (need to decrypt for use)
- At least AES-128 bit encryption
- Per-user salt for additional security
- Key rotation capability
Decision
We will use Fernet (symmetric encryption) from the Python cryptography library for encrypting mail credentials.
Alternatives Considered
1. AES Directly (PyCrypto/cryptography)
- Pros: Full control, widely supported
- Cons: Easy to implement incorrectly, need to handle padding, IV, etc.
2. Database-Level Encryption (PostgreSQL)
- Pros: Transparent to application, secure
- Cons: All-or-nothing encryption, harder key rotation, requires DB support
3. HashiCorp Vault
- Pros: Enterprise-grade secret management, audit logs, key rotation
- Cons: Additional infrastructure, complexity, operational overhead
4. AWS KMS / Cloud KMS
- Pros: Managed service, automatic key rotation
- Cons: Cloud vendor lock-in, network latency for each decrypt, cost
Rationale
Fernet was chosen because:
- High-Level API: Implements encryption best practices by default
- Proven Security: Based on AES-128 in CBC mode with HMAC for authentication
- Python Native: Part of
cryptographylibrary (PyCA) - Timestamp Validation: Built-in support for expiring encrypted data
- No Complexity: Handles padding, IV, authentication tag automatically
- Battle-Tested: Used in production by many Python applications
Implementation Details
Key Derivation
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
# Generate key from master secret + per-user salt
kdf = PBKDF2(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100_000,
)
key = base64.urlsafe_b64encode(kdf.derive(ENCRYPTION_KEY.encode()))
Encryption/Decryption
def encrypt_password(password: str, user_id: int) -> str:
"""Encrypt password with user-specific salt."""
salt = get_user_salt(user_id)
key = derive_key(ENCRYPTION_KEY, salt)
fernet = Fernet(key)
return fernet.encrypt(password.encode()).decode()
def decrypt_password(encrypted_password: str, user_id: int) -> str:
"""Decrypt password with user-specific salt."""
salt = get_user_salt(user_id)
key = derive_key(ENCRYPTION_KEY, salt)
fernet = Fernet(key)
return fernet.decrypt(encrypted_password.encode()).decode()
Consequences
Positive
- Simple, secure implementation
- No risk of implementing encryption incorrectly
- Built-in authentication (prevents tampering)
- Can add TTL expiration if needed
- Easy to test and validate
Negative
- Slower than AES-GCM (includes HMAC overhead)
- Fixed to AES-128 (no AES-256 option without manual implementation)
- All encrypted values become invalid if master key changes (no key rotation)
Mitigation Strategies
For Key Rotation
# Support multiple encryption keys with versioning
ENCRYPTION_KEY_V1 = os.getenv('ENCRYPTION_KEY_V1')
ENCRYPTION_KEY_V2 = os.getenv('ENCRYPTION_KEY_V2') # New key
# Store key version with encrypted data
encrypted_data = f"v2:{fernet_v2.encrypt(data)}"
# Decrypt with appropriate key
version, encrypted = encrypted_data.split(':', 1)
if version == 'v1':
return fernet_v1.decrypt(encrypted)
elif version == 'v2':
return fernet_v2.decrypt(encrypted)
For Per-User Salt
# Generate unique salt per user (stored in users table)
def get_or_create_user_salt(user_id: int) -> bytes:
# Use deterministic salt based on user_id + global salt
# OR store random salt in database per user
return hashlib.sha256(f'pop3_forwarder_user_{user_id}'.encode()).digest()
Security Best Practices
- Never log encryption keys: Keys only in environment variables
- Rotate keys regularly: Plan for annual key rotation
- Secure key storage: Use secrets manager in production
- Strong master key: Minimum 32 characters, random
- Audit access: Log when credentials are decrypted
- Principle of least privilege: Only workers need decryption
Monitoring
- Track decryption failures (wrong key indicator)
- Monitor performance impact of encryption
- Alert on unusual decryption volume
- Log credential access for audit
Future Improvements
- Migrate to HashiCorp Vault for enterprise deployments
- Implement automatic key rotation
- Add encryption key versioning
- Consider AWS KMS for AWS deployments
- Add audit trail for credential access
Related Decisions
- See ADR-006 for key management in production
- See ../SECURITY_REPORT.md for security analysis