MAATRIX / Блог / Почему хеш пароля обязан быть медленным и чем это отличается от обычного хеша

Почему хеш пароля обязан быть медленным и чем это отличается от обычного хеша

MAATRIX

Если вы когда-нибудь сверяли контрольную сумму скачанного ISO-образа и потом настраивали хранение паролей пользователей в своём приложении, у вас наверняка возникал вопрос: почему бы просто не взять тот же SHA-256 и для паролей тоже? Функция одна и та же, зачем городить bcrypt, scrypt или Argon2. Разница не в криптографической стойкости самого алгоритма, а в том, для какой скорости работы он спроектирован — и это меняет всё.

Зачем вообще хешировать пароль

Идея хеширования пароля простая: сервер не должен хранить пароль в виде, из которого его можно восстановить. Вместо самого пароля в базе лежит результат работы хеш-функции — строка фиксированной длины, из которой практически невозможно вычислить исходные символы в обратную сторону. При входе пользователь присылает пароль, сервер хеширует его тем же алгоритмом и сравнивает результат с тем, что лежит в базе. Совпало — значит, пароль верный, и при этом сам пароль на сервере в открытом виде нигде не хранился и не передавался повторно.

Это защищает от одной конкретной угрозы: утечки базы данных. Если у вас украли таблицу пользователей, у вас украли хеши, а не пароли. Дальше всё зависит от того, насколько дорого атакующему превратить хеш обратно в пароль — и вот здесь начинается разница между «обычным» хешем и хешем, спроектированным именно для паролей.

Важно сразу разделить две разные функции, которые условно называют одним словом «хеш»: контрольная сумма для целостности данных и производная для хранения секрета. Задачи разные, требования к скорости — противоположные.

Обычный хеш создан, чтобы быть быстрым

MD5, SHA-1, SHA-256, SHA-3 — это криптографические хеш-функции общего назначения. Их придумывали для задач вроде: убедиться, что файл не повреждён при передаче; подписать коммит в git; построить структуру индекса; сравнить два больших объекта по их «отпечатку», не читая оба целиком. Во всех этих сценариях скорость — прямое достоинство. Чем быстрее вы можете хешировать гигабайты данных, тем быстрее проверяете бэкап, тем быстрее собираете дерево коммитов, тем меньше накладных расходов на каждый чек-сумм при заливке образа на сервер.

Поэтому такие функции проектировались и оптимизировались именно под скорость: минимум арифметических операций на байт входа, удобство для аппаратного ускорения, возможность обрабатывать поток данных почти на скорости диска или сети. SHA-256 на современном CPU с инструкциями SHA extensions хеширует гигабайты в секунду — это буквально его конкурентное преимущество как алгоритма. На сервере вы полагаетесь на эту скорость каждый день, когда rsync или borg сверяют контрольные суммы бэкапа, или когда TLS-рукопожатие проверяет отпечаток сертификата.

Проблема в том, что если взять эту же самую функцию и применить её напрямую к паролю — sha256(password) — вы даёте атакующему точно такое же преимущество, только против вас.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Почему скорость превращается в уязвимость

Представьте, что у вас украли базу с хешами паролей. Онлайн-перебор (когда атакующий стучится в форму логина) вы можете ограничить — блокировкой по IP, капчей, лимитом попыток. А офлайн-перебор ограничить нечем: у атакующего есть файл с хешами, и он подбирает пароли локально, на своём железе, без всякого контакта с вашим сервером.

Здесь и вступает в игру скорость хеш-функции. Если хеширование одного пароля занимает исчезающе малое время CPU, атакующий может перебрать огромное количество вариантов пароля в секунду — точный порядок величины зависит от железа, но он принципиально другой, чем при медленном алгоритме. Современный перебор идёт не на обычном процессоре, а на GPU или специализированных ASIC/FPGA-решениях, которые умеют вычислять быстрые хеш-функции параллельно тысячами потоков одновременно — минимум обращений к памяти и простая арифметика хорошо ложатся именно на такую архитектуру.

В результате словарная атака или брутфорс по хешам, полученным из быстрой функции, становится задачей, решаемой за разумное время на арендованной вычислительной мощности — тем более если пароли пользователей не идеально случайны, а построены по человеческим шаблонам. Соль (случайная добавка к паролю перед хешированием) решает другую проблему — не даёт атакующему один раз посчитать таблицу хешей для всех популярных паролей и применить её ко всей базе сразу (rainbow-таблицы). Но соль не делает вычисление одного конкретного хеша медленнее. От скорости перебора соль не спасает вообще — для этого нужен принципиально другой алгоритм.

Специальные алгоритмы для хеширования паролей

