Files
ss-tools/backend/src/core/encryption.py
busya a9a453109c security: critical auth fixes, test migration to SQLite+FK, 44 test fixes
CRITICAL (CVE-class):
- C-4: Remove DEV_MODE fallback with hardcoded postgres:postgres credentials
- C-3: WebSocket endpoints now require JWT or API key token (?token=) auth
- C-1: Superset environment passwords encrypted via Fernet at rest in DB
- H-4: SESSION_SECRET_KEY separated from JWT AUTH_SECRET_KEY
- H-1: Log injection via %s-formatting instead of f-strings
- H-5: X-Trace-ID header validated as UUID4 to prevent trace poisoning

INFRASTRUCTURE:
- New src/core/encryption.py — EncryptionManager extracted from llm_provider
- Test DB: per-module sqlite:///:memory: with PRAGMA foreign_keys=ON
- testcontainers PostgreSQL via TEST_DB=postgres env var
- conftest temp-file global engine (no 10GB shared-cache leak)

TEST FIXES (44 pre-existing → 0):
- test_smoke_plugins: module-level sys.modules mock isolated to per-test fixture
- test_migration_engine: EXT:Python:uuid → uuid syntax fix
- test_dashboards_api: mock env attributes + correct patch target
- test_constants_audit_fixes: sync expected constant names with actual
- test_defensive_guards: patch.object instead of module-level Repo mock
- test_clean_release_cli: removed empty config.json directory
- FK violations: TaskRecord parents in log_persistence, Environment in
  mapping_service, ReleaseCandidate in candidate_manifest_services

ORTHOGONAL TESTS (18 new):
- test_security_orthogonal.py: bcrypt 72-byte limit, unicode, API key
  format invariants, Fernet robustness, encryption key lifecycle,
  log injection protection, JWT edge cases

MODEL FIXES:
- MetricSnapshot.job_id: nullable with SET NULL (was broken FK for
  aggregate prune snapshots, hidden by SQLite)

CLEANUP:
- Removed stale :memory:test_main/test_auth/test_tasks file databases
- Removed duplicate #endregion in encryption_key.py
2026-05-26 14:58:49 +03:00

111 lines
4.6 KiB
Python

# #region EncryptionCore [C:5] [TYPE Module] [SEMANTICS encryption, fernet, key, crypto, secret]
# @BRIEF Core encryption infrastructure: Fernet-based EncryptionManager for reversible secret storage.
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [LoggerModule]
# @INVARIANT Encryption initialization never falls back to a hardcoded secret.
# ENCRYPTION_KEY must be set via environment or .env before first use.
# @PRE Runtime environment has a valid Fernet ENCRYPTION_KEY.
# @POST EncryptionManager provides encrypt/decrypt operations backed by validated Fernet key.
# @SIDE_EFFECT Reads ENCRYPTION_KEY from process environment.
# @DATA_CONTRACT Input[str] -> Encrypted[str] -> Output[str]
# @RATIONALE Extracted from services/llm_provider.py to decouple core encryption from LLM domain
# and enable reuse by ConfigManager for Superset password encryption (see [SEC:C-1]).
import os
from cryptography.fernet import Fernet
from .logger import belief_scope, logger
# #region _require_fernet_key [C:5] [TYPE Function]
# @BRIEF Load and validate the Fernet key used for secret encryption.
# @PRE ENCRYPTION_KEY environment variable must be set to a valid Fernet key.
# @POST Returns validated key bytes ready for Fernet initialization.
# @RELATION DEPENDS_ON -> [LoggerModule]
# @SIDE_EFFECT Emits belief-state logs for missing or invalid encryption configuration.
# @DATA_CONTRACT Input[ENCRYPTION_KEY:str] -> Output[bytes]
# @INVARIANT Encryption initialization never falls back to a hardcoded secret.
def _require_fernet_key() -> bytes:
with belief_scope("_require_fernet_key"):
raw_key = os.getenv("ENCRYPTION_KEY", "").strip()
if not raw_key:
logger.explore(
"Missing ENCRYPTION_KEY blocks EncryptionManager initialization"
)
raise RuntimeError(
"ENCRYPTION_KEY must be set. Run ensure_encryption_key() "
"during application startup before using EncryptionManager."
)
key = raw_key.encode()
try:
Fernet(key)
except Exception as exc:
logger.explore(
"Invalid ENCRYPTION_KEY blocks EncryptionManager initialization"
)
raise RuntimeError("ENCRYPTION_KEY must be a valid Fernet key") from exc
logger.reflect("Validated ENCRYPTION_KEY for EncryptionManager initialization")
return key
# #endregion _require_fernet_key
# #region EncryptionManager [C:5] [TYPE Class]
# @BRIEF Handles encryption and decryption of sensitive data like API keys and passwords.
# @RELATION CALLS -> [_require_fernet_key]
# @PRE ENCRYPTION_KEY is configured with a valid Fernet key before instantiation.
# @POST Manager exposes reversible encrypt/decrypt operations for persisted secrets.
# @SIDE_EFFECT Initializes Fernet cryptography state from process environment.
# @DATA_CONTRACT Input[str] -> Output[str]
# @INVARIANT Uses only a validated secret key from environment.
#
# @TEST_CONTRACT EncryptionManagerModel ->
# {
# required_fields: {},
# invariants: [
# "encrypted data can be decrypted back to the original string"
# ]
# }
# @TEST_FIXTURE basic_encryption_cycle -> {"data": "my_secret_key"}
# @TEST_EDGE decrypt_invalid_data -> raises Exception
# @TEST_EDGE empty_string_encryption -> {"data": ""}
# @TEST_INVARIANT symmetric_encryption -> verifies: [basic_encryption_cycle, empty_string_encryption]
class EncryptionManager:
# region EncryptionManager_init [TYPE Function]
# @PURPOSE: Initialize the encryption manager with a Fernet key.
# @PRE ENCRYPTION_KEY env var must be set to a valid Fernet key.
# @POST Fernet instance ready for encryption/decryption.
def __init__(self):
self.key = _require_fernet_key()
self.fernet = Fernet(self.key)
# endregion EncryptionManager_init
# region encrypt [TYPE Function]
# @PURPOSE: Encrypt a plaintext string.
# @PRE data must be a non-empty string.
# @POST Returns encrypted string.
def encrypt(self, data: str) -> str:
with belief_scope("encrypt"):
return self.fernet.encrypt(data.encode()).decode()
# endregion encrypt
# region decrypt [TYPE Function]
# @PURPOSE: Decrypt an encrypted string.
# @PRE encrypted_data must be a valid Fernet-encrypted string.
# @POST Returns original plaintext string.
def decrypt(self, encrypted_data: str) -> str:
with belief_scope("decrypt"):
return self.fernet.decrypt(encrypted_data.encode()).decode()
# endregion decrypt
# #endregion EncryptionManager
# #endregion EncryptionCore