Хеширование паролей в Python: что знать до первой строки кода
Как хранить пароли в Python-приложении, чтобы утечка базы не превратилась в утечку паролей. Разбираем bcrypt, Argon2 и почему hashlib с солью — не то, что вам нужно.

Вы пишете регистрацию пользователей и дошли до строчки, где нужно сохранить пароль в базу. Через полчаса у вас будет рабочая функция: пароль превращается в строку, которую нельзя расшифровать обратно, но с которой можно проверить вход. Понадобится Python 3.8+ и одна из библиотек — argon2-cffi или bcrypt. На всё уйдёт минут двадцать, если не считать чтения этого текста.
Сразу главное: пароли не шифруют, их хешируют. Разница принципиальна. Шифрование обратимо — есть ключ, есть способ вернуть исходный текст. Хеш необратим: из результата нельзя восстановить пароль, можно только взять кандидата, прогнать через ту же функцию и сравнить. Именно это вам и нужно: чтобы даже вы сами не могли прочитать чужой пароль.
Что нужно иметь до начала
- Python 3.8 или новее (примеры проверены на 3.11 и 3.12).
- Установленная библиотека. Для Argon2:
pip install argon2-cffi. Для bcrypt:pip install bcrypt. - Поле в базе под хеш — строка длиной хотя бы 128 символов. Не
VARCHAR(32), туда хеш не влезет. - Понимание, что
hashlibиз стандартной библиотеки для этой задачи не подходит. Почему — ниже.
Почему hashlib с солью — ловушка
Первое, что находит новичок: hashlib.sha256(password.encode()).hexdigest(). Выглядит правильно — необратимая функция, строка на выходе. Читая дальше, человек узнаёт про соль и добавляет её. И останавливается, считая задачу решённой.
Проблема в том, что SHA-256 создавали быстрым. Он должен хешировать гигабайты за секунды — это его работа. Для паролей скорость — уязвимость. Современная видеокарта перебирает миллиарды SHA-256 в секунду. Если база утекла, короткие и словарные пароли вскрываются перебором за минуты, даже с солью: соль защищает от радужных таблиц, но не замедляет перебор конкретного хеша.
Функции для паролей — bcrypt, scrypt, Argon2 — устроены наоборот: они намеренно медленные и требуют памяти. Одна проверка занимает десятки миллисекунд, что незаметно при логине, но делает массовый перебор дорогим.
Правило простое: если библиотека называется «хеш общего назначения» — она не для паролей. Если в её описании есть слова «work factor», «cost» или «memory cost» — она для паролей.
Три рабочих варианта
Argon2 — выбор по умолчанию
Argon2 победил конкурс Password Hashing Competition в 2015 году и сейчас считается рекомендуемым для новых проектов. Библиотека argon2-cffi сама генерирует соль, сама встраивает параметры в итоговую строку и сама проверяет.
# argon2-cffi 23.x, Python 3.11
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
hasher = PasswordHasher() # параметры по умолчанию разумны для веба
# при регистрации: получаем хеш для хранения в базе
hash_to_store = hasher.hash("пароль-пользователя")
# hash_to_store — строка вида $argon2id$v=19$m=65536,t=3,p=4$...
# при входе: сверяем введённый пароль с хешем из базы
try:
hasher.verify(hash_to_store, "пароль-пользователя")
print("пароль верный")
except VerifyMismatchError:
print("пароль неверный")
Соль хранить отдельно не нужно — она уже внутри строки hash_to_store. Это частый вопрос новичков: «а где хранить соль?» Ответ — нигде, она в самом хеше.
bcrypt — проверенный ветеран
bcrypt старше, но до сих пор надёжен и повсеместно поддержан. Стоит выбрать его, если проект должен работать там, где Argon2 недоступен, или если так решил ваш фреймворк.
# bcrypt 4.x, Python 3.11
import bcrypt
password_bytes = "пароль-пользователя".encode("utf-8")
# регистрация: gensalt() сам создаёт соль, cost по умолчанию 12
hash_to_store = bcrypt.hashpw(password_bytes, bcrypt.gensalt())
# вход: сравниваем введённый пароль с хешем из базы
if bcrypt.checkpw(password_bytes, hash_to_store):
print("пароль верный")
else:
print("пароль неверный")
У bcrypt есть особенность: он игнорирует всё после 72 байт пароля. Для латиницы это 72 символа, для кириллицы в UTF-8 — примерно 36. На практике мешает редко, но знать об этом нужно, чтобы не удивляться, почему очень длинные парольные фразы «совпадают» с обрезанными.
Не изобретать: взять то, что даёт фреймворк
Если вы на Django, не пишите хеширование руками. В Django 5.x есть django.contrib.auth.hashers.make_password() и check_password(), а стандартная модель пользователя хеширует пароль сама. По умолчанию используется PBKDF2, но набор алгоритмов настраивается через PASSWORD_HASHERS в настройках. Точный список алгоритмов и порядок сверяйте в документации вашей версии.
Сравнение вариантов
| Критерий | Argon2 (argon2-cffi) | bcrypt | hashlib + соль |
|---|---|---|---|
| Пригоден для паролей | Да, рекомендуется | Да | Нет |
| Соль хранится отдельно | Нет, внутри хеша | Нет, внутри хеша | Да, вручную |
| Ограничение длины | Нет практического | 72 байта | Нет |
| Настраивается стойкость | Память + время + потоки | Cost factor | Нет защиты от перебора |
| Когда брать | Новый проект | Совместимость, старый стек | Никогда для паролей |
Если не получилось
ModuleNotFoundError: No module named 'argon2'— пакет называетсяargon2-cffi, но импортируется какargon2. Проверьте, что установили именноargon2-cffi, а не пакетargon2(это другой, устаревший).TypeError: Unicode-objects must be encoded before hashingв bcrypt — вы передали строку вместо байтов. bcrypt работает сbytes, вызовите.encode("utf-8").- Хеш обрезается в базе — поле слишком короткое. Строка Argon2 занимает около 97 символов, bcrypt — 60. Ставьте с запасом,
VARCHAR(255)илиTEXT. - Проверка всегда возвращает «неверно» при правильном пароле — частая причина в кодировке: при хешировании байты получены одним способом, при проверке другим. Используйте
utf-8в обоих местах. - Логин заметно тормозит — вы выставили слишком высокий cost или memory cost. Одна проверка должна занимать около 50–250 мс. Замерьте
timeitи подберите параметры под своё железо.
Что делать дальше
Рабочее хеширование — это только фундамент. После него посмотрите на четыре вещи:
- Перехеширование. И Argon2, и bcrypt умеют подсказать, что старый хеш создан со слабыми параметрами. У Argon2 это
hasher.check_needs_rehash()— вызывайте после успешного входа и, если нужно, пересчитывайте хеш, пока у вас есть открытый пароль. Так база плавно переезжает на более стойкие параметры без сброса паролей. - Ограничение попыток входа. Медленный хеш не спасёт, если атакующий может слать тысячи запросов к вашей форме логина. Rate limiting — отдельная и обязательная защита.
- Параметры под нагрузку. Дефолтные значения Argon2 требуют памяти на каждую проверку. Под высокой параллельностью логинов это упирается в RAM сервера — тюнинг параметров под конкретное железо и нагрузку это отдельная тема, здесь мы её не разворачиваем.
- Никогда не логируйте пароли. Проверьте, что открытый пароль не попадает в логи, трейсбеки и системы мониторинга. Это частый источник утечек, не связанный с хешированием вовсе.
Начните с Argon2 через argon2-cffi, если проект новый и у вас нет ограничений стека. Это самый безопасный выбор по умолчанию на сегодня.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: argon2-cffi — официальная документация, OWASP Password Storage Cheat Sheet
Частые вопросы
У меня Django, нужна ли отдельная библиотека?
А если у меня старая версия библиотеки и код не работает?
Где хранить соль отдельно от хеша?
Можно ли просто взять SHA-256 с солью, если пароли сложные?
Пользователь забыл пароль — как его восстановить из хеша?
Насколько медленной должна быть проверка пароля?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.