Encryption
Auth supports deterministic field-level encryption for sensitive data using AES-256-CTR with HMAC-derived initialization vectors.
Overview
What is Encrypted
When encryption is enabled, Auth encrypts:
User identifiers in the
auth_membershiptablePermission names in the
auth_permissiontableRole descriptions (optional)
Why Deterministic Encryption
Deterministic encryption means the same plaintext always produces the same ciphertext.
Benefits:
Database queries work on encrypted data
User lookups remain efficient
Permission checks don’t require decryption
No performance impact on operations
Trade-offs:
Pattern analysis possible (same values = same ciphertext)
Acceptable for usernames and permission names
Still protects data at rest
Example
# Without encryption
Database: "alice@example.com", "bob@example.com", "manage_users"
# With deterministic encryption
Database: "xxqjTSaj0YGZD7v8khExdKkV+dA=", "sJ4Yaz56uRxmNF0mj3wOwUNE8Y8=", "k7Pq..."
Configuration
Generate Encryption Key
# Generate a Fernet key
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Output example:
9fJ3K8pL2mN5oP7qR9sT1uV3wX6yZ8aB4cD6eF8gH0i=
Enable Encryption
# Set environment variables
export AUTH_ENABLE_ENCRYPTION=true
export AUTH_ENCRYPTION_KEY="9fJ3K8pL2mN5oP7qR9sT1uV3wX6yZ8aB4cD6eF8gH0i="
Or in .env file:
AUTH_ENABLE_ENCRYPTION=true
AUTH_ENCRYPTION_KEY=9fJ3K8pL2mN5oP7qR9sT1uV3wX6yZ8aB4cD6eF8gH0i=
Verify Encryption
from auth import Authorization
import uuid
auth = Authorization(str(uuid.uuid4()))
# Add data (will be encrypted automatically)
auth.add_membership('alice@example.com', 'admin')
# Query works normally (encryption is transparent)
has_membership = auth.has_membership('alice@example.com', 'admin')
print(has_membership) # True
Technical Details
Encryption Algorithm
AES-256-CTR with HMAC-derived IVs:
Algorithm: AES (Advanced Encryption Standard)
Mode: CTR (Counter Mode)
Key Size: 256 bits
IV Generation: HMAC-SHA256 of plaintext (deterministic)
Key Derivation: PBKDF2 with 100,000 iterations
Implementation
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import hmac
import hashlib
class DeterministicEncryption:
def __init__(self, key: bytes):
self.key = key
def _derive_key(self, salt: bytes) -> bytes:
kdf = PBKDF2(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100_000,
backend=default_backend()
)
return kdf.derive(self.key)
def _generate_iv(self, plaintext: str) -> bytes:
# HMAC ensures same plaintext = same IV
h = hmac.new(self.key, plaintext.encode(), hashlib.sha256)
return h.digest()[:16] # AES block size
def encrypt(self, plaintext: str) -> str:
iv = self._generate_iv(plaintext)
cipher = Cipher(
algorithms.AES(self.key[:32]),
modes.CTR(iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext.encode()) + encryptor.finalize()
return base64.b64encode(ciphertext).decode()
Data Flow
Writing encrypted data:
User Input: "alice@example.com"
↓
HMAC-SHA256 → IV (deterministic)
↓
AES-256-CTR encryption
↓
Base64 encoding
↓
Database: "xxqjTSaj0YGZD7v8khExdKkV+dA="
Reading encrypted data:
Query: "alice@example.com"
↓
Encrypt query value (same process)
↓
Database lookup: "xxqjTSaj0YGZD7v8khExdKkV+dA="
↓
Result returned (still encrypted)
↓
Decrypt for display (if needed)
Migration
Enabling Encryption on Existing Data
Important: Enabling encryption on an existing database requires migration.
from auth.database import SessionLocal
from auth.encryption import get_encryptor
from auth.models.sql import AuthMembership, AuthPermission
def migrate_to_encrypted():
"""Encrypt existing data"""
db = SessionLocal()
encryptor = get_encryptor()
try:
# Encrypt memberships
memberships = db.query(AuthMembership).all()
for membership in memberships:
if not is_encrypted(membership.user):
membership.user = encryptor.encrypt(membership.user)
# Encrypt permissions
permissions = db.query(AuthPermission).all()
for permission in permissions:
if not is_encrypted(permission.name):
permission.name = encryptor.encrypt(permission.name)
db.commit()
print("Migration completed successfully")
except Exception as e:
db.rollback()
print(f"Migration failed: {e}")
finally:
db.close()
Disabling Encryption
To disable encryption, decrypt existing data first:
def migrate_from_encrypted():
"""Decrypt existing data"""
db = SessionLocal()
encryptor = get_encryptor()
try:
# Decrypt memberships
memberships = db.query(AuthMembership).all()
for membership in memberships:
if is_encrypted(membership.user):
membership.user = encryptor.decrypt(membership.user)
# Decrypt permissions
permissions = db.query(AuthPermission).all()
for permission in permissions:
if is_encrypted(permission.name):
permission.name = encryptor.decrypt(permission.name)
db.commit()
# Now disable encryption
os.environ['AUTH_ENABLE_ENCRYPTION'] = 'false'
except Exception as e:
db.rollback()
raise
finally:
db.close()
Key Management
Key Storage
Development:
# .env file (gitignored)
AUTH_ENCRYPTION_KEY=9fJ3K8pL2mN5oP7qR9sT1uV3wX6yZ8aB4cD6eF8gH0i=
Production - AWS Secrets Manager:
import boto3
import json
def get_encryption_key():
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='auth/encryption-key')
return json.loads(response['SecretString'])['key']
os.environ['AUTH_ENCRYPTION_KEY'] = get_encryption_key()
Production - HashiCorp Vault:
import hvac
client = hvac.Client(url='https://vault.example.com')
client.auth.approle.login(role_id='...', secret_id='...')
secret = client.secrets.kv.v2.read_secret_version(path='auth/keys')
os.environ['AUTH_ENCRYPTION_KEY'] = secret['data']['data']['encryption_key']
Production - Kubernetes:
apiVersion: v1
kind: Secret
metadata:
name: auth-encryption
type: Opaque
data:
encryption-key: OWZKM0s4cEwybU41b1A3cVI5c1QxdVYzd1g2eVo4YUI0Y0Q2ZUY4Z0gwaz0=
Key Rotation
Generate new key:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Rotation process:
from auth.encryption import get_encryptor
def rotate_encryption_key(old_key, new_key):
"""Rotate encryption keys"""
db = SessionLocal()
old_encryptor = DeterministicEncryption(old_key)
new_encryptor = DeterministicEncryption(new_key)
try:
# Re-encrypt all data
memberships = db.query(AuthMembership).all()
for membership in memberships:
# Decrypt with old key
plaintext = old_encryptor.decrypt(membership.user)
# Encrypt with new key
membership.user = new_encryptor.encrypt(plaintext)
permissions = db.query(AuthPermission).all()
for permission in permissions:
plaintext = old_encryptor.decrypt(permission.name)
permission.name = new_encryptor.encrypt(plaintext)
db.commit()
print("Key rotation completed")
except Exception as e:
db.rollback()
raise
finally:
db.close()
Rotation schedule:
Development: No fixed schedule
Production: Quarterly or annually
After breach: Immediately
Security Considerations
Key Security
DO:
Use cryptographically secure random keys
Store keys in secrets management systems
Rotate keys regularly
Use different keys per environment
Restrict key access (need-to-know basis)
DON’T:
Commit keys to version control
Share keys via email/chat
Use weak or predictable keys
Store keys in application code
Reuse keys across applications
Encryption Limitations
Deterministic encryption protects against:
Unauthorized database access
Database backup theft
SQL injection (data still encrypted)
Insider threats (DBA access)
Deterministic encryption does NOT protect against:
Pattern analysis (same values visible)
Frequency analysis (common values identifiable)
Known plaintext attacks (if attacker has samples)
For maximum security:
Combine with database encryption at rest
Use network encryption (SSL/TLS)
Implement access controls
Enable audit logging
Performance Impact
Encryption Performance
Benchmarks (SQLite, 10,000 operations):
Without encryption: 1.2s
With encryption: 1.4s
Overhead: ~16%
Benchmarks (PostgreSQL, 10,000 operations):
Without encryption: 0.8s
With encryption: 0.9s
Overhead: ~12%
Optimization
from functools import lru_cache
@lru_cache(maxsize=1024)
def cached_encrypt(plaintext: str) -> str:
"""Cache encrypted values (deterministic = cacheable)"""
return encryptor.encrypt(plaintext)
Best Practices
Enable encryption from the start
Easier than migrating later.
Use environment variables for keys
Never hardcode in source.
Different keys per environment
Dev, staging, production should have separate keys.
Regular key rotation
Set a schedule and stick to it.
Audit key access
Track who accesses encryption keys.
Test key rotation
Practice the process before you need it.
Backup keys securely
Store in multiple secure locations.
Document key locations
Ensure team knows where keys are stored.
Troubleshooting
Decryption Errors
Error: “Invalid token” or “Decryption failed”
Causes:
Wrong encryption key
Data encrypted with different key
Corrupted ciphertext
Solution:
# Verify key matches
print(os.environ.get('AUTH_ENCRYPTION_KEY'))
# Test encryption/decryption
from auth.encryption import get_encryptor
encryptor = get_encryptor()
test = encryptor.encrypt('test')
assert encryptor.decrypt(test) == 'test'
Migration Issues
Error: Mixed encrypted/unencrypted data
Solution:
def is_encrypted(value: str) -> bool:
"""Check if value is encrypted (base64 check)"""
try:
import base64
base64.b64decode(value)
return len(value) > 20 # Encrypted values are longer
except:
return False
# Encrypt only unencrypted values
if not is_encrypted(membership.user):
membership.user = encryptor.encrypt(membership.user)
Next Steps
Security - Overall security practices
Configuration - Encryption configuration
Production Guide - Production deployment
Troubleshooting - Common issues