Files
ObsiGate/backend/auth/password.py
T
bruno 162a5b4acc
CI / lint (push) Successful in 1m20s
CI / security (push) Successful in 47s
CI / test (push) Successful in 2m21s
CI / build (push) Successful in 43s
CI / e2e (push) Successful in 10m48s
fix(security): consolidation & securite phase 1 (#84, BUG-021 a BUG-034)
- sanitizer XSS serveur (markdown + page de partage) [BUG-021/022]
- rate-limit/lockout MFA [BUG-023]
- isolation vaults par segments [BUG-024]
- caps regex ReDoS [BUG-025]
- SSRF webhooks + secrets externalises [BUG-026]
- rotation/revocation des jetons [BUG-027]
- politique de mot de passe + invalidation sessions [BUG-028]
- verrous users.json [BUG-029]
- IP reelle dans les audits [BUG-030]
- rate-limit par compte [BUG-031]
- symlinks hors vault ignores [BUG-032]
- recherche simple via inverted index [BUG-033]
- token en memoire + cookie HttpOnly, CSP durcie [BUG-034]

Tests: pytest 961 passed / 6 skipped, ruff 0, mypy 0, frontend vert.
2026-09-13 10:51:42 -04:00

59 lines
1.7 KiB
Python

# backend/auth/password.py
# Argon2id password hashing — OWASP 2024 recommended algorithm.
# Parameters: time_cost=2, memory_cost=64MB, parallelism=2
from argon2 import PasswordHasher
from argon2.exceptions import VerificationError, VerifyMismatchError
ph = PasswordHasher(
time_cost=2,
memory_cost=65536, # 64 MB
parallelism=2,
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)