@cryptotaxi247 / CoPilot / commits / bba5e4a5

fix: 2FA setup paths — Pillow regression + clear TOTP key error (#838) (#852)

* fix: re-add Pillow — qrcode.make() needs it transitively for 2FA QR The unused-deps prune in #842 dropped Pillow because no Python file in backend/ has a direct `import PIL`. That was a false positive — `qrcode.make()` lazily imports `qrcode.image.pil` on its first call, which in turn does `from PIL import Image, ImageDraw`. Without Pillow installed, every 2FA enrolment crashes with: POST /api/auth/2fa/setup HTTP/1.0 500 Internal Server Error File "/opt/venv/lib/python3.11/site-packages/qrcode/image/pil.py", line 2 from PIL import Image, ImageDraw ModuleNotFoundError: No module named 'PIL' `qrcode 8.x`'s default image factory is the PIL one, used by `app/auth/services/totp.py:_generate_qr_data_uri` to produce the `data:image/png;base64,…` payload returned to the frontend. Adds Pillow to requirements.in with a comment explaining it's a transitive runtime dep that Python imports won't reveal at static audit time. pip-compile resolves to Pillow 12.2.0. Verified locally: - POST /api/auth/2fa/setup returns 200 with a valid PNG QR data URI (910 bytes), an otpauth_url, and 8 backup codes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: clear error when TOTP_ENCRYPTION_KEY is malformed (#838) When TOTP_ENCRYPTION_KEY is set to a value that isn't valid Fernet base64 (missing padding, surrounding quotes, accidental newline, truncated copy/paste), backend startup currently dies with an opaque chain: binascii.Error: Incorrect padding The above exception was the direct cause of the following exception: ValueError: Fernet key must be 32 url-safe base64-encoded bytes. …which doesn't tell the operator which env var is the problem or how to generate a valid replacement. Issue #838 reports exactly this class of confusion. Wraps the Fernet init at app/auth/services/totp.py:35 with try/except. On failure, raises a RuntimeError that names the env var, gives the expected format (32-byte url-safe base64, typically 44 chars ending with `=`), and includes the exact regen command: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" Plus an explicit hint about avoiding surrounding quotes or trailing whitespace in .env (a common cause). The underlying error is appended for diagnostic context. Defensive .strip() added when reading TOTP_ENCRYPTION_KEY — newlines or trailing spaces from .env editors no longer cause failures even if the rest of the value is correct. Verified locally: - garbage non-base64 string: surfaces the new clear RuntimeError - missing trailing `=` padding (the most likely real-world cause): surfaces the new clear RuntimeError - valid key: module loads cleanly (sanity check) Does not change behavior for correctly-configured deployments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: taylor_socfortress <taylor.walton@socfortress.co> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

taylorcopilot committed May 8, 2026 at 08:18 UTC bba5e4a566907f6909de057195869fe1aeb6e246
3 files changed +24 -2
backend/app/auth/services/totp.py
+22 -2
@@ -29,10 +29,30 @@ from app.db.db_session import async_engine
29 # haven't set TOTP_ENCRYPTION_KEY continue to decrypt stored TOTP secrets.
30 _totp_enc_key = os.environ.get("TOTP_ENCRYPTION_KEY")
31 if _totp_enc_key:
32 - _fernet_key = _totp_enc_key.encode()
32 + # Trim accidental whitespace/newlines that .env editors sometimes append
33 + _fernet_key = _totp_enc_key.strip().encode()
34 + _fernet_key_source = "TOTP_ENCRYPTION_KEY"
35 else:
36 _fernet_key = base64.urlsafe_b64encode(hashlib.sha256(AuthHandler.secret.encode()).digest())
35 -_fernet = Fernet(_fernet_key)
37 + _fernet_key_source = "JWT_SECRET (derived fallback)"
38 +
39 +try:
40 + _fernet = Fernet(_fernet_key)
41 +except (ValueError, TypeError) as e:
42 + # Surface a clear, actionable error instead of the obscure
43 + # `binascii.Error: Incorrect padding` chain. See issue #838.
44 + if _fernet_key_source == "TOTP_ENCRYPTION_KEY":
45 + raise RuntimeError(
46 + "TOTP_ENCRYPTION_KEY is malformed. Expected a 32-byte url-safe base64 key "
47 + "(typically 44 characters ending with '='). Generate a valid one with:\n"
48 + " python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"\n"
49 + f"Then paste the value into your .env without surrounding quotes or trailing whitespace. "
50 + f"(underlying error: {e})",
51 + ) from e
52 + # Fallback path failed — would be a bug, not a misconfiguration
53 + raise RuntimeError(
54 + f"Failed to initialise TOTP Fernet from derived JWT_SECRET key. (underlying error: {e})",
55 + ) from e
56
57 _pwd_ctx = CryptContext(schemes=["bcrypt"])
58
backend/requirements.in
+1
@@ -24,6 +24,7 @@ packaging
24 passlib
25 passlib[bcrypt]
26 pdfkit
27 +Pillow # transitive runtime dep of qrcode for 2FA QR generation (qrcode.make() uses qrcode.image.pil)
28 playwright
29 pydantic[email]
30 PyJWT
backend/requirements.txt
+1
@@ -134,6 +134,7 @@ oss2==2.19.1
134 packaging==26.2
135 passlib[bcrypt]==1.7.4
136 pdfkit==1.0.0
137 +pillow==12.2.0
138 playwright==1.59.0
139 policyuniverse==1.5.1.20231109
140 portalocker==2.10.1