104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
"""Password hashing implementation (application layer)."""
|
|
|
|
import bcrypt
|
|
|
|
|
|
class PasswordHasher:
|
|
"""Password hashing implementation using bcrypt."""
|
|
|
|
def __init__(self, rounds: int = 12):
|
|
"""
|
|
Initialize password hasher.
|
|
|
|
Args:
|
|
rounds: bcrypt cost factor (default: 12)
|
|
"""
|
|
if rounds < 4 or rounds > 31:
|
|
raise ValueError("rounds must be between 4 and 31")
|
|
|
|
self.rounds = rounds
|
|
|
|
def hash_password(self, password: str) -> str:
|
|
"""Hash a password using bcrypt."""
|
|
if not password:
|
|
raise ValueError("Password cannot be empty")
|
|
|
|
password_bytes = password.encode("utf-8")
|
|
salt = bcrypt.gensalt(rounds=self.rounds)
|
|
hashed = bcrypt.hashpw(password_bytes, salt)
|
|
return hashed.decode("utf-8")
|
|
|
|
def verify_password(self, password: str, hashed_password: str) -> bool:
|
|
"""Verify a password against a hash."""
|
|
if not password or not hashed_password:
|
|
return False
|
|
|
|
try:
|
|
password_bytes = password.encode("utf-8")
|
|
hashed_bytes = hashed_password.encode("utf-8")
|
|
return bcrypt.checkpw(password_bytes, hashed_bytes)
|
|
except Exception:
|
|
return False
|
|
|
|
def needs_rehash(self, hashed_password: str) -> bool:
|
|
"""Check if a password hash needs to be rehashed with a different cost factor."""
|
|
try:
|
|
hashed_bytes = hashed_password.encode("utf-8")
|
|
current_rounds = bcrypt.getsalt(hashed_bytes)
|
|
return current_rounds != self.rounds
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
class PasswordValidator:
|
|
"""Password validation rules."""
|
|
|
|
def __init__(
|
|
self,
|
|
min_length: int = 8,
|
|
require_uppercase: bool = True,
|
|
require_lowercase: bool = True,
|
|
require_digit: bool = True,
|
|
require_special: bool = False,
|
|
):
|
|
self.min_length = min_length
|
|
self.require_uppercase = require_uppercase
|
|
self.require_lowercase = require_lowercase
|
|
self.require_digit = require_digit
|
|
self.require_special = require_special
|
|
|
|
def validate(self, password: str) -> tuple[bool, Optional[str]]:
|
|
"""Validate password against rules."""
|
|
if not password:
|
|
return False, "Password cannot be empty"
|
|
|
|
if len(password) < self.min_length:
|
|
return False, f"Password must be at least {self.min_length} characters"
|
|
|
|
if self.require_uppercase and not any(c.isupper() for c in password):
|
|
return False, "Password must contain at least one uppercase letter"
|
|
|
|
if self.require_lowercase and not any(c.islower() for c in password):
|
|
return False, "Password must contain at least one lowercase letter"
|
|
|
|
if self.require_digit and not any(c.isdigit() for c in password):
|
|
return False, "Password must contain at least one digit"
|
|
|
|
if self.require_special:
|
|
special_chars = "!@#$%^&*()_+-=[]{}|;:',.<>/?"
|
|
if not any(c in special_chars for c in password):
|
|
return False, "Password must contain at least one special character"
|
|
|
|
return True, None
|
|
|
|
|
|
# Default instances
|
|
password_hasher = PasswordHasher(rounds=12)
|
|
password_validator = PasswordValidator(
|
|
min_length=8,
|
|
require_uppercase=True,
|
|
require_lowercase=True,
|
|
require_digit=True,
|
|
require_special=False,
|
|
)
|