Spaces:
Running
Running
File size: 2,454 Bytes
09801ca | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | """
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()
|