Хранение паролей (bcrypt, Argon2)
| Задача | хранить проверочное значение, по которому нельзя быстро восстановить пароль |
|---|---|
| Рекомендуется | Argon2id (RFC 9106) |
| Альтернативы | scrypt, bcrypt, PBKDF2 (для FIPS) |
| Обязательно | уникальная соль на каждый пароль |
| Нельзя | открытый текст, обратимое шифрование, MD5/SHA-1/SHA-256 без растяжения |
| Стандарты | NIST SP 800-63B, OWASP Password Storage Cheat Sheet |
Хранение паролей — набор приёмов, с помощью которых сервер запоминает пароли пользователей так, чтобы проверять их при входе, но не иметь возможности (и не давать её взломщику) восстановить исходный пароль. Современная практика — хранить результат медленной функции хеширования паролей с уникальной солью: Argon2id, scrypt, bcrypt или PBKDF2.[1] Это часть аутентификации, и одна из самых частых ошибок в ней.
Почему не просто хеш
Базы пользователей утекают регулярно. Если пароли лежат открытым текстом или зашифрованы обратимо (ключ обычно лежит рядом), утечка сразу раскрывает их все — а люди повторяют пароли на разных сайтах.
Обычная криптографическая хеш-функция (SHA-256, тем более MD5 или SHA-1) тоже не годится. Она спроектирована быть быстрой: современная видеокарта перебирает миллиарды SHA-256 в секунду, поэтому словарь из популярных паролей и их вариаций проверяется за минуты. Кроме того, одинаковые пароли дают одинаковые хеши, и заранее посчитанные таблицы (радужные таблицы) работают против всех пользователей сразу.
Поэтому функция для паролей должна обладать тремя свойствами:
- Соль — случайная строка (16+ байт), уникальная для каждого пароля и хранимая рядом с хешем. Она делает одинаковые пароли разными и обесценивает заранее посчитанные таблицы.
- Растяжение (work factor) — настраиваемая стоимость вычисления: проверка одного пароля занимает десятки–сотни миллисекунд вместо наносекунд.
- Затраты памяти (memory-hardness) — функция требует много оперативной памяти, что мешает массовому параллельному перебору на GPU и ASIC.
Алгоритмы
| Алгоритм | Год | Память | Комментарий |
|---|---|---|---|
| PBKDF2 | 2000 (RFC 2898) | нет | много итераций HMAC; нужен там, где требуется FIPS-140 |
| bcrypt | 1999[4] | 4 КБ | основан на шифре Blowfish, параметр cost; пароль обрезается до 72 байт |
| scrypt | 2009 (RFC 7914)[5] | да | первая популярная memory-hard функция |
| Argon2id | 2015 (RFC 9106)[2] | да | победитель Password Hashing Competition; рекомендация по умолчанию |
OWASP рекомендует Argon2id с параметрами не ниже 19 МиБ памяти, 2 итерации, 1 поток; при его отсутствии — scrypt, затем bcrypt с cost ≥ 10, а PBKDF2-HMAC-SHA256 — с 600 000 итераций.[1] Параметры подбирают так, чтобы проверка занимала заметное, но терпимое время на своём железе, и периодически повышают.
Результат обычно хранится в одной строке в формате, который содержит алгоритм, параметры и соль:
$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
Благодаря этому параметры можно менять со временем: при следующем успешном входе пароль перехешируется с новыми настройками.
Пример на Python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher() # параметры по умолчанию — разумные
stored = ph.hash("correct horse battery staple")
def check(password: str, stored: str) -> bool:
try:
ph.verify(stored, password)
except VerifyMismatchError:
return False
if ph.check_needs_rehash(stored):
... # сохранить ph.hash(password) вместо старого значения
return True
Библиотека argon2-cffi сама генерирует соль и сравнивает значения за постоянное время.[6] В других языках есть аналогичные готовые функции (password_hash в PHP, golang.org/x/crypto/argon2 и bcrypt в Go, passlib/bcrypt в Python); писать свою схему не нужно.
Перец и прочие детали
- Перец (pepper) — дополнительный секрет, общий для всех паролей и хранящийся отдельно от базы (в хранилище секретов или HSM). Утечка одной базы без перца не позволяет даже начать перебор.
- Сравнивать хеши нужно функцией постоянного времени, чтобы не создавать утечку по времени ответа.
- Медленное хеширование делает форму входа удобной целью для перегрузки: нужны ограничения частоты запросов.
- Ограничение bcrypt в 72 байта иногда обходят предварительным хешированием пароля, но это надо делать аккуратно (в base64), иначе нулевые байты обрежут вход.
Политика паролей
NIST SP 800-63B (редакция 4) пересмотрел привычные правила:[3] требовать длину не менее 8 символов (лучше 15 при однофакторном входе), разрешать длинные пароли (64+ символа) и любые символы Unicode, не требовать обязательной смены по расписанию и «состава» (заглавная, цифра, спецсимвол), зато проверять пароль по спискам утёкших и популярных. Лучшая защита от последствий утечек — Двухфакторная аутентификация и passkeys, при которых сервер вообще не хранит общий секрет.
См. также
Источники
- OWASP — Password Storage Cheat Sheet
- RFC 9106 — Argon2 Memory-Hard Function for Password Hashing
- NIST SP 800-63B — Digital Identity Guidelines: Authentication
- Provos, Mazières — A Future-Adaptable Password Scheme (bcrypt, USENIX 1999)
- RFC 7914 — The scrypt Password-Based Key Derivation Function
- Python — argon2-cffi documentation