Перейти к содержимому
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

Пароли и хеширование с Argon2id

Почему быстрые хеши не годятся для паролей, что делают соль и коэффициент нагрузки и как хешировать и проверять пароли с Argon2id в Python через argon2-cffi.

Средний9 мин чтенияАвтор: NotHoly

О чём эта статья

Базы паролей утекают чаще, чем команды закладывают в планы. Когда это случается, атакующему достаются хеши, а не пароли. Сколько работы понадобится, чтобы перейти от одного к другому, зависит от двух решений: какую функцию вы взяли и какие настройки ей задали. В статье объясняется, почему для хранения паролей нужен особый вид хеша, что делают соль и коэффициент нагрузки и почему актуальные рекомендации указывают на Argon2id. Вы захешируете и проверите пароль в Python, разберёте каждую часть закодированного хеша и перехешируете сохранённые пароли при смене настроек.

Понадобятся терминал и Python 3. Лабораторная ставит один пакет, argon2-cffi.

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

Хеш-функция превращает входные данные в выход фиксированного размера. SHA-256 делает это очень быстро, и для контрольных сумм и целостности файлов это как раз то, что нужно. Для паролей скорость становится проблемой. Украденную таблицу паролей атакуют перебором: взять кандидата, захешировать, сравнить, повторить. Если каждая попытка дёшева, на видеокартах их можно выполнять миллиардами.

В OWASP Password Storage Cheat Sheet об этом сказано прямо. SHA-256 — быстрая функция, а скорость и нужна атакующему, когда на входе пароль, то есть короткая строка, придуманная человеком.

Разрыв можно измерить на своей машине. Бенчмарк в лабораторной хеширует пароль через SHA-256 с солью и через Argon2id, а затем печатает скорость каждого варианта.

Что делает соль

Соль — это случайное значение, разное для каждого пароля, которое хранится рядом с хешем. У неё две задачи.

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

Соль не секрет, и слабый пароль она сильным не делает. Её задача в том, чтобы атакующий не мог повторно использовать работу для разных пользователей. RFC 9106 рекомендует 128-битную соль и допускает 64 бита, если места мало. NIST SP 800-63B требует не меньше 32 бит. В argon2-cffi по умолчанию 16 байт, то есть 128 бит.

Что делает коэффициент нагрузки

Коэффициент нагрузки (work factor) — это настройка, которая намеренно делает каждую попытку дорогой. Более старые схемы, такие как PBKDF2, в основном считают итерации. У Argon2 три параметра.

  • Затраты памяти (m): объём RAM в KiB, который использует каждый хеш.
  • Затраты времени (t): сколько проходов функция делает по этой памяти.
  • Параллелизм (p): сколько дорожек может работать одновременно.

Главную роль играет память. У GPU тысячи маленьких ядер, и у каждого ядра мало собственной памяти. Если на каждую попытку нужны десятки мегабайт, атакующий не сможет выполнять параллельно столько попыток, сколько против функции, которой хватает нескольких килобайт. Это свойство называется memory-hardness.

Коэффициент нагрузки настраивают, раз и навсегда он не выбирается. Повышайте его по мере роста скорости оборудования и в пределах бюджета на вход. Храните настройки вместе с каждым хешем, чтобы позже распознавать и обновлять старые хеши. Это делают последние шаги лабораторной.

Почему Argon2id

Argon2 выиграл Password Hashing Competition в 2015 году. У него три варианта.

  • Argon2d выбирает адреса памяти в зависимости от данных. Он хорошо устойчив к атакам с использованием GPU, но такая зависящая от данных схема доступа может раскрывать информацию через тайминги.
  • Argon2i использует схему доступа, которая не зависит от входа. Это защищает от утечек через тайминги, но отчасти снижает устойчивость к атакам, которые обменивают память на время.
  • Argon2id выполняет независимую от данных схему в первой половине первого прохода, а потом переключается на зависимую.

Argon2id закрывает обе проблемы. RFC 9106 рекомендует его как вариант для общего применения, а OWASP ставит его первым среди вариантов хеширования паролей.

RFC 9106 приводит два рекомендуемых набора настроек. Первый, для систем с запасом памяти: 2 GiB, 1 проход и 4 дорожки. Второй, для окружений с ограниченной памятью: 64 MiB, 3 прохода и 4 дорожки. Минимум OWASP составляет 19 MiB, 2 итерации и 1 дорожку. OWASP также приводит альтернативы, которые обменивают память на проходы, например 12 MiB при 3 проходах или 7 MiB при 5 проходах, все при 1 дорожке. Значения argon2-cffi по умолчанию совпадают со вторым набором из RFC.

Считайте это нижними границами и точками отсчёта, останавливаться на них как на цели не стоит. Распространённое практическое правило: настраивайте параметры, пока один хеш не станет занимать несколько сотен миллисекунд на вашем продакшен-оборудовании. Затем убедитесь, что память на хеш, умноженная на число входов, которые могут прийти одновременно, всё ещё помещается на ваших серверах. Это правило пришло из практики, ни в одном из двух стандартов его нет.

