Ctrl K
ComunidadGuías…

credstore.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Per-user encrypted credential store (collective-services backend).

Mail/Nextcloud passwords live here — Fernet-encrypted, one row per
(user, provider). The key lives ONLY in `.env` (MAIL_CREDS_KEY), never in
the DB and never in logs. Plaintext exists solely in-process between
decrypt and the IMAP/HTTP login call.
"""

VALID_PROVIDERS = ("gmail", "proton", "nextcloud")

# Required payload keys per provider (account = address/username, shown back
# in listings; secret = never returned by any endpoint).
PROVIDER_FIELDS = {
    "gmail": ("address", "app_password"),
    "proton": ("address", "bridge_password"),
    "nextcloud": ("url", "user", "password"),
}


def _get_key() -> bytes:
    """Load MAIL_CREDS_KEY, generating + persisting it on first use."""
    import os

    raw = (os.getenv("MAIL_CREDS_KEY") or "").strip()
    if raw:
        return raw.encode("utf-8")
    from cryptography.fernet import Fernet

    fresh = Fernet.generate_key()
    try:
        from config import ENV_PATH

        line = f"MAIL_CREDS_KEY={fresh.decode('utf-8')}\n"
        with open(ENV_PATH, "a", encoding="utf-8") as f:
            f.write(line)
        os.environ["MAIL_CREDS_KEY"] = fresh.decode("utf-8")
    except Exception:
        pass
    import logging as _logging

    _logging.getLogger("piSynapse").warning(
        "MAIL_CREDS_KEY was missing; a fresh key was generated. "
        "Previously stored credentials (if any) are unreadable."
    )
    return fresh


def encrypt_payload(payload: dict) -> str:
    """Encrypt a JSON-serializable payload. Never raises (ValueError instead)."""
    import json as _json

    from cryptography.fernet import Fernet

    try:
        return Fernet(_get_key()).encrypt(_json.dumps(payload).encode("utf-8")).decode("utf-8")
    except Exception as e:
        raise ValueError(f"credential encrypt failed: {e}") from e


def decrypt_payload(blob: str) -> dict | None:
    """Decrypt, or None on any failure (wrong key, tampered row). Never raises."""
    import json as _json

    from cryptography.fernet import Fernet

    try:
        return _json.loads(Fernet(_get_key()).decrypt(blob.encode("utf-8")).decode("utf-8"))
    except Exception:
        return None