security: Make encryption always-on with auto-cleanup

BREAKING CHANGES:
- Encryption is now ALWAYS enabled (cannot be disabled)
- Removed DG_CACHE_ENCRYPTION environment variable

Security Enhancements:
- Encryption is mandatory for all cache operations
- Ephemeral encryption keys per process (forward secrecy)
- Automatic deletion of corrupted cache files on decryption failures
- Auto-cleanup on both decryption failures and SHA mismatches

Changes:
- Removed DG_CACHE_ENCRYPTION toggle from CLI and SDK
- Updated EncryptedCache to auto-delete corrupted files
- Simplified cache initialization (always wrapped with encryption)
- DG_CACHE_ENCRYPTION_KEY remains optional for persistent keys

Documentation:
- Updated CLAUDE.md with encryption always-on behavior
- Updated CHANGELOG.md with breaking changes
- Clarified security model and auto-cleanup behavior

Testing:
- All 119 tests passing with encryption always-on
- Type checking: 0 errors (mypy)
- Linting: All checks passed (ruff)

Rationale:
- Zero-trust cache architecture requires encryption
- Corrupted cache is security risk - auto-deletion prevents exploitation
- Ephemeral keys provide maximum security by default
- Users who need cross-process sharing can opt-in with persistent keys

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Simone Scarduzio
2025-10-10 09:51:29 +02:00
parent e8fb926fd6
commit ac7d4e067f
5 changed files with 51 additions and 25 deletions

View File

@@ -164,9 +164,17 @@ class EncryptedCache(CachePort):
decrypted_data = self._cipher.decrypt(encrypted_data)
except Exception as e:
# Fernet raises InvalidToken for tampering/wrong key
# SECURITY: Auto-delete corrupted cache files
try:
encrypted_path.unlink(missing_ok=True)
# Clean up mapping
if key in self._plaintext_sha_map:
del self._plaintext_sha_map[key]
except Exception:
pass # Best effort cleanup
raise CacheCorruptionError(
f"Decryption failed for {bucket}/{prefix}: {e}. "
f"Cache may be corrupted or key mismatch."
f"Corrupted cache deleted automatically."
) from e
# Validate SHA of decrypted content
@@ -174,9 +182,18 @@ class EncryptedCache(CachePort):
actual_sha = hashlib.sha256(decrypted_data).hexdigest()
if actual_sha != expected_sha:
# SECURITY: Auto-delete corrupted cache files
try:
encrypted_path.unlink(missing_ok=True)
# Clean up mapping
if key in self._plaintext_sha_map:
del self._plaintext_sha_map[key]
except Exception:
pass # Best effort cleanup
raise CacheCorruptionError(
f"Decrypted content SHA mismatch for {bucket}/{prefix}: "
f"expected {expected_sha}, got {actual_sha}"
f"expected {expected_sha}, got {actual_sha}. "
f"Corrupted cache deleted automatically."
)
# Write decrypted content to temporary file

View File

@@ -75,13 +75,9 @@ def create_service(
# Filesystem-backed with Content-Addressed Storage
base_cache = ContentAddressedCache(cache_dir, hasher)
# Apply encryption if enabled
enable_encryption = os.environ.get("DG_CACHE_ENCRYPTION", "true").lower() == "true"
cache: CachePort
if enable_encryption:
cache = EncryptedCache.from_env(base_cache)
else:
cache = base_cache
# Always apply encryption with ephemeral keys (security hardening)
# Encryption key is optional via DG_CACHE_ENCRYPTION_KEY (ephemeral if not set)
cache: CachePort = EncryptedCache.from_env(base_cache)
clock = UtcClockAdapter()
logger = StdLoggerAdapter(level=log_level)

View File

@@ -1159,13 +1159,9 @@ def create_client(
# Filesystem-backed with Content-Addressed Storage
base_cache = ContentAddressedCache(cache_dir, hasher)
# Apply encryption if enabled (default: true)
enable_encryption = os.environ.get("DG_CACHE_ENCRYPTION", "true").lower() == "true"
cache: CachePort
if enable_encryption:
cache = EncryptedCache.from_env(base_cache)
else:
cache = base_cache
# Always apply encryption with ephemeral keys (security hardening)
# Encryption key is optional via DG_CACHE_ENCRYPTION_KEY (ephemeral if not set)
cache: CachePort = EncryptedCache.from_env(base_cache)
clock = UtcClockAdapter()
logger = StdLoggerAdapter(level=log_level)