NIST подходит к вопросу иначе. SP 800-63B требует, чтобы пароли были посолены и захешированы подходящей схемой, и говорит, что следует использовать одобренную схему из SP 800-132, где описан PBKDF2. Argon2id там не назван. Если программа соответствия требует одобренных NIST алгоритмов, согласуйте выбор Argon2id с аудитором заранее.

Как устроен сохранённый хеш, шаг за шагом

Когда библиотека хеширования сохраняет пароль, происходит пять вещей.

  1. Библиотека генерирует случайную соль.
  2. Она прогоняет Argon2id по паролю, соли и параметрам стоимости.
  3. Она кодирует вариант, версию, параметры, соль и выход в одну строку.
  4. При входе она читает строку, берёт из неё соль и параметры, заново вычисляет выход и сравнивает два значения.
  5. Если параметры в строке старее текущих настроек, после успешного входа вы перехешируете пароль.

Благодаря шагу 3 отдельный столбец для соли не нужен. Закодированная строка имеет такой формат.

text
$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
  • argon2id — это вариант.
  • v=19 — версия Argon2, 1.3, записанная в десятичном виде. В шестнадцатеричном это 0x13.
  • m, t и p — память в KiB, затраты времени и параллелизм.
  • <salt> и <hash> записаны в base64 без дополнения. Хеш — это производный выход, по умолчанию 32 байта.

Строка из лабораторной занимает 97 символов. Используйте столбец не меньше чем на 255 символов или текстовый тип. Делить её на столбцы не стоит, разве что вам нравится писать собственный парсер.

Лабораторная

Лабораторная использует локальное виртуальное окружение и не требует сетевых сервисов. Вывод ниже получен на одной машине с Windows 11, Python 3.12.10, argon2-cffi 25.1.0 и argon2-cffi-bindings 26.1.0. На этой машине python3 не было в PATH в Git Bash, поэтому команды запускались как python. Используйте python3, если он у вас работает. Строки хешей у вас будут другими, потому что соль случайна.

1. Создайте окружение

bash
python3 -m venv .venv && . .venv/bin/activate
pip install argon2-cffi

В Windows активируйте его командой .venv\Scripts\activate в PowerShell или cmd либо . .venv/Scripts/activate в Git Bash.

2. Хеширование, проверка и контроль параметров

bash
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h); print(ph.verify(h,'correct horse battery')); print(ph.check_needs_rehash(h))"

Полученный вывод:

text
$argon2id$v=19$m=65536,t=3,p=4$abVC6VF2EGJW1SCX/D6WBQ$XHKggONKypxBhdq7sWTbL/sMkglTAKu9y1FlL5p4dic
True
False

verify возвращает True для правильного пароля. check_needs_rehash возвращает False, потому что этот хеш создан с текущими значениями по умолчанию. Это затраты времени 3, затраты памяти 65536 KiB (64 MiB), параллелизм 4, 32-байтный хеш, 16-байтная соль и вариант ID.

3. Определение неверного пароля

bash
python3 -c "from argon2 import PasswordHasher; from argon2.exceptions import VerifyMismatchError; ph=PasswordHasher(); h=ph.hash('a');
try: ph.verify(h,'b')
except VerifyMismatchError: print('mismatch detected')"

Полученный вывод:

text
mismatch detected

При неверном пароле возврата False не будет. verify выбрасывает VerifyMismatchError, поэтому проверку надо помещать в блок except. Искажённая сохранённая строка вызывает другое исключение, InvalidHashError. Обрабатывайте его как проблему данных на сервере и не показывайте пользователю.

4. Один пароль, разные соли

Захешируйте один и тот же пароль дважды.

bash
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"

Полученный вывод:

text
$argon2id$v=19$m=65536,t=3,p=4$p8OU0plYOCO2/v70mjYTFA$FwhMW3ECJ0F/8rulKrJ158HjyNekURguaCIfoVk0i/o
$argon2id$v=19$m=65536,t=3,p=4$MTgAgrtBHJmc1eL2iKy8nA$h/7+8J6Cdb82XTBGWsLjbYBIh05934Yel4y6tyZ6NYg

Один пароль дал две разные строки. Параметры совпадают, потому что оба запуска использовали одинаковые значения по умолчанию. Различаются только соль и выход. По таблице нельзя понять, что у этих двух аккаунтов один и тот же пароль.

5. Разбор частей закодированного хеша

Сохраните это как parts.py.

python
from argon2 import PasswordHasher

h = PasswordHasher().hash("correct horse battery")
print(h.split("$"))

Запустите командой python3 parts.py. Полученный вывод:

text
['', 'argon2id', 'v=19', 'm=65536,t=3,p=4', 'waS+Ec4iQCNIuhRQbv2sWQ', 'yLioGsHJoVv+vTu2kRoYmXTLjbbjJSe2tPSExz3Sd5A']

Пустой первый элемент появляется из-за ведущего $. Дальше идут вариант, версия, параметры, соль и хеш. Для проверки больше ничего не нужно, поэтому одного столбца достаточно.

