Spaces:
Running
Running
| """ | |
| Security Module — Password hashing and validation. | |
| Supports Argon2id (recommended) with automatic bcrypt migration on login. | |
| """ | |
| import re | |
| import hashlib | |
| import secrets | |
| from typing import Optional | |
| from passlib.context import CryptContext | |
| # Argon2id is the primary hasher; bcrypt is kept for backward compatibility | |
| _pwd_context = CryptContext( | |
| schemes=["argon2", "bcrypt"], | |
| default="argon2", | |
| deprecated=["bcrypt"], | |
| argon2__rounds=4, | |
| argon2__memory_cost=65536, | |
| argon2__parallelism=2, | |
| ) | |
| def hash_password(password: str) -> tuple[str, str]: | |
| """ | |
| Hash a password using Argon2id. | |
| Returns (hashed_password, algorithm_name). | |
| """ | |
| return _pwd_context.hash(password), "argon2" | |
| def verify_password(plain_password: str, hashed_password: str) -> bool: | |
| """ | |
| Verify a password against its hash. | |
| Works with both Argon2 and legacy bcrypt hashes. | |
| """ | |
| try: | |
| return _pwd_context.verify(plain_password, hashed_password) | |
| except Exception: | |
| return False | |
| def needs_rehash(hashed_password: str) -> bool: | |
| """ | |
| Check if a password hash needs to be upgraded (e.g., bcrypt → argon2). | |
| Call this after successful login to auto-migrate hashes. | |
| """ | |
| return _pwd_context.needs_update(hashed_password) | |
| def validate_password_strength(password: str) -> Optional[str]: | |
| """ | |
| Validate password meets minimum security requirements. | |
| Returns error message if invalid, None if valid. | |
| """ | |
| if len(password) < 8: | |
| return "Password must be at least 8 characters long" | |
| if len(password) > 128: | |
| return "Password must be at most 128 characters long" | |
| if not re.search(r"[a-z]", password): | |
| return "Password must contain at least one lowercase letter" | |
| if not re.search(r"[A-Z]", password): | |
| return "Password must contain at least one uppercase letter" | |
| if not re.search(r"\d", password): | |
| return "Password must contain at least one digit" | |
| if not re.search(r"[!@#$%^&*(),.?\":{}|<>_\-+=\[\]\\\/~`]", password): | |
| return "Password must contain at least one special character" | |
| return None | |
| def generate_token(length: int = 32) -> str: | |
| """Generate a cryptographically secure random token.""" | |
| return secrets.token_urlsafe(length) | |
| def hash_token(token: str) -> str: | |
| """Hash a token for storage (SHA-256). Used for refresh tokens, API keys.""" | |
| return hashlib.sha256(token.encode()).hexdigest() | |