Как правильно хранить пароли: bcrypt, scrypt и Argon2
Практика4 минРедакция Innovialabs
Коротко
Пароли хранят только как медленный хеш с солью — Argon2id, scrypt или bcrypt, — а не как MD5 или SHA. Старые хеши переводят на новый алгоритм постепенно, при входе пользователя.
Короткий ответ
Пароль никогда не хранят в открытом виде и не шифруют. Его прогоняют через специальную функцию хеширования паролей — Argon2id, scrypt или bcrypt — и хранят результат. При входе пароль хешируют заново и сравнивают.
Для нового проекта выбирайте Argon2id. Если в вашем стеке его нет, подойдут scrypt или bcrypt. MD5, SHA-1 и даже SHA-256 «как есть» для паролей не годятся.
Почему MD5 и SHA не работают
MD5 и SHA создавались для проверки целостности данных, и главное их свойство — скорость. Для паролей это минус: если база утекла, злоумышленник перебирает варианты на видеокартах с огромной скоростью.
Что идёт не так с «простым» хешем:
- Одинаковые пароли дают одинаковые хеши. Видно, у кого пароль совпадает, и один подобранный пароль раскрывает всех.
- Радужные таблицы. Для популярных паролей хеши давно посчитаны заранее, подбор сводится к поиску в таблице.
- Быстрый перебор. Словари и правила мутаций («Password1!», «qwerty2024») проверяются за минуты.
Двойной хеш (md5(sha1(p))) или «секретная» соль в коде проблему не решают — скорость остаётся прежней.
Что делают соль и work factor
Соль — случайная строка, уникальная для каждого пароля. Она хранится рядом с хешем и не является секретом. Соль делает хеши одинаковых паролей разными и обесценивает радужные таблицы.
Work factor (стоимость) — параметр, который делает вычисление хеша намеренно медленным. Для пользователя разница незаметна, а для атакующего каждая попытка становится дорогой.
Современные библиотеки генерируют соль сами и записывают её вместе с параметрами прямо в строку хеша, например $argon2id$v=19$m=19456,t=2,p=1$.... Отдельная колонка под соль не нужна.
Сравнение алгоритмов
| Алгоритм | Что настраивается | Особенности |
|---|---|---|
| Argon2id | память, итерации, параллелизм | Устойчив к перебору на GPU за счёт расхода памяти. Выбор по умолчанию |
| scrypt | N (память и время), r, p | Тоже требует много памяти, хорошо поддерживается |
| bcrypt | cost (логарифм числа раундов) | Проверен временем, есть почти везде. Учитывает только первые 72 байта пароля |
| PBKDF2 | число итераций | Нужен, если требуется соответствие FIPS. Нагружает только CPU |
Как выбрать параметры
- Начните с рекомендаций OWASP Password Storage Cheat Sheet — там указаны актуальные минимальные значения для каждого алгоритма.
- Измерьте время хеширования на своём продакшен-сервере. Ориентир — доли секунды на одну операцию, чтобы вход оставался быстрым, а сервер выдерживал пиковую нагрузку.
- Учитывайте память: Argon2 с большим параметром памяти при множестве одновременных входов может исчерпать RAM.
- Пересматривайте параметры при смене железа и повышайте их со временем.
Пример на Python с библиотекой argon2-cffi:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher() # безопасные параметры по умолчанию
stored = ph.hash("user-password")
try:
ph.verify(stored, "user-password")
if ph.check_needs_rehash(stored):
stored = ph.hash("user-password") # параметры устарели — пересохраняем
except VerifyMismatchError:
pass # неверный пароль
В Node.js с bcrypt: await bcrypt.hash(password, 12) и await bcrypt.compare(password, hash). В PHP — password_hash(), password_verify() и password_needs_rehash().
Миграция старых хешей при входе
Пароли пользователей вы не знаете, поэтому пересчитать всю базу сразу нельзя. Рабочая схема:
- Добавьте поле с алгоритмом хеша (или определяйте его по префиксу строки).
- При входе проверьте пароль старым способом.
- Если пароль верный — сразу посчитайте хеш Argon2id и перезапишите запись.
- Пользователей, которые долго не входят, можно защитить сразу: обернуть старый хеш в новый (
argon2(md5(password))), а при следующем входе заменить на чистый Argon2id. - Через разумный срок оставшиеся старые хеши удалите и попросите таких пользователей сбросить пароль.
Частые ошибки
- Сравнение хешей обычным
==вместо функции библиотеки (возможна атака по времени). - Слишком низкий cost «потому что тормозит» — лучше ограничить частоту попыток входа.
- Логирование паролей в запросах и ошибках.
- Обрезание пароля или запрет длинных паролей и спецсимволов.
- Отсутствие rate limiting и 2FA — даже идеальный хеш не спасает от подбора через форму входа.