Именно поэтому существует отдельный класс алгоритмов — bcrypt, scrypt, PBKDF2, Argon2 (в вариантах Argon2i, Argon2d, Argon2id) — которые решают ровно обратную инженерную задачу: сделать вычисление одного хеша заведомо дорогим. Не «сломанным» или «неэффективным» — а именно спроектированным так, чтобы даже одна попытка стоила ощутимого времени CPU и, в случае scrypt и Argon2, ощутимого объёма памяти.

Два независимых рычага, которые используют эти алгоритмы:

  • Число итераций (time cost). Вместо одного прохода функции внутри выполняется тысячи или десятки тысяч последовательных раундов преобразования. Это напрямую умножает время на одну попытку.
  • Требование к памяти (memory cost). scrypt и Argon2 устроены так, что вычисление требует держать в памяти существенный объём промежуточных данных — и не позволяют дёшево обменять память на дополнительные вычисления. Это называют memory-hard функцией.

Второй пункт — ключевой ответ на угрозу параллельного перебора на GPU и ASIC. GPU отлично параллелит простую арифметику с маленьким объёмом рабочей памяти на поток — именно так устроены быстрые хеш-функции. А вот если каждому потоку вычисления нужен собственный крупный блок памяти, то видеокарта или специализированный чип быстро упираются не в вычислительную мощность, а в объём и пропускную способность памяти, которая на таком железе сильно ограниченнее, чем на обычном сервере с DDR-памятью. Атакующий не может просто добавить ядер — ему физически некуда положить рабочие данные тысяч параллельных попыток одновременно. Это не делает атаку невозможной в принципе, но резко меняет её экономику: стоимость перебора на специализированном железе перестаёт быть на порядки ниже, чем на обычном CPU.

Argon2id на сегодня — рекомендуемый по умолчанию выбор для новых систем (он же победитель профильного конкурса Password Hashing Competition), совмещающий устойчивость к атакам по времени (side-channel) и к GPU-перебору. bcrypt остаётся на практике самым распространённым вариантом из-за возраста, простоты и встроенной поддержки в большинстве экосистем — он ограничен по управлению памятью, но время итераций у него настраивается так же явно.

Cost factor: сложность, которая растёт вместе с железом

У всех этих алгоритмов есть параметр стоимости — обычно его называют cost factor, work factor или просто «раунды». Это не случайная деталь API, а центральная идея всей конструкции: стоимость вычисления одного хеша должна оставаться неудобно высокой для атакующего и приемлемо низкой для легитимного сервера, который делает ровно одну проверку за раз при логине пользователя.

У bcrypt это степень двойки (обычно параметр от 10 до 14 в современных системах) — при увеличении на единицу время вычисления удваивается. У scrypt — три параметра: N (сложность по CPU и памяти), r (размер блока) и p (степень параллелизма). У Argon2 — memory cost (сколько килобайт памяти использовать), time cost (число проходов) и parallelism (число потоков).

Смысл в том, что этот параметр не зафиксирован раз и навсегда. Вычислительная мощность железа со временем растёт, и то, что было «дорого» посчитать десять лет назад, сегодня становится дёшево. Поэтому cost factor нужно пересматривать в сторону увеличения по мере того, как растёт доступная атакующему вычислительная мощность — это встроенный механизм устаревания в обратную сторону: систему делают строже с течением времени, а не слабее.

Практический ориентир простой и не завязан на конкретные цифры производительности: выберите такой cost factor, чтобы вычисление одного хеша на вашем сервере занимало заметное, но терпимое для пользователя время при логине — условно от нескольких десятков до нескольких сотен миллисекунд, конкретное значение зависит от CPU вашего сервера и от того, сколько логинов в секунду вам нужно обслуживать одновременно. Замерьте это значение на своём железе — на другом сервере с другим процессором тот же параметр даст другое реальное время, поэтому переносить чужие «рекомендованные» цифры без проверки не стоит.

# Python, пример с bcrypt — cost factor задаётся явно
import bcrypt

password = b"correct horse battery staple"
# rounds=12 — разумная отправная точка, проверьте время на своём сервере
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))

# при проверке пароля соль и cost factor уже зашиты в самой строке hashed
bcrypt.checkpw(password, hashed)
// Node.js, пакет argon2 — рекомендуемый вариант для новых проектов
const argon2 = require('argon2');

const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 19456, // KiB, подбирается под доступную серверу память
  timeCost: 2,
  parallelism: 1,
});

const valid = await argon2.verify(hash, password);

Обратите внимание: в обоих примерах соль и параметры сложности хранятся прямо в самой строке хеша (например, $2b$12$... у bcrypt или $argon2id$v=19$m=19456,t=2,p=1$... у Argon2). Это удобно — не нужна отдельная колонка в базе для соли или cost factor, и при проверке пароля библиотека сама читает параметры из строки. В PHP та же идея реализована функциями password_hash() и password_verify(), а password_needs_rehash() позволяет поднимать cost со временем без принудительного сброса паролей у всех пользователей разом.

