argon
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user