Прямой ответ
Пароли нельзя хранить ни в открытом виде, ни в виде обычного хеша. Обычные хеш-функции (MD5, SHA-1, SHA-256) созданы для скорости: современная видеокарта считает миллиарды значений в секунду, поэтому база хешей перебирается по словарю за минуты. Для паролей нужны медленные адаптивные функции с солью и настраиваемой стоимостью.
Актуальные варианты:
- Argon2id — победитель Password Hashing Competition, рекомендованный OWASP. Устойчив и к GPU, и к атакам с использованием памяти (memory-hard).
- bcrypt — проверенная временем схема на основе Blowfish; широко поддержана, но ограничена 72 байтами пароля и менее устойчива к аппаратному ускорению.
- scrypt — memory-hard функция, доступна в OpenSSL и библиотеках; рабочий вариант, если Argon2 недоступен.
- PBKDF2 — доступен везде, но требует очень большого числа итераций, чтобы приблизиться к устойчивости memory-hard схем.
Недопустимо: MD5, SHA-1, SHA-256 без растягивания, «двойной MD5», самописные схемы.
Обязательные свойства:
- Соль уникальна для каждого пароля. Общая соль позволяет атаковать всю базу одним словарём. Соль генерируется криптографически случайной и хранится вместе с хешем.
- Параметры стоимости задаются явно. Для Argon2id это объём памяти, число итераций и степень параллелизма; для bcrypt — cost factor.
- Формат хранения самодокументируемый. Строка вида
$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>содержит алгоритм и параметры, поэтому проверка не зависит от настроек по умолчанию в коде. - Сравнение без утечки по времени. Используются функции постоянного времени, а не
==.
Ориентиры по параметрам (OWASP): для Argon2id — 19 МБ памяти, t=2, p=1 как минимум; при возможности 46 МБ, t=1, p=1. Точные значения подбирают так, чтобы одна проверка занимала порядка 100–500 мс на вашем железе.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
stored = ph.hash("correct horse battery staple") # сохраняем в БД
try:
ph.verify(stored, user_input)
if ph.check_needs_rehash(stored):
stored = ph.hash(user_input) # обновляем параметры при входе
except VerifyMismatchError:
raise PermissionError("неверный пароль")
Практика: настройка и миграция
Подбор параметров под своё железо. Измеряйте время одной проверки: слишком быстро — снизится стоимость перебора для атакующего, слишком медленно — пользователи будут ждать, а сервер упрётся в CPU при всплеске входов. Практика: цель 200–300 мс и ограничение числа одновременных проверок.
Миграция со старой схемы без сброса паролей. Если в базе MD5, нельзя «перехешировать» пароли: исходный пароль неизвестен. Рабочая схема:
- Оборачиваем старый хеш: при входе вычисляем
argon2id(старый_md5_хеш + соль)и сохраняем в новом поле, помечая запись как мигрированную. - При следующем успешном входе получаем исходный пароль и заменяем обёртку на полноценный
argon2id(пароль). - Через разумный срок (6–12 месяцев) неактивные записи принудительно сбрасываем.
ALTER TABLE users
ADD COLUMN password_hash_v2 TEXT,
ADD COLUMN password_scheme TEXT NOT NULL DEFAULT 'md5_legacy';
Рекомендации:
- Не ограничивайте длину пароля сверху без причины. Ограничение в 32 символа — наследие старых схем; для argon2 достаточно защититься от DoS лимитом в несколько килобайт.
- Проверяйте пароль по спискам скомпрометированных. Словарная проверка при регистрации ловит больше слабых паролей, чем правила «обязательна цифра и спецсимвол».
- Ограничивайте число попыток входа. Без этого стоимость перебора для атакующего определяется не хешем, а скоростью отправки запросов.
- Хеширование — не шифрование. Если данные должны быть обратимы, это другая задача — см. шифрование данных в покое.
Практический сценарий: утечка базы и разные последствия
Контекст: два сервиса потеряли дамп таблицы пользователей. В одном пароли хранились как SHA-256 с общей солью, в другом — argon2id с индивидуальной солью.
- Первый сервис. Дамп расшифрован по радужным таблицам и словарю за несколько часов: 68 % паролей совпали с распространёнными, часть оказалась одинаковой у разных пользователей — общая соль это не скрывала.
- Второй сервис. Подбор одного пароля занимает сотни миллисекунд и требует значительной памяти; массовый перебор непрактичен. Пострадали только самые слабые пароли, попавшие в публичные списки.
- Действия. Первый сервис принудительно сбросил пароли и уведомил пользователей; второй ограничился уведомлением и рекомендацией сменить пароль, добавив проверку по списку скомпрометированных.
- Проверка. В обоих случаях проверили, не использовались ли пароли повторно в других системах (credential stuffing): добавили ограничение попыток и алерт на всплеск неудачных входов.
- Профилактика. Тест в CI проверяет, что схема хеширования и параметры соответствуют политике; отклонение ломает сборку.
Вывод: выбор функции хеширования определяет стоимость атаки после утечки. Это одна из немногих настроек безопасности, которую невозможно исправить задним числом без принудительного сброса паролей.
FAQ
Почему нельзя использовать SHA-256 с большим числом итераций? Можно, это PBKDF2-HMAC-SHA256, и он лучше «чистого» SHA-256. Но он не является memory-hard, поэтому ASIC и GPU дают атакующему большое преимущество. Argon2id и scrypt требуют значительной памяти на каждую проверку, что уравнивает шансы.
Что делать с bcrypt и паролями длиннее 72 байт? Bcrypt молча игнорирует всё после 72-го байта. Если длинные пароли допустимы, либо выбирают Argon2, либо предварительно хешируют вход SHA-256 с последующим bcrypt — но это добавляет нюансов, поэтому проще взять Argon2id.
Нужна ли «перечница» (pepper) поверх соли? Pepper — секрет, хранящийся вне базы (в KMS или переменных окружения). Он повышает стоимость атаки при утечке только базы. Это разумная дополнительная мера, но она не заменяет корректную функцию хеширования и усложняет ротацию секрета.
Как часто менять параметры хеширования?
По мере роста мощности железа — раз в несколько лет. Механизм check_needs_rehash позволяет обновлять хеши при входе пользователей без принудительного сброса; хранение параметров внутри строки хеша делает такую миграцию прозрачной.
Источники: OWASP Password Storage Cheat Sheet, RFC 9106 (Argon2), PostgreSQL pgcrypto — ссылки в шапке.