Как хранить пароли: хеш — это не шифрование
Чем хеширование отличается от шифрования, почему SHA-256 для паролей не годится, зачем нужна соль и какие алгоритмы и параметры рекомендует OWASP в 2026 году.
Коротко. Пароль не шифруют — его хешируют функцией, которая намеренно медленная. Главный параметр здесь не название алгоритма, а стоимость одного вычисления: она и есть защита.
Разговор почти всегда начинается с одной фразы: «пароли у нас
в базе зашифрованы». Дальше выясняется, что лежит sha256 от пароля,
а иногда и вовсе md5. Путаница между шифрованием и хешированием —
не терминологическая придирка, из неё растут все остальные ошибки.
Разница, из которой всё следует
Шифрование обратимо. У него есть ключ, и по ключу текст восстанавливается. Значит, где-то должен лежать ключ — и тот, кто добрался до базы, чаще всего доберётся и до него. Про то, почему переменная окружения секрет не прячет, есть отдельный разбор.
Хеш необратим. Из него нельзя восстановить исходную строку, потому что восстанавливать нечего: функция односторонняя. Проверка входа работает иначе — берём присланный пароль, считаем хеш заново и сравниваем с сохранённым.
Пароль хранить незачем. Нужна только возможность сказать «да, это он».
Почему SHA-256 здесь плохой выбор
Тот же аргумент, что делает SHA-256 хорошей функцией для контрольных сумм, делает её негодной для паролей: она быстрая. Считать её умеет видеокарта, и умеет миллиардами в секунду.
Нападающий, унёсший базу, не разгадывает хеш. Он перебирает пароли: берёт список из утечек, считает хеш каждого и сравнивает. Вся защита сводится к тому, сколько стоит одно вычисление. У SHA-256 оно стоит ничтожно мало, и база на миллион строк разбирается на ноутбуке.
Поэтому для паролей берут функции, которые намеренно медленные и настраиваемо медленные. Это не побочный эффект, а их назначение.
Соль
Соль — случайная строка, своя у каждого пользователя, которая добавляется к паролю перед хешированием и хранится рядом с хешем открыто. Её секретность и не подразумевается.
Она закрывает две вещи. Во-первых, заранее посчитанные таблицы:
таблица под соль x7q… бесполезна для соли k2m…, а считать таблицу
под каждого пользователя — то же самое, что перебирать напрямую.
Во-вторых, одинаковые пароли перестают давать одинаковые хеши, и по базе
больше не видно, что три сотни человек выбрали одно и то же.
Современные библиотеки генерируют соль сами и кладут её внутрь строки хеша вместе с параметрами. Отдельное поле в таблице заводить не нужно, и самому её придумывать — тоже.
Что брать в 2026 году
Памятка OWASP по хранению паролей ставит первым Argon2id — минимум 19 МиБ памяти, две итерации, одна степень параллелизма. Есть и вариант с меньшим расходом процессора: 46 МиБ, одна итерация.
Дальше по списку scrypt, затем bcrypt, и PBKDF2 — когда требуется сертификация по FIPS. Для bcrypt памятка называет фактор стоимости от 10 и выше.
Отличие Argon2id и scrypt от bcrypt существенное: они требовательны не только к процессору, но и к памяти. Специализированное железо, на котором перебор дёшев, упирается именно в память — её на кристалле много не разместишь.
Ограничение bcrypt, о котором забывают: он принимает 72 байта. Всё, что длиннее, молча отбрасывается. Парольная фраза из десяти слов превращается в свои первые 72 байта, и пользователь об этом не узнает. Тихое усечение — худший вид дефекта: он не проявляется до разбора утечки.
Как уходить со старого алгоритма
Пересчитать хеши разом нельзя: исходных паролей у вас нет, в том и смысл. Перевод идёт по одному человеку, в момент входа.
Пользователь прислал пароль, вы проверили его старым способом и он подошёл — прямо здесь считаете новый хеш и переписываете строку. В записи хранится признак алгоритма, по нему и выбирается проверка. Через несколько месяцев на старом алгоритме остаются те, кто не заходил; их записи гасят принудительно, через сброс.
Схема при этом какое-то время держит оба формата, и это тот же приём, что и в миграциях без простоя: расширить — переехать — сузить.
Что делать, когда база всё-таки утекла
Стоимость перебора покупает не безопасность, а время. Правильно посчитанный Argon2id превращает разбор базы из ночной работы в месяцы на арендованном железе — и эти месяцы существуют для того, чтобы ими воспользоваться.
Порядок действий короткий. Признать утечку и назвать дату. Обнулить все активные сессии — хеши устоят, а вот украденный идентификатор сессии работает сразу и никакой стойкости не требует. Заставить сменить пароль при следующем входе, а не письмом с просьбой. И заново посчитать фактор стоимости: раз база ушла, значит, у нападающего теперь есть время, которого не было.
Что здесь ломается чаще всего — не хеши, а именно сессии. Про них вспоминают последними, хотя срабатывают они первыми.
Чего не делать
Не ограничивать длину и состав пароля сверху. Требование «не более 16 символов, обязательно цифра и знак» выдаёт, что пароль где-то хранится как есть или лежит в поле фиксированной ширины. Хеш всегда одной длины, какой бы ни была строка на входе.
Не досаливать секретом из кода. Идея добавить к соли общий секрет выглядит разумной ровно до вопроса, где его держать и как менять. Если очень нужно — это отдельный уровень, и решается он не строкой в конфиге, а хранилищем ключей.
Не писать своё. Здесь нечего улучшать, а цена ошибки видна только после утечки.
Условие, при котором всё это неважно
Если вы вообще не храните пароли — пускаете людей через внешнего поставщика входа — весь разбор мимо. Это не поражение, а нормальный выбор: чужая команда занимается этим полный рабочий день, а вы нет.
Оговорка ровно одна: вход через поставщика переносит риск, а не убирает его. Ваша база перестаёт быть добычей, но доступ к почте пользователя теперь открывает и ваш продукт тоже.