Криптография
Шифрование, кодирование и хеширование: что делает каждое
Разделяем кодирование, шифрование и хеширование на лабораторных с Base64, SHA-256 и OpenSSL. Почему Base64 ничего не защищает, а хеши необратимы.
Почему быстрые хеши не годятся для паролей, что делают соль и коэффициент нагрузки и как хешировать и проверять пароли с Argon2id в Python через argon2-cffi.
Базы паролей утекают чаще, чем команды закладывают в планы. Когда это случается, атакующему достаются хеши, а не пароли. Сколько работы понадобится, чтобы перейти от одного к другому, зависит от двух решений: какую функцию вы взяли и какие настройки ей задали. В статье объясняется, почему для хранения паролей нужен особый вид хеша, что делают соль и коэффициент нагрузки и почему актуальные рекомендации указывают на 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.
Коэффициент нагрузки настраивают, раз и навсегда он не выбирается. Повышайте его по мере роста скорости оборудования и в пределах бюджета на вход. Храните настройки вместе с каждым хешем, чтобы позже распознавать и обновлять старые хеши. Это делают последние шаги лабораторной.
Argon2 выиграл Password Hashing Competition в 2015 году. У него три варианта.
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 с аудитором заранее.
Когда библиотека хеширования сохраняет пароль, происходит пять вещей.
Благодаря шагу 3 отдельный столбец для соли не нужен. Закодированная строка имеет такой формат.
$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, если он у вас работает. Строки хешей у вас будут другими, потому что соль случайна.
python3 -m venv .venv && . .venv/bin/activate
pip install argon2-cffiВ Windows активируйте его командой .venv\Scripts\activate в PowerShell или cmd либо . .venv/Scripts/activate в Git 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))"Полученный вывод:
$argon2id$v=19$m=65536,t=3,p=4$abVC6VF2EGJW1SCX/D6WBQ$XHKggONKypxBhdq7sWTbL/sMkglTAKu9y1FlL5p4dic
True
Falseverify возвращает True для правильного пароля. check_needs_rehash возвращает False, потому что этот хеш создан с текущими значениями по умолчанию. Это затраты времени 3, затраты памяти 65536 KiB (64 MiB), параллелизм 4, 32-байтный хеш, 16-байтная соль и вариант ID.
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')"Полученный вывод:
mismatch detectedПри неверном пароле возврата False не будет. verify выбрасывает VerifyMismatchError, поэтому проверку надо помещать в блок except. Искажённая сохранённая строка вызывает другое исключение, InvalidHashError. Обрабатывайте его как проблему данных на сервере и не показывайте пользователю.
Захешируйте один и тот же пароль дважды.
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)"Полученный вывод:
$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Один пароль дал две разные строки. Параметры совпадают, потому что оба запуска использовали одинаковые значения по умолчанию. Различаются только соль и выход. По таблице нельзя понять, что у этих двух аккаунтов один и тот же пароль.
Сохраните это как parts.py.
from argon2 import PasswordHasher
h = PasswordHasher().hash("correct horse battery")
print(h.split("$"))Запустите командой python3 parts.py. Полученный вывод:
['', 'argon2id', 'v=19', 'm=65536,t=3,p=4', 'waS+Ec4iQCNIuhRQbv2sWQ', 'yLioGsHJoVv+vTu2kRoYmXTLjbbjJSe2tPSExz3Sd5A']Пустой первый элемент появляется из-за ведущего $. Дальше идут вариант, версия, параметры, соль и хеш. Для проверки больше ничего не нужно, поэтому одного столбца достаточно.
Настройки со временем меняются. Этот скрипт создаёт хеш со слабыми, старыми настройками и сравнивает его с текущими значениями по умолчанию. Сохраните его как rehash.py.
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. Полученный вывод:
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Порядок здесь важен. Сначала проверка, потом перехеширование, чтобы до обновления дошёл только правильный пароль. В настоящем приложении записывайте новую строку в ту же запись сразу после успешного входа. Если пропустить этот шаг, старые настройки останутся в таблице на неопределённый срок.
Сохраните это как bench.py.
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. Полученный вывод:
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 на оборудовании, похожем на продакшен, пока один вход не станет стоить несколько сотен миллисекунд.
verify. Не сравнивайте строки через ==.PasswordHasher, verify и check_needs_rehash.Криптография
Разделяем кодирование, шифрование и хеширование на лабораторных с Base64, SHA-256 и OpenSSL. Почему Base64 ничего не защищает, а хеши необратимы.
Криптография
Публичные и приватные ключи с нуля: что доказывает подпись, почему приватный ключ остаётся в секрете, и лабораторная на Ed25519, где изменённый файл не проходит проверку.
Криптография
Криптография с нуля: шифрование, ключи, хеши, подписи и почему шифр Цезаря учит идеям, но ничего не защищает.