"""Encrypted secrets vault. Credentials are stored as Fernet ciphertext in app_secret; the key lives only in the environment (RECORDGOD_SECRET_KEY), never in the DB or the repo. If the key isn't set we REFUSE to store secrets — never silently store them in plaintext.""" import os from cryptography.fernet import Fernet, InvalidToken from sqlalchemy import text _key = os.getenv("RECORDGOD_SECRET_KEY") _fernet = Fernet(_key.encode()) if _key else None class SecretsUnconfigured(RuntimeError): pass def _f(): if _fernet is None: raise SecretsUnconfigured( "RECORDGOD_SECRET_KEY not set — refusing to store secrets unencrypted") return _fernet async def set_secret(db, name, plaintext): enc = _f().encrypt(plaintext.encode()) await db.execute(text(""" INSERT INTO app_secret (name, value_enc, updated_at) VALUES (:n, :v, now()) ON CONFLICT (name) DO UPDATE SET value_enc = EXCLUDED.value_enc, updated_at = now() """), {"n": name, "v": enc}) await db.commit() async def get_secret(db, name): """Decrypt one secret for server-side use (e.g. the Woo client). Never exposed over HTTP.""" row = (await db.execute(text("SELECT value_enc FROM app_secret WHERE name = :n"), {"n": name})).first() if not row: return None try: return _f().decrypt(bytes(row[0])).decode() except InvalidToken: return None # key rotated / corrupt — treat as not-set rather than crash async def list_secret_names(db): rows = await db.execute(text("SELECT name, updated_at FROM app_secret ORDER BY name")) return [{"name": r[0], "updated_at": r[1].isoformat() if r[1] else None} for r in rows]