"""Encryption at rest for forge tokens (0.24.0, P0/P2). Forge access tokens are the keys to the kingdom: they are stored Fernet-encrypted (AES-128-CBC + HMAC) in forge_connections.token_encrypted and never shown again after entry (the admin UI only ever displays a mask). Master key resolution (decision D5): 1. KANBANTE_MASTER_KEY (urlsafe base64, 32 bytes) — recommended; 2. otherwise a random key generated ONCE and kept in kv_store ("master_key_generated"): as safe as the database file itself, zero configuration, but it travels with DB backups — set the env var for anything stricter. """ import logging import os import secrets as _secrets from cryptography.fernet import Fernet from app import models logger = logging.getLogger("kanbante.crypto") _fernet: Fernet | None = None def _master_key() -> bytes: env_key = os.getenv("KANBANTE_MASTER_KEY", "").strip() if env_key: return env_key.encode("utf-8") stored = models.get_kv("master_key_generated") if stored: return stored.encode("utf-8") generated = Fernet.generate_key() models.set_kv("master_key_generated", generated.decode("utf-8")) logger.warning( "KANBANTE_MASTER_KEY not set: generated a master key and " "stored it in the database (kv_store). Set the env var to " "control it explicitly." ) return generated def _get_fernet() -> Fernet: global _fernet if _fernet is None: _fernet = Fernet(_master_key()) return _fernet def encrypt_token(plain: str) -> str: if not plain: return "" return _get_fernet().encrypt(plain.encode("utf-8")).decode("utf-8") def decrypt_token(stored: str) -> str: if not stored: return "" try: return _get_fernet().decrypt(stored.encode("utf-8")).decode("utf-8") except Exception as exc: logger.error("forge token decryption failed: %s", exc) return "" def mask_token(stored: str) -> str: return "••••••••" if stored else "—"