All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m14s
61 lines
2.3 KiB
Python
61 lines
2.3 KiB
Python
"""Bcrypt password hashing для DB-auth (#2550 — foundation, эпик #2549).
|
||
|
||
bcrypt тихо обрезает пароли длиннее 72 байт (UTF-8) — это silent-truncation
|
||
дыра (два разных пароля с общим 72-байтовым префиксом хешируются одинаково).
|
||
`hash_password` явно ловит это и падает с ValueError вместо тихого поведения.
|
||
`verify_password` на длинном пароле возвращает False (не raise) — сравнение
|
||
паролей не должно ронять запрос авторизации.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
import bcrypt
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_BCRYPT_MAX_BYTES = 72
|
||
_BCRYPT_ROUNDS = 12
|
||
|
||
|
||
def hash_password(plain: str) -> str:
|
||
"""Хеширует пароль через bcrypt (rounds=12).
|
||
|
||
Raises:
|
||
ValueError: пустой пароль или пароль длиннее 72 байт в UTF-8
|
||
(bcrypt тихо обрезает — недопустимо, см. модульный docstring).
|
||
"""
|
||
if not plain:
|
||
raise ValueError("password must not be empty")
|
||
|
||
encoded = plain.encode("utf-8")
|
||
if len(encoded) > _BCRYPT_MAX_BYTES:
|
||
raise ValueError(
|
||
f"password too long: {len(encoded)} bytes (bcrypt max {_BCRYPT_MAX_BYTES})"
|
||
)
|
||
|
||
salt = bcrypt.gensalt(rounds=_BCRYPT_ROUNDS)
|
||
hashed = bcrypt.hashpw(encoded, salt)
|
||
return hashed.decode("utf-8")
|
||
|
||
|
||
def verify_password(plain: str, hashed: str) -> bool:
|
||
"""Сверяет пароль с bcrypt-хешем.
|
||
|
||
Пустой пароль или пароль длиннее 72 байт в UTF-8 → False (не raise —
|
||
verify — это false/true проверка на этапе логина, а не валидация ввода).
|
||
"""
|
||
if not plain or not hashed:
|
||
return False
|
||
|
||
encoded = plain.encode("utf-8")
|
||
if len(encoded) > _BCRYPT_MAX_BYTES:
|
||
return False
|
||
|
||
try:
|
||
return bcrypt.checkpw(encoded, hashed.encode("utf-8"))
|
||
except (ValueError, TypeError) as e:
|
||
# Malformed hash (напр. не-bcrypt строка в БД) — не должно ронять login.
|
||
logger.warning("verify_password: malformed hash rejected: %s", e)
|
||
return False
|