"""Encryption and presentation helpers for credentials stored in the database.""" import re from cryptography.fernet import Fernet, InvalidToken from app.utils.Secrets import get_runtime_secret PREFIX = "enc:v1:" SENSITIVE_KEYS = re.compile( r"(?:api[_-]?key|password|passwd|secret|token|authorization|credential)", re.I ) def _fernet(): return Fernet(get_runtime_secret("credential_encryption_key").encode("ascii")) def is_encrypted(value): return isinstance(value, str) and value.startswith(PREFIX) def encrypt_credential(value): value = "" if value is None else str(value) if not value or is_encrypted(value): return value return PREFIX + _fernet().encrypt(value.encode("utf-8")).decode("ascii") def decrypt_credential(value): value = "" if value is None else str(value) if not is_encrypted(value): return value try: return _fernet().decrypt(value[len(PREFIX):].encode("ascii")).decode("utf-8") except (InvalidToken, ValueError) as exc: raise RuntimeError("credential decryption failed; check the configured encryption key") from exc def mask_credential(value): plain = decrypt_credential(value) if not plain: return "" if len(plain) <= 4: return "****" return plain[:2] + "****" + plain[-2:] def redact_mapping(value): if not isinstance(value, dict): return value return {key: ("[REDACTED]" if SENSITIVE_KEYS.search(str(key)) else val) for key, val in value.items()} def migrate_existing_credentials(): """Idempotently encrypt legacy plaintext credentials in existing rows.""" from app.models import LLMModel, StreamModel changed = 0 for item in LLMModel.objects.only("id", "api_key").iterator(): encrypted = encrypt_credential(item.api_key) if encrypted != item.api_key: LLMModel.objects.filter(pk=item.pk).update(api_key=encrypted) changed += 1 for item in StreamModel.objects.only( "id", "pull_stream_username", "pull_stream_password" ).iterator(): updates = {} for field in ("pull_stream_username", "pull_stream_password"): old_value = getattr(item, field) new_value = encrypt_credential(old_value) if new_value != old_value: updates[field] = new_value if updates: StreamModel.objects.filter(pk=item.pk).update(**updates) changed += 1 return changed