cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
114 lines
3.7 KiB
Python
114 lines
3.7 KiB
Python
"""Password hashing and validation utilities."""
|
|
|
|
from typing import Literal, cast
|
|
|
|
from passlib.context import CryptContext
|
|
|
|
# Configure bcrypt context - set truncate_error=False to allow passwords >72 bytes
|
|
# without raising an error. They will be automatically truncated by bcrypt to 72 bytes.
|
|
pwd_context = CryptContext(
|
|
schemes=["bcrypt"],
|
|
deprecated="auto",
|
|
bcrypt__truncate_error=False,
|
|
)
|
|
|
|
|
|
def hash_password(password: str) -> str:
|
|
"""Hash a password using bcrypt.
|
|
|
|
bcrypt has a maximum password length of 72 bytes. Longer passwords
|
|
are automatically truncated to comply with this limit.
|
|
|
|
Args:
|
|
password: The plain text password to hash
|
|
|
|
Returns:
|
|
The hashed password
|
|
"""
|
|
# bcrypt has a 72 byte limit - encode and truncate if necessary
|
|
password_bytes = password.encode("utf-8")
|
|
if len(password_bytes) > 72:
|
|
# Truncate to 72 bytes and decode back, dropping incomplete UTF-8 sequences
|
|
password = password_bytes[:72].decode("utf-8", errors="ignore")
|
|
return cast(str, pwd_context.hash(password))
|
|
|
|
|
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
"""Verify a password against a hash.
|
|
|
|
bcrypt has a maximum password length of 72 bytes. Longer passwords
|
|
are automatically truncated to match hash_password behavior.
|
|
|
|
Args:
|
|
plain_password: The plain text password to verify
|
|
hashed_password: The hashed password to verify against
|
|
|
|
Returns:
|
|
True if the password matches the hash, False otherwise
|
|
"""
|
|
try:
|
|
# bcrypt has a 72 byte limit - encode and truncate if necessary to match hash_password
|
|
password_bytes = plain_password.encode("utf-8")
|
|
if len(password_bytes) > 72:
|
|
# Truncate to 72 bytes and decode back, dropping incomplete UTF-8 sequences
|
|
plain_password = password_bytes[:72].decode("utf-8", errors="ignore")
|
|
return cast(bool, pwd_context.verify(plain_password, hashed_password))
|
|
except Exception:
|
|
# Invalid hash format or other error - return False
|
|
return False
|
|
|
|
|
|
def validate_password_strength(password: str) -> tuple[bool, str]:
|
|
"""Validate password meets minimum security requirements.
|
|
|
|
Password requirements:
|
|
- At least 8 characters long
|
|
- Contains at least one uppercase letter
|
|
- Contains at least one lowercase letter
|
|
- Contains at least one digit
|
|
|
|
Args:
|
|
password: The password to validate
|
|
|
|
Returns:
|
|
A tuple of (is_valid, error_message). If valid, error_message is empty.
|
|
"""
|
|
if len(password) < 8:
|
|
return False, "Password must be at least 8 characters long"
|
|
|
|
has_upper = any(c.isupper() for c in password)
|
|
has_lower = any(c.islower() for c in password)
|
|
has_digit = any(c.isdigit() for c in password)
|
|
|
|
if not (has_upper and has_lower and has_digit):
|
|
return False, "Password must contain uppercase, lowercase, and numbers"
|
|
|
|
return True, ""
|
|
|
|
|
|
def get_password_strength(password: str) -> Literal["weak", "moderate", "strong"]:
|
|
"""Determine the strength of a password.
|
|
|
|
Strength levels:
|
|
- weak: less than 8 characters
|
|
- moderate: 8+ characters but missing at least one of uppercase, lowercase, or digit
|
|
- strong: 8+ characters with uppercase, lowercase, and digit
|
|
|
|
Args:
|
|
password: The password to evaluate
|
|
|
|
Returns:
|
|
One of "weak", "moderate", or "strong"
|
|
"""
|
|
if len(password) < 8:
|
|
return "weak"
|
|
|
|
has_upper = any(c.isupper() for c in password)
|
|
has_lower = any(c.islower() for c in password)
|
|
has_digit = any(c.isdigit() for c in password)
|
|
|
|
if not (has_upper and has_lower and has_digit):
|
|
return "moderate"
|
|
|
|
return "strong"
|