This commit is contained in:
2026-04-08 00:44:43 +03:00
parent 1c451016f9
commit d3f6736a2b
3 changed files with 8 additions and 35 deletions

View File

@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import os import os
import hashlib
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
@@ -13,10 +12,10 @@ from sqlalchemy.orm import Session
from backend.db import get_db from backend.db import get_db
from backend.models import User from backend.models import User
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
bearer = HTTPBearer(auto_error=False) bearer = HTTPBearer(auto_error=False)
AUTH_VERSION = "prehash-sha256-2026-04-07" AUTH_VERSION = "argon2-2026-04-07"
def _env(name: str, default: str) -> str: def _env(name: str, default: str) -> str:
@@ -29,38 +28,23 @@ JWT_ALG = _env("JWT_ALG", "HS256")
JWT_EXPIRES_MINUTES = int(_env("JWT_EXPIRES_MINUTES", "480")) # 8h JWT_EXPIRES_MINUTES = int(_env("JWT_EXPIRES_MINUTES", "480")) # 8h
def _bcrypt_safe_password(password: str) -> str:
"""
bcrypt only uses first 72 bytes of the input.
To support long passwords safely, pre-hash using SHA-256 when needed.
"""
raw = password.encode("utf-8")
if len(raw) <= 72:
return password
return hashlib.sha256(raw).hexdigest()
def hash_password(password: str) -> str: def hash_password(password: str) -> str:
safe = _bcrypt_safe_password(password)
try: try:
return pwd_context.hash(safe) return pwd_context.hash(password)
except Exception as e: except Exception as e:
orig_len = len(password.encode("utf-8")) orig_len = len(password.encode("utf-8"))
safe_len = len(safe.encode("utf-8"))
raise RuntimeError( raise RuntimeError(
f"{str(e)} (orig_bytes_len={orig_len}, safe_bytes_len={safe_len}, safe_type={type(safe).__name__})" f"{str(e)} (orig_bytes_len={orig_len})"
) from e ) from e
def verify_password(password: str, password_hash: str) -> bool: def verify_password(password: str, password_hash: str) -> bool:
safe = _bcrypt_safe_password(password)
try: try:
return pwd_context.verify(safe, password_hash) return pwd_context.verify(password, password_hash)
except Exception as e: except Exception as e:
orig_len = len(password.encode("utf-8")) orig_len = len(password.encode("utf-8"))
safe_len = len(safe.encode("utf-8"))
raise RuntimeError( raise RuntimeError(
f"{str(e)} (orig_bytes_len={orig_len}, safe_bytes_len={safe_len}, safe_type={type(safe).__name__})" f"{str(e)} (orig_bytes_len={orig_len})"
) from e ) from e

View File

@@ -33,12 +33,6 @@ from backend.db import engine, get_db # noqa: E402
from backend.models import Base, User # noqa: E402 from backend.models import Base, User # noqa: E402
def _password_ok_or_400(password: str):
# bcrypt has a 72-byte input limit (bytes, not chars)
if len(password.encode("utf-8")) > 72:
raise HTTPException(status_code=400, detail="Password is too long (max 72 bytes for bcrypt)")
class GenerationRequest(BaseModel): class GenerationRequest(BaseModel):
description: str description: str
platform: str = "GitHub Actions" # or GitLab CI platform: str = "GitHub Actions" # or GitLab CI
@@ -94,7 +88,6 @@ def register(req: RegisterRequest, db: Session = Depends(get_db)):
if not email or not req.password: if not email or not req.password:
raise HTTPException(status_code=400, detail="Email and password are required") raise HTTPException(status_code=400, detail="Email and password are required")
pw_bytes_len = len(req.password.encode("utf-8")) pw_bytes_len = len(req.password.encode("utf-8"))
_password_ok_or_400(req.password)
try: try:
exists = db.execute(select(User).where(User.email == email)).scalar_one_or_none() exists = db.execute(select(User).where(User.email == email)).scalar_one_or_none()
@@ -130,7 +123,6 @@ def login(req: LoginRequest, db: Session = Depends(get_db)):
email = req.email.strip().lower() email = req.email.strip().lower()
if not req.password: if not req.password:
raise HTTPException(status_code=400, detail="Password is required") raise HTTPException(status_code=400, detail="Password is required")
_password_ok_or_400(req.password)
try: try:
user = db.execute(select(User).where(User.email == email)).scalar_one_or_none() user = db.execute(select(User).where(User.email == email)).scalar_one_or_none()
if not user or not verify_password(req.password, user.password_hash): if not user or not verify_password(req.password, user.password_hash):

View File

@@ -4,10 +4,7 @@ requests
pyyaml pyyaml
sqlalchemy sqlalchemy
psycopg[binary] psycopg[binary]
passlib[bcrypt] passlib[argon2]
argon2-cffi
python-jose[cryptography] python-jose[cryptography]
pydantic[email] pydantic[email]
fastapi
uvicorn[standard]
requests
pyyaml