Datavision / backend /app /core /security.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
2.45 kB
"""
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()