Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

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"