From 1171a35d9fead20bcaf3bb5394b1a12d30b522e2 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 6 Jul 2026 16:17:47 +0200 Subject: [PATCH] =?UTF-8?q?fix(rmi-backend):=20drop=20passlib=20=E2=80=94?= =?UTF-8?q?=20route=20backup=20codes=20through=20direct=20bcrypt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reported symptom (AttributeError: module 'bcrypt' has no attribute '__about__') was misleading. passlib 1.7.4 (the latest released version — the project is unmaintained) actually traps that AttributeError with a bare `except:` and continues. The real failure occurs later in `passlib.handlers.bcrypt._finalize_backend_mixin`: detect_wrap_bug -> verify(secret, bug_hash) where secret = (b"0123456789"*26)[:255] # 255-byte test secret bcrypt 4.0+ removed silent 72-byte truncation and now raises `ValueError: password cannot be longer than 72 bytes`, breaking the backend probe and crashing every `pwd_context.hash()/verify()` call. Three options were considered: A. Upgrade passlib — impossible, 1.7.4 is the latest PyPI release. B. Pin bcrypt<4.0 — conflicts with chromadb 1.1.1 (>=4.0.1 required), even though chromadb's bcrypt usage is optional and still works at runtime in practice. C. Shim bcrypt.__about__ — doesn't fix the actual truncation error. Chosen: refactor app/auth.py to use bcrypt directly for backup-code hashing too (the main `hash_password`/`verify_password` already did). Drops the passlib dependency entirely, restoring bcrypt 5.x for chromadb compatibility and eliminating the unmaintained passlib pin. Verified: * hash_password / verify_password round-trip OK * _hash_backup_code / _verify_backup_code round-trip OK on freshly generated 8-digit backup codes * `pytest tests/unit` -> 717 passed, 32 warnings, 60 subtests passed --- app/auth.py | 10 +++------- app/routers/bulletin.py | 2 +- requirements.txt | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/auth.py b/app/auth.py index 62b8e39..8862117 100644 --- a/app/auth.py +++ b/app/auth.py @@ -12,9 +12,9 @@ import secrets from datetime import datetime, timedelta from typing import Any +import bcrypt from fastapi import APIRouter, HTTPException, Request from jose import JWTError, jwt -from passlib.context import CryptContext from pydantic import BaseModel, Field from app.core.redis import get_redis @@ -86,12 +86,12 @@ def _generate_backup_codes(count: int = 8) -> list: def _hash_backup_code(code: str) -> str: """Hash a backup code for storage (same as password hashing).""" - return pwd_context.hash(code) + return hash_password(code) def _verify_backup_code(code: str, hashed: str) -> bool: """Verify a backup code against its hash.""" - return pwd_context.verify(code, hashed) + return verify_password(code, hashed) # ── Config ── @@ -99,10 +99,6 @@ JWT_SECRET = os.getenv("JWT_SECRET", "rmi-jwt-secret-change-me") JWT_ALGORITHM = "HS256" JWT_EXPIRY_DAYS = 7 -import bcrypt # noqa: E402 - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - def hash_password(password: str) -> str: """Hash password using bcrypt directly.""" diff --git a/app/routers/bulletin.py b/app/routers/bulletin.py index 701cf2e..eed4b6d 100644 --- a/app/routers/bulletin.py +++ b/app/routers/bulletin.py @@ -31,7 +31,7 @@ from app.services.supabase_service import ( # noqa: E402 ) -# Lazy auth dependency - avoids importing passlib+jose+bcrypt at module load +# Lazy auth dependency - avoids importing jose+bcrypt at module load async def _require_public_profile(request: Request): from app.auth import require_public_profile diff --git a/requirements.txt b/requirements.txt index 8bfa844..64164c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ pydantic>=2.9.0 python-telegram-bot>=21.0 supabase>=2.0.0 python-jose[cryptography]>=3.3.0 -passlib[bcrypt]>=1.7.4 python-multipart>=0.0.17 aiohttp>=3.11.0 feedparser>=6.0.11