backend/sensitive_keys.py
Rama master · réplica de lectura del repositorio original.
"""Classify which config keys / credential fields hold secrets that must be
encrypted at rest.
A key is sensitive when it ends with one of ``SENSITIVE_SUFFIXES`` or equals a
member of ``SENSITIVE_EXACT`` — UNLESS it is in ``EXCLUDED_KEYS`` or namespaced
(contains a ``.``).
Excluded:
- ``ui_session_secret``: the Flask ``SECRET_KEY`` source, stored inside the DB.
Encrypting it would co-locate key and ciphertext and create a circular
dependency (the encryption key must live outside the DB).
- ``ui_password_hash``: already a bcrypt hash, not reversible plaintext.
Namespaced (dotted) keys — e.g. ``plugin.<provider>.<field>`` or
``post_processing.<...>`` — are always treated as non-sensitive here, even if
their leaf segment matches a sensitive suffix. These keys are owned by
sub-repositories (``PluginRepository`` in ``db/repositories/plugins.py``,
``post_processing/config_store.py``) that read/write ``config_entries.value``
directly via raw ORM queries and never go through the encrypt/decrypt layer
(``ConfigRepository`` / ``config_crypto``). If the classifier marked a dotted
key sensitive, the back-fill migration would encrypt it, but the bypass
repository would then read back ``enc:v1:`` ciphertext as if it were the
plaintext value — a silent auth failure. Only flat, non-dotted top-level keys
(``opensubtitles_api_key``, ``api_key``, ``github_token``, ...) are the actual
encryption targets.
"""
SENSITIVE_SUFFIXES: tuple[str, ...] = (
"_api_key",
"_apikey",
"_password",
"_token",
"_secret",
"_pin",
)
SENSITIVE_EXACT: frozenset[str] = frozenset({"api_key"})
EXCLUDED_KEYS: frozenset[str] = frozenset({"ui_session_secret", "ui_password_hash"})
def is_sensitive_key(key: str) -> bool:
if key.startswith("hub."):
# Catalog provider credentials (providers/hub). Unlike the other dotted
# namespaces they are only ever read and written through
# ConfigRepository, which decrypts, so they can be encrypted at rest.
return _is_hub_secret(key)
if "." in key:
# Namespaced keys belong to sub-repositories that manage their own
# storage and bypass the encrypt/decrypt layer — see module docstring.
return False
if key in EXCLUDED_KEYS:
return False
if key in SENSITIVE_EXACT:
return True
return key.endswith(SENSITIVE_SUFFIXES)
def _is_hub_secret(key: str) -> bool:
try:
from providers.hub import is_hub_secret_key
return is_hub_secret_key(key)
except Exception: # noqa: BLE001 — unreadable manifests: fall back to the leaf name
leaf = key.rsplit(".", 1)[-1]
return (
leaf in SENSITIVE_EXACT
or leaf.endswith(SENSITIVE_SUFFIXES)
or leaf
in {
"password",
"token",
"secret",
"cookies",
"passkey",
}
)