75 lines
2.7 KiB
Python
75 lines
2.7 KiB
Python
import base64
|
|
import hashlib
|
|
from typing import Optional
|
|
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
|
|
from .config import settings
|
|
|
|
|
|
ENCRYPTED_PREFIX = "enc:v1:"
|
|
SENSITIVE_SETTING_KEYS = frozenset(
|
|
{
|
|
"jellystat_api_key", "magent_ssl_certificate_pem", "magent_ssl_private_key_pem",
|
|
"magent_notify_email_smtp_password", "magent_notify_discord_webhook_url",
|
|
"magent_notify_telegram_bot_token", "magent_notify_push_token",
|
|
"magent_notify_push_user_key", "magent_notify_webhook_url", "jellyseerr_api_key",
|
|
"jellyfin_api_key", "sonarr_api_key", "radarr_api_key", "bazarr_api_key",
|
|
"prowlarr_api_key", "qbittorrent_password", "discord_webhook_url",
|
|
}
|
|
)
|
|
|
|
|
|
def _fernet_key() -> bytes:
|
|
configured = str(settings.settings_encryption_key or "").strip()
|
|
if configured:
|
|
try:
|
|
decoded = base64.urlsafe_b64decode(configured.encode("ascii"))
|
|
except Exception as exc:
|
|
raise RuntimeError("SETTINGS_ENCRYPTION_KEY must be a valid Fernet key") from exc
|
|
if len(decoded) != 32:
|
|
raise RuntimeError("SETTINGS_ENCRYPTION_KEY must decode to exactly 32 bytes")
|
|
return configured.encode("ascii")
|
|
jwt_secret = str(settings.jwt_secret or "").strip()
|
|
if len(jwt_secret) < 32 or jwt_secret == "change-me":
|
|
raise RuntimeError(
|
|
"SETTINGS_ENCRYPTION_KEY is required when JWT_SECRET is not a strong migration key"
|
|
)
|
|
derived = hashlib.sha256(("magent-settings-v1:" + jwt_secret).encode("utf-8")).digest()
|
|
return base64.urlsafe_b64encode(derived)
|
|
|
|
|
|
def is_sensitive_setting(key: str) -> bool:
|
|
return str(key or "").strip().lower() in SENSITIVE_SETTING_KEYS
|
|
|
|
|
|
def validate_secret_storage_configuration() -> None:
|
|
"""Validate the configured or JWT-derived Fernet key without touching stored data."""
|
|
Fernet(_fernet_key())
|
|
|
|
|
|
def encrypt_setting_value(key: str, value: Optional[str]) -> Optional[str]:
|
|
if value is None or not is_sensitive_setting(key):
|
|
return value
|
|
text = str(value)
|
|
if text.startswith(ENCRYPTED_PREFIX):
|
|
return text
|
|
token = Fernet(_fernet_key()).encrypt(text.encode("utf-8")).decode("ascii")
|
|
return ENCRYPTED_PREFIX + token
|
|
|
|
|
|
def decrypt_setting_value(key: str, value: Optional[str]) -> Optional[str]:
|
|
if value is None or not is_sensitive_setting(key):
|
|
return value
|
|
text = str(value)
|
|
if not text.startswith(ENCRYPTED_PREFIX):
|
|
return text
|
|
try:
|
|
return Fernet(_fernet_key()).decrypt(
|
|
text[len(ENCRYPTED_PREFIX) :].encode("ascii")
|
|
).decode("utf-8")
|
|
except InvalidToken as exc:
|
|
raise RuntimeError(
|
|
f"Stored secret '{key}' cannot be decrypted with the configured key"
|
|
) from exc
|