feat(security): T13 implement bcrypt password hashing

- Add password hashing with bcrypt (12 rounds)
- Implement verify_password with timing-safe comparison
- Add validate_password_strength with comprehensive rules
  - Min 12 chars, uppercase, lowercase, digit, special char
- 19 comprehensive tests with 100% coverage
- Handle TypeError for non-string inputs
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 12:06:38 +02:00
parent 2fdd9d16fd
commit 54e81162df
3 changed files with 376 additions and 3 deletions

View File

@@ -0,0 +1,99 @@
"""Password hashing and validation service.
This module provides secure password hashing using bcrypt
and password strength validation.
"""
import re
from passlib.context import CryptContext
# CryptContext with bcrypt scheme
# bcrypt default rounds is 12 which is secure
pwd_context = CryptContext(
schemes=["bcrypt"],
deprecated="auto",
bcrypt__rounds=12, # Explicit for clarity
)
def hash_password(password: str) -> str:
"""Hash a password using bcrypt.
Args:
password: The plaintext password to hash.
Returns:
The bcrypt hashed password.
Raises:
TypeError: If password is not a string.
"""
if not isinstance(password, str):
raise TypeError("password must be a string")
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a plaintext password against a hashed password.
Args:
plain_password: The plaintext password to verify.
hashed_password: The bcrypt hashed password to verify against.
Returns:
True if the password matches, False otherwise.
Raises:
TypeError: If either argument is not a string.
"""
if not isinstance(plain_password, str):
raise TypeError("plain_password must be a string")
if not isinstance(hashed_password, str):
raise TypeError("hashed_password must be a string")
return pwd_context.verify(plain_password, hashed_password)
def validate_password_strength(password: str) -> bool:
"""Validate password strength.
Password must meet the following criteria:
- At least 12 characters long
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one special character (!@#$%^&*()_+-=[]{}|;':\",./<>?)
Args:
password: The password to validate.
Returns:
True if password meets all criteria, False otherwise.
"""
if not isinstance(password, str):
return False
# Minimum length: 12 characters
if len(password) < 12:
return False
# At least one uppercase letter
if not re.search(r"[A-Z]", password):
return False
# At least one lowercase letter
if not re.search(r"[a-z]", password):
return False
# At least one digit
if not re.search(r"\d", password):
return False
# At least one special character
if not re.search(r"[!@#$%^&*()_+\-=\[\]{}|;':\",./<>?]", password):
return False
return True