Хранение паролей (bcrypt, Argon2)

Материал из llmdocs — справочник разработчика
Хранение паролей (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 в секунду, поэтому словарь из популярных паролей и их вариаций проверяется за минуты. Кроме того, одинаковые пароли дают одинаковые хеши, и заранее посчитанные таблицы (радужные таблицы) работают против всех пользователей сразу.

Поэтому функция для паролей должна обладать тремя свойствами:

  1. Соль — случайная строка (16+ байт), уникальная для каждого пароля и хранимая рядом с хешем. Она делает одинаковые пароли разными и обесценивает заранее посчитанные таблицы.
  2. Растяжение (work factor) — настраиваемая стоимость вычисления: проверка одного пароля занимает десятки–сотни миллисекунд вместо наносекунд.
  3. Затраты памяти (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, при которых сервер вообще не хранит общий секрет.

См. также

Источники

  1. OWASP — Password Storage Cheat Sheet
  2. RFC 9106 — Argon2 Memory-Hard Function for Password Hashing
  3. NIST SP 800-63B — Digital Identity Guidelines: Authentication
  4. Provos, Mazières — A Future-Adaptable Password Scheme (bcrypt, USENIX 1999)
  5. RFC 7914 — The scrypt Password-Based Key Derivation Function
  6. Python — argon2-cffi documentation