"""Sealing a credential Scribe stores so the database alone cannot read it. Fernet (AES-128-CBC with an HMAC) under a key derived from the instance's SECRET_KEY. A copy of the database, or of a backup made from it, then holds only ciphertext: reading the credential also needs the SECRET_KEY, which lives in the deployment's environment rather than in the data. The derivation is labelled and versioned, so this key is never the same bytes as anything else made from SECRET_KEY (the session-cookie signing uses it raw). An instance that changes its SECRET_KEY can no longer open what it sealed before: `unseal` answers None, and the caller asks for the credential again rather than failing on it. """ from __future__ import annotations import base64 import hashlib from cryptography.fernet import Fernet, InvalidToken from scribe.config import Config _LABEL = b"scribe.secrets.v1\x00" def _fernet() -> Fernet: digest = hashlib.sha256(_LABEL + Config.SECRET_KEY.encode()).digest() return Fernet(base64.urlsafe_b64encode(digest)) def seal(plain: str) -> str: """Encrypt `plain` for storage; the result is ASCII.""" return _fernet().encrypt(plain.encode()).decode() def unseal(token: str) -> str | None: """Decrypt a value `seal` produced, or None when it can't be opened — sealed under a different SECRET_KEY, or not a sealed value at all.""" if not token: return None try: return _fernet().decrypt(token.encode()).decode() except (InvalidToken, ValueError): return None