51 lines
1.5 KiB
Python
51 lines
1.5 KiB
Python
from argon2 import PasswordHasher
|
|||
|
|
from argon2.exceptions import InvalidHashError, VerifyMismatchError
|
||
|
|
from argon2.low_level import Type
|
||
|
|
from zxcvbn import zxcvbn
|
||
|
|
|
||
|
|
from app.config import settings
|
||
|
|
|
||
|
|
|
||
|
|
class WeakPasswordError(ValueError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
_hasher = PasswordHasher(
|
||
|
|
time_cost=3,
|
||
|
|
memory_cost=65536,
|
||
|
|
parallelism=4,
|
||
|
|
hash_len=32,
|
||
|
|
salt_len=16,
|
||
|
|
type=Type.ID,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _peppered(password: str) -> str:
|
||
|
|
return password + settings.password_pepper
|
||
|
|
|
||
|
|
|
||
|
|
# возвращает хэш Argon2id от password + pepper;
|
||
|
|
def hash_password(password: str) -> str:
|
||
|
|
return _hasher.hash(_peppered(password))
|
||
|
|
|
||
|
|
|
||
|
|
# проверяет соответствие
|
||
|
|
def verify_password(password: str, hashed: str) -> bool:
|
||
|
|
try:
|
||
|
|
return _hasher.verify(hashed, _peppered(password))
|
||
|
|
except (VerifyMismatchError, InvalidHashError):
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
# проверяет политику
|
||
|
|
def validate_policy(password: str, user_inputs: list[str] | None = None) -> None:
|
||
|
|
# длина не меньше settings.password_min_length;
|
||
|
|
if len(password) < settings.password_min_length:
|
||
|
|
raise WeakPasswordError(
|
||
|
|
f"Password must be >= {settings.password_min_length} chars"
|
||
|
|
)
|
||
|
|
result = zxcvbn(password, user_inputs=user_inputs or [])
|
||
|
|
# оценка стойкости через zxcvbn должна быть не ниже 3 (по шкале 0–4);
|
||
|
|
if result["score"] < 3:
|
||
|
|
raise WeakPasswordError("Password is too weak")
|