mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-18 22:33:24 +02:00
A stored secret that failed to decrypt was used as if it were plaintext. unseal_headers handed the Fernet token back, so every run whose context carried auth sent `Authorization: gAAAAAB...` to the target and reported success. EncryptedJSON returned None, so a scan context's auth silently vanished and the run went out unauthenticated. load_proxy_endpoints returned an empty list, so a scan configured to go through a proxy went direct instead. Rotating SECRET_KEY was enough to trigger all three. decrypt_stored() now separates the two cases a lenient decrypt conflated. A value that carries no Fernet token was written before sealing shipped (2026-09-06) and is returned as it is. A value that carries one and will not open raises SecretDecryptionError, naming the header, the column or the proxy and what to do about it. A stage turns that into a failed activity with the message on it; the api answers 503 through a handler beside the existing DBAPIError one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X5roHYDYzSPRNaTkn8hSkJ
62 lines
1.7 KiB
Python
62 lines
1.7 KiB
Python
import base64
|
|
import hashlib
|
|
import os
|
|
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
|
|
_DEFAULT_SECRET = "change-me-in-production-use-openssl-rand-hex-32" # noqa: S105
|
|
|
|
# a Fernet token opens with a 0x80 version byte and a zero-padded timestamp
|
|
_FERNET_PREFIX = "gAAAAA"
|
|
|
|
|
|
class SecretDecryptionError(RuntimeError):
|
|
"""A stored value is sealed but this instance's key cannot open it."""
|
|
|
|
|
|
def _fernet() -> Fernet:
|
|
secret = os.environ.get("SECRET_KEY", _DEFAULT_SECRET)
|
|
key = hashlib.sha256(secret.encode()).digest()
|
|
return Fernet(base64.urlsafe_b64encode(key))
|
|
|
|
|
|
def encrypt_secret(plaintext: str) -> str:
|
|
return _fernet().encrypt(plaintext.encode()).decode()
|
|
|
|
|
|
def decrypt_secret(token: str) -> str:
|
|
try:
|
|
return _fernet().decrypt(token.encode()).decode()
|
|
except InvalidToken as exc:
|
|
msg = "Invalid or corrupted secret token"
|
|
raise ValueError(msg) from exc
|
|
|
|
|
|
def try_decrypt(token: str | None) -> str | None:
|
|
if token is None:
|
|
return None
|
|
try:
|
|
return decrypt_secret(token)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def is_sealed(value: str) -> bool:
|
|
"""Whether a stored value carries a Fernet token."""
|
|
return value.startswith(_FERNET_PREFIX)
|
|
|
|
|
|
def decrypt_stored(value: str, *, label: str) -> str:
|
|
"""Open a sealed value. A value written before sealing shipped is returned as it is."""
|
|
if not is_sealed(value):
|
|
return value
|
|
try:
|
|
return decrypt_secret(value)
|
|
except ValueError as exc:
|
|
msg = (
|
|
f"{label} was sealed with a different SECRET_KEY and cannot be read. "
|
|
"Restore the SECRET_KEY this instance was set up with, or enter the "
|
|
"credential again."
|
|
)
|
|
raise SecretDecryptionError(msg) from exc
|