Где это уже работает у вас на сервере

Похожий принцип используется не только в веб-приложениях, но и в самой Linux-системе, когда вы создаёте пользователя или меняете пароль root на VPS. Пароли системных учётных записей хранятся в /etc/shadow в виде хешей, полученных через crypt(), а не в открытом виде. Современные дистрибутивы по умолчанию используют SHA-512-based crypt или yescrypt (в последних релизах Debian/Ubuntu) — оба варианта тоже поддерживают управляемое число раундов.

# посмотреть, какой алгоритм хеширования паролей используется по умолчанию
grep ENCRYPT_METHOD /etc/login.defs

# для sha512crypt можно явно задать диапазон раундов —
# чем выше значение, тем дороже перебор офлайн, если /etc/shadow утёк
grep SHA_CRYPT /etc/login.defs

Если вы арендуете VPS и настраиваете его с нуля, стоит один раз проверить эти параметры, а заодно свериться с тем, как устроен SSH-ключ изнутри — это соседний по духу вопрос: там вместо пароля используется асимметричная пара ключей, и офлайн-перебор просто неприменим как класс атаки, потому что не хешируется ничего похожего на короткий человеческий секрет.

Типичные ошибки при работе с хешами паролей

На практике большая часть проблем — не в выборе алгоритма (сегодня почти везде уже стоит bcrypt или Argon2 по умолчанию в фреймворке), а в деталях вокруг него:

  • Быстрая функция «по старой памяти». Самая частая ошибка в legacy-коде — md5(password) или sha256(password) без соли и cost factor, доставшиеся от старой версии проекта. Выглядит как хеширование, но не даёт ожидаемой защиты.
  • Слишком низкий cost factor «для скорости сайта». Понижение параметра ради ускорения логина при нагрузке — это перекладывание проблемы производительности на пользователей в случае утечки. Правильный путь — масштабировать сервер логина, а не снижать стоимость перебора.
  • Самописная реализация «как bcrypt, но проще». Своя версия медленного хеша почти всегда либо недостаточно устойчива к параллелизации, либо содержит побочные каналы утечки времени. Используйте проверенные библиотеки экосистемы.
  • Отсутствие плана на повышение cost factor. Параметр, зашитый один раз при запуске проекта, через несколько лет окажется неоправданно низким относительно текущего железа. password_needs_rehash и подобные проверки при логине позволяют поднимать cost factor постепенно.
  • Пароль в логах или в письмах при регистрации. Даже идеальный алгоритм бесполезен, если пароль в открытом виде на секунду оказался в access-логе прокси или в письме подтверждения.

Если вы разворачиваете сервис на арендованном сервере и храните секреты приложения (ключи API, пароли к БД) не в самих паролях пользователей, а рядом — в переменных окружения или Docker-конфигурации — это уже отдельная, но смежная тема: у нас есть отдельный разбор про управление паролями в Docker secrets, которая касается защиты не хешей пользователей, а секретов самого приложения.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли просто добавить соль к SHA-256 и не переходить на bcrypt?

Соль решает проблему rainbow-таблиц (предвычисленных хешей популярных паролей), но не замедляет перебор конкретного пароля. SHA-256 с солью всё равно вычисляется за исчезающе малое время. Соль обязательна, но она не заменяет медленный алгоритм — это две разные защиты для двух разных атак.

Насколько выше должен быть cost factor, чем сейчас стоит по умолчанию?

Универсального числа не существует — значение зависит от процессора сервера, нагрузки логинов в секунду и допустимой задержки. Замерьте реальное время хеширования на своём железе при разных значениях параметра, а не переносите цифры из чужой статьи без проверки.

Что лучше выбрать для нового проекта — bcrypt, scrypt или Argon2?

Разумной точкой по умолчанию на 2026 год считается Argon2id — он memory-hard и учитывает опыт предыдущих алгоритмов. bcrypt остаётся приемлемым выбором там, где важна максимальная совместимость с существующими библиотеками, но контроля над памятью у него меньше.

Если у меня уже есть база с паролями, хешированными SHA-256, что делать?

Одномоментно пересчитать все хеши нельзя — у вас нет исходных паролей. При следующем успешном логине пользователя перехешируйте пароль правильным алгоритмом (в этот момент он у вас есть в открытом виде) и замените старую запись. Так база мигрирует постепенно, без принудительного сброса паролей у всех сразу.

Замедляет ли Argon2 или bcrypt сервер при большом числе одновременных логинов?

Да, это ожидаемый компромисс: чем выше cost factor, тем больше CPU и памяти требует один логин, и это стоит учитывать в расчёте ресурсов сервера. Логин обычно не самая частая операция по сравнению с остальными запросами приложения, поэтому запас под аутентификацию закладывают отдельно.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →