Backend: - fix 17 missing 'await' in git route handlers causing silent no-ops (branches, diff, history, commit, push, pull, merge, promote, sync) - fix async coroutine passed to run_blocking in git_plugin.py Frontend: - add collapsible 'How it works' onboarding (GitHelpPanel) - add status legend with color-coded repository statuses - i18n: add 50+ missing keys, replace hardcoded strings - add Refresh button in modal header - add PROD deploy confirmation dialog (replaces browser prompt()) - add CommitHistory to workspace tab with timeline nodes - add post-commit success banner with next-step guidance - increase success toast duration to 8s - group local/remote branches in selector (optgroup) - format last_modified dates timezone-aware - change PROD badge from red to neutral indigo - extract shared resolveGitStatusToken to git-utils.ts - fix 'slug' label regression - remove dead init_repo_button key UI/UX audit fixes: - add descriptions to Create/Init buttons in init panel - add actionable CTA to server mismatch warning - improve checkbox text phrasing
168 lines
7.1 KiB
Python
168 lines
7.1 KiB
Python
# #region EncryptionCore [C:5] [TYPE Module] [SEMANTICS encryption, fernet, key, crypto, secret]
|
||
# @defgroup Core Module group.
|
||
# @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 base64
|
||
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]
|
||
# @defgroup Core Module group.
|
||
# @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.
|
||
# @RATIONALE Fernet key validation (Fernet(key)) is O(1) but repeated ~90× per translation
|
||
# run — cached at module level via get_encryption_manager() singleton to eliminate
|
||
# redundant env-var reads and key validation.
|
||
#
|
||
# @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
|
||
|
||
|
||
# Module-level singleton — avoids redundant env reads and Fernet construction
|
||
# across ~90 LLMProviderService instantiations per translation run.
|
||
_encryption_manager: EncryptionManager | None = None
|
||
|
||
|
||
# #region is_fernet_token [C:2] [TYPE Function] [SEMANTICS encryption,validation]
|
||
# @BRIEF Check whether a string looks like a valid Fernet-encrypted token.
|
||
# @PRE value is a string.
|
||
# @POST Returns True if value is base64-decodable with decoded length >= 32 bytes.
|
||
# @RATIONALE Differentiates Fernet-encrypted values from plaintext without decrypting.
|
||
# Used by ConnectionService and ConfigManager to handle plaintext legacy data
|
||
# vs. data corrupted by key mismatch.
|
||
# @REJECTED Trying decryption as detection was rejected — it logs noisy "Assumption
|
||
# violated" warnings for every plaintext value and cannot distinguish between
|
||
# "not encrypted" and "encrypted with a different key".
|
||
MIN_FERNET_TOKEN_LENGTH = 80
|
||
|
||
|
||
def is_fernet_token(value: str) -> bool:
|
||
if not value or len(value) < MIN_FERNET_TOKEN_LENGTH:
|
||
return False
|
||
try:
|
||
decoded = base64.urlsafe_b64decode(value.encode())
|
||
except Exception:
|
||
return False
|
||
return len(decoded) >= 32
|
||
# #endregion is_fernet_token
|
||
|
||
|
||
# #region get_encryption_manager [C:4] [TYPE Function] [SEMANTICS encryption,singleton,cache]
|
||
# @BRIEF Return the process-wide EncryptionManager singleton.
|
||
# @PRE ENCRYPTION_KEY env var is set to a valid Fernet key.
|
||
# @POST Returns the same EncryptionManager instance on every call (idempotent).
|
||
# @SIDE_EFFECT Creates EncryptionManager on first call (reads env, validates Fernet key).
|
||
# @RATIONALE Eliminates redundant ENCRYPTION_KEY reads and Fernet key validation —
|
||
# LLMProviderService was creating ~90 EncryptionManager instances per
|
||
# translation run (2 per batch). The singleton reduces this to 1.
|
||
def get_encryption_manager() -> EncryptionManager:
|
||
"""Return the process-wide EncryptionManager singleton.
|
||
|
||
Creates on first call; returns cached instance thereafter.
|
||
Eliminates redundant ENCRYPTION_KEY reads and Fernet key validation.
|
||
"""
|
||
global _encryption_manager
|
||
if _encryption_manager is None:
|
||
_encryption_manager = EncryptionManager()
|
||
return _encryption_manager
|
||
# #endregion get_encryption_manager
|
||
|
||
# #endregion EncryptionCore
|