# backend/auth/password.py # Argon2id password hashing — OWASP 2024 recommended algorithm. # Parameters (BUG-038): time_cost=2, memory_cost=19 MiB, parallelism=1 # (OWASP current recommendation for Argon2id). The previous 64 MiB setting # allowed memory exhaustion under concurrent login attempts. from argon2 import PasswordHasher from argon2.exceptions import VerificationError, VerifyMismatchError #: Argon2id cost parameters (OWASP 2024: m=19456 KiB, t=2, p=1). ARGON2_TIME_COST = 2 ARGON2_MEMORY_COST_KIB = 19456 # 19 MiB ARGON2_PARALLELISM = 1 ph = PasswordHasher( time_cost=ARGON2_TIME_COST, memory_cost=ARGON2_MEMORY_COST_KIB, parallelism=ARGON2_PARALLELISM, hash_len=32, salt_len=16, ) # Password policy (BUG-028). Applied by the API validators at account creation # and password change so the rules stay consistent across both paths. MIN_PASSWORD_LENGTH = 8 MAX_PASSWORD_LENGTH = 128 def validate_password_strength(password: str) -> str: """Validate a plaintext password against the project policy. Args: password: Candidate password. Returns: The password unchanged when valid. Raises: ValueError: When the password is too short, too long or blank. """ if password is None or len(password) < MIN_PASSWORD_LENGTH: raise ValueError(f"Minimum {MIN_PASSWORD_LENGTH} caractères") if len(password) > MAX_PASSWORD_LENGTH: raise ValueError(f"Maximum {MAX_PASSWORD_LENGTH} caractères") if not password.strip(): raise ValueError("Le mot de passe ne peut pas être vide") return password def hash_password(password: str) -> str: """Hash a password with Argon2id.""" return ph.hash(password) def verify_password(password: str, hashed: str) -> bool: """Verify a password against its Argon2id hash.""" try: return ph.verify(hashed, password) except (VerifyMismatchError, VerificationError): return False def needs_rehash(hashed: str) -> bool: """Check if hash needs updating (parameters changed).""" return ph.check_needs_rehash(hashed)