import base64 import json import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM # The obvious placeholder shipped in .env.example — must be replaced with a real key. PLACEHOLDER_SECRET_KEY = "CHANGE_ME_generate_with_openssl_rand_base64_32" # The valid-but-PUBLIC key git used to ship in .env.example. Anyone can decrypt creds # encrypted with it, so warn — but don't hard-fail, since an existing install may have # encrypted its credentials under it and hard-failing would brick it mid-flight. _LEGACY_PUBLIC_KEY = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" def secret_key_problem() -> tuple[str, bool] | None: """Return (message, fatal) if LYRA_SECRET_KEY is unusable or insecure, else None. fatal=True ⇒ the app should refuse to start; fatal=False ⇒ start but warn loudly.""" b64 = os.environ.get("LYRA_SECRET_KEY") if not b64: return ("LYRA_SECRET_KEY is not set", True) if b64 == PLACEHOLDER_SECRET_KEY: return ("LYRA_SECRET_KEY is still the .env.example placeholder", True) try: k = base64.b64decode(b64) except Exception: return ("LYRA_SECRET_KEY is not valid base64", True) if len(k) != 32: return ("LYRA_SECRET_KEY must decode to 32 bytes", True) if b64 == _LEGACY_PUBLIC_KEY: return ( "LYRA_SECRET_KEY is the old PUBLIC example key that shipped in git — anyone " "can decrypt your stored credentials; generate a fresh key and re-enter them", False, ) return None def _key() -> bytes: b64 = os.environ.get("LYRA_SECRET_KEY") if not b64: raise RuntimeError("LYRA_SECRET_KEY is not set") k = base64.b64decode(b64) if len(k) != 32: raise RuntimeError("LYRA_SECRET_KEY must decode to 32 bytes") return k def encrypt_secret(plaintext: str) -> str: iv = os.urandom(12) full = AESGCM(_key()).encrypt(iv, plaintext.encode("utf-8"), None) # ct || tag(16) ct, tag = full[:-16], full[-16:] return json.dumps( { "iv": base64.b64encode(iv).decode(), "ct": base64.b64encode(ct).decode(), "tag": base64.b64encode(tag).decode(), } ) def decrypt_secret(envelope: str) -> str: e = json.loads(envelope) iv = base64.b64decode(e["iv"]) ct = base64.b64decode(e["ct"]) tag = base64.b64decode(e["tag"]) pt = AESGCM(_key()).decrypt(iv, ct + tag, None) # AESGCM expects ct || tag return pt.decode("utf-8")