# #region EncryptionKeyModule [C:5] [TYPE Module] [SEMANTICS encryption, fernet, key, env, secret] # @defgroup Core Module group. # @BRIEF Resolve a Fernet encryption key from environment or .env file. # @LAYER Infrastructure # @RELATION DEPENDS_ON -> [LoggerModule] # @INVARIANT Runtime key resolution never falls back to an auto-generated key. # If no key is found in env or .env, the application crashes early. # @PRE ENCRYPTION_KEY is set in process environment or .env file. # @POST A valid Fernet key is returned and set in process environment. # @SIDE_EFFECT Sets os.environ["ENCRYPTION_KEY"] if loaded from .env file. # @DATA_CONTRACT Input[env_file_path] -> Output[encryption_key] # @RATIONALE Auto-generating a key on first run is dangerous — the key differs # per deployment, making encrypted data unrecoverable on restart. # Crash-early forces the admin to explicitly set ENCRYPTION_KEY. # @REJECTED Auto-generation rejected — encrypted data becomes unrecoverable # when the key changes between deployments (see [SEC:H-3]). from __future__ import annotations import os from pathlib import Path from cryptography.fernet import Fernet from .logger import belief_scope, logger DEFAULT_ENV_FILE_PATH = Path(__file__).resolve().parents[2] / ".env" # #region ensure_encryption_key [TYPE Function] # @ingroup Core # @BRIEF Ensure backend runtime has a persistent valid Fernet key. # @PRE ENCRYPTION_KEY is set in process environment or backend/.env file. # @POST Returns a valid Fernet key and sets it in process environment. # @SIDE_EFFECT Sets os.environ["ENCRYPTION_KEY"] if loaded from .env file. # @RAISES RuntimeError if no ENCRYPTION_KEY is found anywhere. def ensure_encryption_key(env_file_path: Path = DEFAULT_ENV_FILE_PATH) -> str: with belief_scope("ensure_encryption_key", f"env_file_path={env_file_path}"): # 1. Check process environment existing_key = os.getenv("ENCRYPTION_KEY", "").strip() if existing_key: Fernet(existing_key.encode()) logger.reason("Using ENCRYPTION_KEY from process environment.") return existing_key # 2. Check .env file if env_file_path.exists(): for raw_line in env_file_path.read_text(encoding="utf-8").splitlines(): if raw_line.startswith("ENCRYPTION_KEY="): persisted_key = raw_line.partition("=")[2].strip() if persisted_key: Fernet(persisted_key.encode()) os.environ["ENCRYPTION_KEY"] = persisted_key logger.reason(f"Loaded ENCRYPTION_KEY from {env_file_path}.") return persisted_key # 3. Crash-early — no auto-generation (see [SEC:H-3]) logger.explore( "No ENCRYPTION_KEY found in environment or .env file. " "Generate one with: python -c \"from cryptography.fernet import Fernet; " "print(Fernet.generate_key().decode())\" and set it in .env or export it." ) raise RuntimeError( "ENCRYPTION_KEY is not set. " "Generate a key and add it to your .env file:\n" " python -c \"from cryptography.fernet import Fernet; " "print(Fernet.generate_key().decode())\"" ) # #endregion ensure_encryption_key # #endregion EncryptionKeyModule