6. Перехеширование при смене параметров

Настройки со временем меняются. Этот скрипт создаёт хеш со слабыми, старыми настройками и сравнивает его с текущими значениями по умолчанию. Сохраните его как rehash.py.

python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

old = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
stored = old.hash('correct horse battery')
print('stored:', stored)

new = PasswordHasher()  # current defaults
print('needs rehash under new params:', new.check_needs_rehash(stored))
try:
    new.verify(stored, 'correct horse battery')
    print('verify ok')
    if new.check_needs_rehash(stored):
        stored = new.hash('correct horse battery')
        print('rehashed:', stored)
except VerifyMismatchError:
    print('wrong password')
print('after rehash needs rehash:', new.check_needs_rehash(stored))

Запустите python3 rehash.py. Полученный вывод:

text
stored: $argon2id$v=19$m=19456,t=2,p=1$EWji+p6+3HbAKY+wkykcXg$NxPq/zGPsx9okK4vQCKfZ4BVoSOqeF1QkHxd3f2g2uE
needs rehash under new params: True
verify ok
rehashed: $argon2id$v=19$m=65536,t=3,p=4$FkifxeWUHV9eU7d8JFt6mg$ZFoMkNjYqo4D/bax/jPz80Hvr4mjLWRi4TVoqCoR6ng
after rehash needs rehash: False

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

7. Измерьте стоимость на своём оборудовании

Сохраните это как bench.py.

python
import hashlib, time, os
from argon2 import PasswordHasher

pw = b'correct horse battery'
salt = os.urandom(16)
n = 1_000_000
t = time.perf_counter()
for _ in range(n):
    hashlib.sha256(salt + pw).digest()
dt = time.perf_counter() - t
print(f"sha256 salted: {n} hashes in {dt:.2f} s, {n/dt:,.0f} per second")

ph = PasswordHasher()
m = 10
t = time.perf_counter()
for _ in range(m):
    ph.hash('correct horse battery')
dt = time.perf_counter() - t
print(f"argon2id default: {m} hashes in {dt:.2f} s, {dt/m*1000:.0f} ms per hash")

Запустите python3 bench.py. Полученный вывод:

text
sha256 salted: 1000000 hashes in 4.06 s, 246,343 per second
argon2id default: 10 hashes in 6.40 s, 640 ms per hash

Это единичные запуски на одной машине с Windows. Строка SHA-256 измеряет цикл на Python без оптимизаций, так что занижает то, на что способно с SHA-256 специализированное оборудование. Строка Argon2id показывает цену, которую платит каждый вход. Точные цифры значат меньше, чем разрыв. Вызов SHA-256 с солью здесь занимает около 4 микросекунд. Вызов Argon2id занимает около 640 миллисекунд, и так и задумано. Настраивайте m, t и p на оборудовании, похожем на продакшен, пока один вход не станет стоить несколько сотен миллисекунд.

Частые ошибки

  • Быстрые хеши. SHA-256, SHA-512 или MD5 для паролей. Пароль и контрольная сумма файла — разные задачи.
  • Общая или предсказуемая соль. Одна соль на всех пользователей или соль, выведенная из имени пользователя. Генерируйте случайную соль для каждого пароля.
  • Копирование настроек без измерений. Значения библиотеки по умолчанию годятся для старта, но для вашего сервера их ещё надо проверить. Сначала измерьте.
  • Память без бюджета на параллельность. 64 MiB на вход звучит скромно, пока не придут одновременно 100 входов, а это около 6.4 GB. Рассчитывайте память на пиковое число одновременных входов.
  • Сравнение строк хешей вручную. Вызывайте verify. Не сравнивайте строки через ==.
  • Пароли в логах. В логах, отчётах об ошибках и аналитике не должно быть ни пароля, ни хеша, ни полного тела запроса на вход.
  • Нет пути обновления. Храните параметры внутри сохранённой строки и перехешируйте при успешном входе.
  • Перец как лекарство. OWASP предлагает перец, секрет вне базы данных, как дополнительный эшелон защиты. В том же cheat sheet отмечено, что сам по себе перец добавляет мало. Если используете его, храните в хранилище секретов и не заменяйте им нормальный хеш.

Что дальше

  • Прочитайте разделы RFC 9106 о параметрах и безопасности и сравните их со своей моделью угроз.
  • Прочитайте документацию argon2-cffi о PasswordHasher, verify и check_needs_rehash.
  • Если в вашем веб-фреймворке есть модуль паролей, проверьте, не использует ли он уже Argon2id, прежде чем писать обёртку.
  • Прежде чем проектировать вход, разберитесь в разнице между хешированием, кодированием и шифрованием. Об этом предыдущая статья: шифрование, кодирование и хеширование. Следующая переходит к ключевым парам и подписям: открытые и закрытые ключи.

Источники

Источники

  1. RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
  2. OWASP Cheat Sheet Series: Password Storage Cheat Sheet
  3. NIST SP 800-63B-4: Authentication and Authenticator Management
  4. argon2-cffi documentation

Ещё по теме: Криптография