Files
ss-tools/backend/src/core/encryption.py
busya 64564da988 fix(git): fix 17 missing async/await bugs + UX overhaul
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
2026-07-01 20:47:25 +03:00

168 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# #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