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:
99
src/openrouter_monitor/services/password.py
Normal file
99
src/openrouter_monitor/services/password.py
Normal 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
|
||||
Reference in New Issue
Block a user