Библиотека — KDF для паролей: почему медленно и с памятью, а не быстро [X]
Шифр
crypto-password-kdf
Разделы
security
Сверено
14 сентября — в этот день утверждения статьи сверялись с первоисточниками, а не правился текст
Объём
44 404 знака · 23 раздела · чтения около 45 минут

KDF для паролей: почему медленно и с памятью, а не быстро


Две разные задачи под одним словом

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

Первая задача разобрана в статье про обмен ключами: на входе уже есть секрет, у которого достаточно энтропии, но он неравномерен и не размечен по ролям. HKDF-Extract собирает энтропию в равномерную строку, HKDF-Expand разводит её на именованные ключи. Быстрота здесь — достоинство: рукопожатие делается на каждое соединение, а противнику перебирать нечего — гадать про 32 байта из CSPRNG бессмысленно.

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

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

Статья закрывает вопрос, оставленный открытым таблицей префиксов в заметке про пользователей и PAM: там сказано, что yescrypt устойчив к перебору на GPU, но не сказано почему. Граница между текстами проходит не по именам настроек, а по вопросу: «как это настроить и где править» — там, «почему в разных системах вышли разные умолчания и что за ними стоит» — здесь. Поэтому ENCRYPT_METHOD встретится и ниже, но как наблюдаемое следствие выбора, а не как инструкция.

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


Пароль — не ключ

Насколько именно не ключ

Порядки величин стоит держать в голове, потому что из них следует всё остальное. Бонно в «The Science of Guessing» (IEEE S&P 2012) разобрал корпус почти из 70 миллионов паролей Yahoo! и оценил их стойкость так: меньше 10 бит против онлайнового перебора и около 20 бит против оптимальной офлайновой атаки по словарю. Двадцать бит — это миллион вариантов. Симметричный ключ, о котором идёт речь в соседних статьях цикла, — 128 бит.

Разрыв в сотню бит не закрывается никаким KDF, и это надо сказать прямо, потому что на этом держится вся дальнейшая арифметика. RFC 8018, §4.2, формулирует предельно честно: счётчик итераций c увеличивает стойкость пароля на log2(c) бит. Модные сегодня 600 000 итераций PBKDF2 — это плюс девятнадцать бит. Защитник платит за них линейно, атакующий расплачивается логарифмом.

Значит, задача KDF — не догнать 128 бит, а сдвинуть экономику: сделать так, чтобы перебор миллиона правдоподобных вариантов по всей утёкшей базе стоил дороже, чем пароли в ней стоят. Это ровно та формулировка, которую даёт RFC 9106, §7.3, описывая рационального защитника: он максимизирует стоимость перебора для атакующего при фиксированном времени вычисления на своей машине. Не «при фиксированной стойкости», а при фиксированном своём времени — потому что своё время и есть единственный дефицитный ресурс.

Соль публична, и это не оговорка

Соль решает три задачи, и ни одна из них не требует секретности. Страница crypt(5) из libxcrypt перечисляет их в явном виде: нельзя посчитать хеш пароля один раз и проверить его против всех учётных записей сразу; нельзя узнать, что у двух записей одинаковый пароль, не угадав хотя бы один из них; таблица предвычисленных хешей должна содержать по записи на каждое значение соли, что делает её непрактично большой.

Третий пункт — это и есть ответ радужным таблицам, но обратите внимание, что он в списке не первый. Важнее амортизация: без соли стоимость атаки на базу из миллиона записей равна стоимости атаки на одну. Соль ломает именно это, и потому она обязана быть уникальной на запись, а не просто случайной. Длину задают трое, и разброс между ними четырёхкратный: SP 800-63B (редакция 4, 26 августа 2025) требует не меньше 32 бит, RFC 8018, §4.1, советует не меньше восьми октетов, то есть 64 бит, RFC 9106 рекомендует 128. Читать это стоит так: 32 бита — нижняя граница, ниже которой начинается нарушение «SHALL», 128 — норма, и экономить между ними не на чем, потому что соль не стоит ничего ни в вычислениях, ни в хранении.

Перец отвечает на другую угрозу

Перец (pepper) — секретное значение, общее на всю установку и хранящееся отдельно от хешей. Оно защищает от строго одного сценария: утёк дамп базы, но не утёк конфиг. SP 800-63B-4 рекомендует это отдельным SHOULD: выполнить дополнительный проход keyed-хешированием или шифрованием на ключе, известном только верификатору, причём «the secret key value SHALL be stored separately from the hashed passwords» и хранить его следует в защищённом аппаратно контуре — HSM или TEE.

Дальше начинается развилка, и рекомендации по ней расходятся. Внутренний вариант подмешивает перец к паролю до хеширования, внешний накладывает его на уже готовый хеш. OWASP описывает оба и для bcrypt прямо советует внутренний: bcrypt(base64(hmac-sha384(data:$password, key:$pepper)), $salt, $cost). Доводы там конкретные и оба вне темы перца: предварительный HMAC укладывает пароль любой длины в 72 байта bcrypt и закрывает password shucking — приём, при котором атакующий сверяет не пароль, а утёкший откуда-то ещё хеш H($password), сводя взлом bcrypt к взлому быстрой функции H. Цена названа в том же документе, в общих требованиях к перцу: сменить перец, не зная паролей, нельзя, поэтому его компрометация означает принудительный сброс паролей у всех.

Вердикт здесь авторский, и он в пользу внешнего варианта — ровно из-за сменяемости. Ключ, который годами лежит в HSM, рано или поздно придётся ротировать, а массовый сброс паролей по ущербу сопоставим с той самой утечкой, от которой перец и защищает. Одна оговорка существенна и обычно проглатывается: дёшево ротируется только шифрование поверх хеша — расшифровать старым ключом, зашифровать новым; внешний HMAC необратим точно так же, как внутренний, и сменяемости не даёт. SP 800-63B-4 разрешает оба примитива («keyed hashing or encryption operation») — из этих двух надо брать шифрование. Довод OWASP про 72 байта при этом не опровергается, он просто про другое: он доказывает, что длинный пароль перед bcrypt надо пред-хешировать, а не то, что делать это следует на секретном ключе.


Три поколения, и каждое — ответ на подешевевшее железо

Перечислять поколения в хронологическом порядке было бы бессмысленно: интересна не последовательность, а причинность. Каждое следующее появилось потому, что предыдущее проигрывало не криптоанализу, а прогрессу в кремнии.

Формулировку этого проигрыша дал Персиваль во введении к «Stronger Key Derivation via Sequential Memory-Hard Functions» (BSDCan'09), и она стоит того, чтобы привести её целиком по смыслу. Правильная единица измерения стоимости вычисления — доллар-секунды, а не операции. По мере развития полупроводниковой технологии схемы не просто становятся быстрее — они становятся меньше, а значит, за те же деньги помещается больше параллельных экземпляров. Следствие: «using existing key derivation algorithms, even if the iteration count is increased such that the time taken to verify a password remains constant, the cost of finding a password by using a brute force attack implemented in hardware drops each year». То есть схема, настроенная на постоянное время проверки, дешевеет для атакующего сама по себе, без единой правки конфигурации.

Итерации: дорого по времени, дёшево по площади

PBKDF2 (RFC 8018, он же PKCS #5 v2.1) применяет HMAC заданное число раз и складывает результаты по XOR. Единственный параметр — счётчик итераций. Состояние функции — два состояния хеша и пара блоков буфера, то есть сотни байт.

Именно этот размер и проигрывает. Задача перебора embarrassingly parallel: каждая догадка независима, синхронизировать нечего. Кристалл, на котором помещается сотня байт состояния, вмещает столько ядер перебора, сколько влезет по площади ALU, — и с каждым технологическим узлом их становится больше. Защитник при этом привязан к одному ядру своего процессора: ему надо уложиться в интерактивную задержку.

Цифры из RFC 8018 сегодня выглядят анекдотично, и полезно понимать, что это не ошибка авторов, а возраст документа: «a modest number of iterations (say, 1000) is not likely to be a burden for legitimate parties». Документ повторяет рекомендацию NIST SP 800-132 — минимум 1000, для особо критичных ключей может подойти 10 000 000. Рекомендация OWASP на дату сверки — 600 000 итераций с HMAC-SHA-256, и она приведена там ровно с одной оговоркой: «if FIPS-140 compliance is required».

Вердикт: PBKDF2 в новой системе — только под внешнее требование. Это не мнение: SP 800-132 остаётся действующим документом 2010 года, NIST объявил о намерении его пересмотреть в мае 2023-го — с прямо названной целью одобрить дополнительную память-жёсткую функцию, — и на дату сверки черновик не опубликован. Пока он не выйдет, «одобренный NIST парольный KDF» означает PBKDF2 и ничего больше, и цепочка замыкается: SP 800-63B-4 в разделе про хранение паролей отсылает к «an approved password hashing scheme published in the latest revision of [SP800-132] or updated NIST guidelines».

Память: перенос стоимости туда, где нет преимущества

scrypt (RFC 7914, 2016; работа Персиваля — 2009) добавляет второй ресурс. Функция ROMix заполняет массив из N блоков по 128·r байт последовательно, а затем N раз читает из него по адресу, зависящему от только что вычисленного значения. Второй проход и делает всю работу: адрес неизвестен заранее, поэтому весь массив обязан лежать в памяти целиком.

Честная мера здесь — не время и не память по отдельности, а их произведение. RFC 9106, §7.2, называет её прямо: time-area product, где память и дополнительные ядра дают площадь, а время растёт из-за перевычислений. Смысл перехода к памяти в том, что площадь памяти покупается за деньги в куда более линейной пропорции, чем площадь логики, и у атакующего на ней нет преимущества перед защитником: гигабайт DRAM стоит одинаково обоим.

Слово «обязан лежать» требует уточнения, и в нём — разница между поколениями. Память можно разменять на время: хранить половину блоков, а недостающие пересчитывать. Такой размен называется TMTO, и scrypt к нему намеренно дружелюбен — в сравнении от автора yescrypt это сформулировано как «scrypt is deliberately TMTO-friendly (and moreover, computing it in less memory takes up to 4x less than proportionately longer)». Argon2 и yescrypt, наоборот, TMTO-стойкие: считать их в меньшей памяти непропорционально дольше.

Порядок выигрыша виден по таблице 1 в работе Персиваля — стоимости оборудования, способного найти пароль в среднем за год. Колонку надо называть точно, иначе числа не сойдутся: речь про «8 chars» — восемь знаков, выбранных из 95 печатных ASCII. В ней стоит $18 тыс. у PBKDF2 (100 мс), $130 тыс. у bcrypt (95 мс) и $4,8 млн у scrypt (64 мс); в соседней колонке «8 letters», где восемь случайных строчных букв, у scrypt всего $150 — в тридцать тысяч раз меньше, и перепутать их, открыв PDF, легко. Для интерактивного логина автор подытоживает всю таблицу как 35-кратный выигрыш над bcrypt и 260-кратный над PBKDF2. Оговорка самих авторов существенна и её нельзя опускать: значения «very approximate» и отражают стоимость только криптографической части схемы по технологии примерно 2002 года. Полезно здесь отношение между строками, а не абсолютные суммы.

Настраиваемые профили: три параметра вместо одного

Argon2 выиграл конкурс Password Hashing Competition в июле 2015 года (конкурс шёл с 2013-го, четыре финалиста получили особое упоминание) и описан в RFC 9106. Параметров три: m — память, t — число проходов, p — число дорожек (lanes), то есть степень параллелизма внутри одного вычисления.

Третий параметр — не бесплатное ускорение, и это тонкость, которую обычно пропускают. Дорожки позволяют защитнику занять несколько ядер и уложиться в бюджет времени при большей памяти. Но тот же параллелизм достаётся атакующему: память одного вычисления оказывается разделена между потоками, и на GPU это превращается в возможность потратить больше вычислительных ресурсов на ту же суммарную память. Именно за это Argon2 критикуют авторы yescrypt, и критика конкретна: Argon2 допускает параллельную обработку восьми 128-байтных кусков внутри килобайтного блока всего с двумя точками синхронизации на блок.

Вариантов три, и различаются они тем, как выбирается адрес для чтения:

Вариант Адрес зависит от Устойчив к Уязвим к
Argon2d обрабатываемых данных TMTO лучше всех: лучший размен даёт всего 1,33× утечке по времени доступа к памяти
Argon2i только от индексов атакам по сторонним каналам TMTO: для 1–2 проходов известен размен в 5×
Argon2id первая половина первого прохода как 2i, дальше как 2d и к тому, и к другому лучший размен для одного прохода — около 2,1×

Числа в таблице — из RFC 9106, §7.2. Из них же следует и вердикт самого RFC, и он безальтернативен: «if you do not know the difference between the types or you consider side-channel attacks to be a viable threat, choose Argon2id». Argon2d осмыслен там, где атакующий заведомо не делит машину с защитником, — RFC называет майнинг криптовалют; Argon2i отдельно брать не за чем, потому что его стойкость к сторонним каналам целиком содержится в 2id.

Вердикт по поколениям

Функция Что настраивается Память при типовых настройках Куда упирается атакующий Когда брать
PBKDF2 итерации сотни байт площадь логики; растёт с каждым техпроцессом только под требование FIPS
bcrypt цена ключевого расписания 4 KiB, фиксировано локальная память ядра GPU — но 4 KiB в неё помещаются не переписывать существующее
scrypt N, r, p от единиц до сотен MiB площадь памяти, но с поблажкой на TMTO когда нет Argon2id, и в готовых форматах
yescrypt один логарифмический cost в интерфейсе crypt() 16 MiB при умолчании libxcrypt площадь памяти плюс задержка умножений и кэш системный crypt()
Argon2id m, t, p независимо от 19 MiB до гигабайтов площадь памяти, TMTO не помогает новый код, где есть библиотека

Для нового кода — Argon2id: у него единственного параметры разведены так, что бюджет по памяти и бюджет по времени задаются независимо, и он есть во всех живых экосистемах. Существующий bcrypt переписывать не надо: он не сломан, а цена миграции парольных хешей — принудительная смена паролей или ожидание естественной ротации. PBKDF2 и sha512crypt — ровно наоборот: не сломаны криптографически, но проигрывают по устройству, и их стоит менять при первой возможности сменить бесплатно.


Что стоит в системах на самом деле

Интерфейс решает за алгоритм

Хеш в /etc/shadow вычисляет crypt(3), а интерфейс у неё — из семидесятых: одна строка на вход, одна на выход, все параметры закодированы внутри строки-настройки. Реализацию давно поставляет не libc: glibc собирала libcrypt по умолчанию последний раз в 2.37 — в 2.38 сборка отключена по умолчанию и --enable-crypt стал обязательным, — а в 2.39 (январь 2024) её убрали совсем вместе с опциями --enable-crypt и --enable-nss-crypt. На всех массовых дистрибутивах на её месте стоит libxcrypt.

Отсюда следует то, что объясняет умолчания лучше любых сравнений алгоритмов: в системном shadow может оказаться только то, что реализовано в libxcrypt. crypt(5) перечисляет полный список — yescrypt, gost-yescrypt, sm3-yescrypt, scrypt, bcrypt, sha512crypt, sha256crypt и далее вниз по возрасту. Argon2 в нём нет. Новость Arch Linux о смене умолчания называет это первым доводом: yescrypt выбран «due to its adoption (readily available in libxcrypt, which is used by pam)», и там же прямо сказано, что Argon2 выиграл PHC, но в libxcrypt его нет.

yescrypt: почему он, а не Argon2id

yescrypt спроектирован Solar Designer как надстройка над scrypt и получил на PHC особое упоминание, не выиграв конкурс. Технический довод в пользу него сформулирован в README проекта и стоит того, чтобы прочитать его внимательно: у память-жёстких схем «at high target throughput and/or low target latency their memory usage is unreasonably low, up to the point where they're not obviously better than the much older bcrypt (considering attackers with pre-existing hardware)».

Это и есть ключ ко всему разделу. Логин — это как раз режим низкой задержки: на проверку пароля можно потратить десятки миллисекунд, и в них влезает не два гигабайта, а полтора-два десятка мегабайт. На таком бюджете память сама по себе почти ничего не покупает — GPU с несколькими гигабайтами памяти всё ещё держит сотни параллельных попыток. yescrypt добавляет второй механизм: латентность вычисления, завязанную на целочисленное умножение и скорость локальной памяти. По собственной оценке авторов, yescrypt выполняет 96 последовательных умножений на килобайт против 16 у Argon2, и это даёт гарантированный минимум времени независимо от того, насколько быстрая память у атакующего.

Три довода, которыми предложение Fedora 35 обосновывает выбор yescrypt вместо Argon2, повторяют этот же сюжет: зависимость не только от RAM, но и от быстрой памяти на кристалле, что даёт «bcrypt-like anti-GPU properties even at very low per-hash RAM sizes»; меньше низкоуровневого параллелизма, который можно извлечь на GPU; и криптографическая часть на SHA-256, HMAC и PBKDF2, то есть на одобренных NIST примитивах, против BLAKE2 у Argon2.

Умолчание конкретно: libxcrypt при cost-факторе 5 (значение по умолчанию, диапазон 1–11) ставит N = 4096, r = 32, p = 1, что даёт 128 · 32 · 4096 = 16 MiB на хеш. Это в точности «large and slow» из рекомендаций автора, где он оценивается как сопоставимый с bcrypt cost 2⁸: латентность 10–30 мс и больше тысячи хешей в секунду на шестнадцатиядерном сервере.

Обратная сторона названа самими авторами и в статье замалчиваться не должна: yescrypt сложен («higher risk of human error occurring and remaining unnoticed for long»), небезопасен по кэш-таймингу, как scrypt и Argon2d, не является победителем PHC и поддерживается в заметно меньшем числе сторонних проектов. Для выделенного сервера аутентификации первые два пункта почти не весят, для библиотеки общего назначения — весят сильно.

bcrypt: почему держится и где кончается

bcrypt (Провос и Мазьер, USENIX 1999) устроен вокруг блочного шифра Blowfish с намеренно дорогим ключевым расписанием. Его живучесть объясняется одной фразой из оригинальной работы: «the eksblowfish S-Boxes require 4 KB of constantly accessed and modified memory. Thus, the S-Boxes cannot be shared across key schedules — separate S-Boxes must exist for every simultaneous execution. This vastly limits the usefulness of any attempts to pipeline the Feistel network in hardware». Иными словами, bcrypt был память-жёстким до того, как термин появился, — просто с фиксированными и очень маленькими 4 килобайтами.

Отсюда обе его черты. Живучесть: 4 KiB на попытку четверть века мешали строить дешёвые конвейеры. Потолок: 4 KiB прекрасно помещаются в локальную память вычислительного блока современного GPU, а поднять их нельзя — параметра памяти у bcrypt нет вовсе, цена регулируется только числом проходов ключевого расписания.

Второе ограничение — длина, и у него два разных числа. Оригинальная работа говорит о ключе «up to 56 bytes (including a terminating zero byte)», то есть о 55 значащих символах; Персиваль в 2009-м считал именно по этому пределу и объяснял им, почему bcrypt на длинных фразах проигрывает даже PBKDF2: «bcrypt's inability to use more than the first 55 characters of a passphrase». Современные реализации берут до 72 байт, и именно 72 документирует crypt(5). Откуда взялся переход с 56 на 72 и в какой реализации он зафиксирован, подтвердить первоисточником не удалось — 56/55 и 72 — это одно расхождение, а не два, и проходит оно между работой 1999 года и тем, что делают сегодняшние библиотеки. Практически важнее другое: лишнее отбрасывается молча, длинная парольная фраза не даёт никакого выигрыша после порога, и пользователь об этом не узнает.

sha512crypt: upstream-умолчание, которое не аргумент

В самом shadow-utils ENCRYPT_METHOD по умолчанию — SHA512, и это стоит понимать как возраст, а не как рекомендацию. sha512crypt — конструкция Дреппера 2008 года, циклическое применение SHA-512 заданное число раз; умолчание по числу раундов — 5000, и crypt(5) комментирует его без дипломатии: «too low for modern hardware».

Дефекты у него не криптографические, и предложение Fedora перечисляет их списком: эффективно используется около 90 бит соли при рекомендованных NIST ≥128; длинный пароль создаёт отказ в обслуживании по процессору; пассивное наблюдение за временем выполнения позволяет предсказать длину пароля; криптографическая функция вывода ключа не используется вовсе. Ни один из пунктов не делает существующие хеши подделываемыми — и все вместе они означают, что выбирать sha512crypt сегодня не за что.

Кто и где выбирает

Выбор делает не login.defs напрямую: пароли пользователей записывает PAM. pam_unix(8) берёт умолчание из ENCRYPT_METHOD в login.defs, а если переменной нет и опция модулю не передана — использует встроенный SHA512. Как это правят, какие значения принимаются и почему на одной машине соседствуют $6$ и $y$ — в заметке про пользователей и PAM; здесь дальше — что в итоге оказалось умолчанием в разных системах и из-за чего они разошлись.

Что стоит в дистрибутивах на дату сверки — проверялось по исходникам пакетов, а не по обзорам:

Система ENCRYPT_METHOD Чем проверено
upstream shadow-utils SHA512 login.defs(5) в дереве проекта
Debian (shadow 4.19.3-2) YESCRYPT etc/login.defs в исходниках пакета
Fedora 35 и новее YESCRYPT предложение изменения F35
CentOS Stream 10 → RHEL 10 YESCRYPT shadow-utils.login.defs, ветка c10s
CentOS Stream 9 → RHEL 9 SHA512 тот же файл, ветка c9s
Arch (shadow ≥ 4.14.0, сентябрь 2023) YESCRYPT новость дистрибутива

Строка про RHEL 9 против RHEL 10 полезнее остальных: yescrypt в девятке поддерживается, но умолчанием не является, и «поддерживается» с «используется» путают постоянно.

Сравнение с FreeBSD

Во FreeBSD выбор задаётся passwd_format в login.conf, умолчание — sha512. Выбирать login.conf(5) предлагает из пяти значений: des, md5, blf (bcrypt), sha256, sha512. Шестое, nth, есть только в таблице crypt_formats[] в lib/libcrypt/crypt.c и к хранению паролей отношения не имеет — это NT-хеш для совместимости. Ни yescrypt, ни scrypt, ни Argon2 в системной crypt(3) нет. Практический вывод для администратора FreeBSD ровно один: осмысленных вариантов два, и blf из них лучше умолчания по всем разобранным выше причинам, кроме совместимости.


Как выбирают параметры

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

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

Шаг второй — померить на своём железе. Чужие числа здесь бесполезны в точности так же, как в сравнении симметричных шифров: меряется неизвестная машина неизвестной сборкой. Мерить нужно то, что реально будет считать хеш:

# scrypt: N, r, p напрямую; работает в любом OpenSSL 3.x
time openssl kdf -keylen 32 -kdfopt pass:test -kdfopt salt:0123456789abcdef \
  -kdfopt n:65536 -kdfopt r:8 -kdfopt p:1 \
  -kdfopt maxmem_bytes:1073741824 SCRYPT

# Argon2id: нужен OpenSSL 3.2 или новее, memcost в килобайтах
time openssl kdf -keylen 32 -kdfopt pass:test -kdfopt salt:0123456789abcdef \
  -kdfopt iter:2 -kdfopt memcost:19456 -kdfopt lanes:1 -kdfopt threads:1 ARGON2ID

# то, что реально будет стоять в заголовке LUKS2, — с калибровкой под эту машину
cryptsetup benchmark --pbkdf argon2id

Результаты ниже получены 14 сентября 2026 года на виртуальной машине с Intel Xeon 2,8 ГГц (4 vCPU), через hashlib из Python 3.11.15 поверх OpenSSL 3.0.13. Это характеристика одной машины, а не алгоритмов; смысл таблицы в отношении между строками, а не в абсолютных числах. Argon2id здесь не мерился: в OpenSSL 3.0.13 его нет, он появился в 3.2 (ноябрь 2023).

Функция и параметры Время, мс Память на одну попытку
PBKDF2-HMAC-SHA256, 100 000 итераций 68 сотни байт
PBKDF2-HMAC-SHA256, 600 000 итераций 403 сотни байт
PBKDF2-HMAC-SHA256, 1 000 000 итераций 677 сотни байт
scrypt, N=2¹⁴, r=8, p=1 45 16 MiB
scrypt, N=2¹⁵, r=8, p=1 105 32 MiB
scrypt, N=2¹⁶, r=8, p=1 210 64 MiB
scrypt, N=2¹⁷, r=8, p=1 431 128 MiB

Две нижние строки и вторая — это и есть весь сюжет статьи в одном сравнении. При одинаковой цене для защитника (около 0,4 секунды) одна функция требует от атакующего сотни байт на параллельный экземпляр, а другая — 128 мегабайт.

Шаг третий — посчитать, что бюджет даёт атакующему. Считать надо не время, а площадь, и делится она в одно действие: память карты, поделённая на память одной попытки, даёт потолок числа одновременных попыток. При 19 MiB на попытку это около 430 на карте с 8 ГиБ, около 1290 на 24 ГиБ и около 4300 на ускорителе с 80 ГиБ: по всему диапазону от игровой карты до дата-центрового ускорителя счёт идёт на сотни и тысячи, сколько бы вычислительных блоков на них ни стояло. Поднимите память на попытку до 1 GiB — и те же три класса дадут 8, 24 и 80. При PBKDF2 память не ограничивает вовсе, и число параллельных попыток упирается только в то, сколько ядер поместилось на кристалле. Это оценка сверху по одному ресурсу, а не бенчмарк: реальная скорость будет ниже из-за пропускной способности памяти, — но для выбора между «память ограничивает» и «память не ограничивает» её достаточно.

Здесь же становится видно, зачем нужны рекомендованные наборы и почему они так расходятся. RFC 9106 первым вариантом называет Argon2id с t=1, p=4 и m=2²¹, то есть 2 GiB, и предлагает считать это умолчанием для всех сред. OWASP рекомендует минимум 19 MiB при t=2, p=1. Расхождение стократное, и оно не ошибка: RFC отвечает на вопрос «сколько стоит сделать одну проверку максимально дорогой», OWASP — на вопрос «что выдержит сервер, который аутентифицирует людей пачками». Приложенное к трём бюджетам из первого шага это сразу даёт вердикт. Сервер аутентификации с его 40 мс — случай OWASP, и 19 MiB для него не «минимум, от которого расти», а примерно всё, что в бюджет влезает. Разблокировка диска и парольный менеджер с их секундой-двумя — случай RFC: гигабайт-другой там реален, и именно на них набор 2 GiB рассчитан. Промежуточных постановок мало, и если ваша оказалась ровно посередине, то мерить по второму шагу придётся всерьёз.

Шаг четвёртый — заложить, что параметры устареют. Они стареют молча и без вашего участия: и RFC 8018, и SP 800-63B-4 требуют повышать стоимость со временем («it SHOULD be increased over time to account for increases in computing performance»). Для парольных хешей это дёшево — новый параметр применяется при следующей смене пароля, а старые записи продолжают проверяться по своим, потому что параметры лежат в самой строке хеша. Для шифрования по паролю это совсем не дёшево, и о том, почему, — следующий раздел.

Отдельный случай: параметр пришёл от противника

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

Спецификация age разбирает это явно: работать полагается «SHOULD apply an upper limit to the work factor», и эталонная реализация действительно ограничивает принимаемый фактор значением 2²², помеченным в коде как «15s on a modern machine», тогда как при шифровании ставит 2¹⁸ — «1s on a modern machine». Если вы проектируете формат с параметрами в заголовке, потолок при чтении — обязательная часть конструкции, а не украшение.


Соседняя задача: ключ для шифрования из пароля

Постановка меняется в двух местах. Первое очевидно: ключ не проверяется, а используется, и неверный пароль даёт не отказ, а мусор — если формат не позаботился о проверке отдельно. Механизм этой проверки и то, почему она не берётся из AEAD бесплатно, разобраны в статье про AEAD в разделе про обязательство по ключу; спецификация age там же, где запрещает смешивать парольную ступень с другими получателями, ссылается на partitioning oracle.

Второе менее очевидно и важнее для выбора параметров: можно ли их вообще потом поменять. Ответ зависит не от алгоритма, а от того, куда попадает выведенный из пароля ключ.

Две конструкции парольного шифрования: слева выведенный из пароля ключ шифрует сами данные, и параметры KDF записаны в самом артефакте, поэтому смена параметров означает перешифрование всех данных; справа выведенный ключ шифрует только случайный мастер-ключ, лежащий в отдельном слоте хранилища, и смена параметров переписывает один слот, не трогая данные
рассмотреть

В прямой конструкции ключ, выведенный из пароля, и есть ключ данных. Параметры записаны там же, где данные, и меняются вместе с ними: чтобы перейти с фактора 2¹⁸ на 2²⁰, надо перешифровать артефакт целиком. Для однократно записываемого файла это нормально и честно; для растущего хранилища — неприемлемо.

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

Так устроены все три хранилища, которые встречаются в базе:

  • LUKS2. Ключ слота выводится Argon2id — умолчание задано при сборке cryptsetup и в апстриме равно argon2id, 2000 мс на вывод, до 1 GiB памяти и до 4 потоков. Параметры не константы: cryptsetup бенчмаркует машину при создании слота, пытаясь уложить запрошенную память в заданное время и снижая память, если не выходит; при бенчмарке она всегда остаётся в диапазоне от 64 MiB до 1 GiB, выше 1 GiB её можно поставить только явным --pbkdf-memory. Смена параметров — это luksAddKey с новыми значениями и luksKillSlot для старого, что попадает в заметку про восстановление системы с другой стороны: заголовок с этими слотами и есть то, что бэкапят luksHeaderBackup.
  • restic. KDF — scrypt, и параметры не зашиты в код: при создании ключа Calibrate подбирает их под машину с потолком в 500 мс и 60 MiB (KDFMemory = 60 уходит в sscrypt.Calibrate(timeout, memMiBytes, params), то есть считается в мебибайтах), после чего N, r и p записываются в файл ключа рядом с обёрнутым мастер-ключом. Калибровка повторяется на каждом добавлении ключа: параметры принадлежат ключу, а не репозиторию, так что переход на более тяжёлые — это новый ключ, подобранный на текущем железе, и удаление старого. Соседняя заметка про бэкапы разбирает, где живёт пароль и чем repokey отличается от keyfile; здесь же существенно, что обе схемы borg — тоже обёртка.
  • GELI во FreeBSD. Та же архитектура с мастер-ключом и слотами, но парольная ступень — PBKDF2 (PKCS#5v2), и число итераций по умолчанию подбирается под две секунды работы. Бюджет тот же, что у LUKS2, а поколение — первое: памяти в этой конструкции нет. Это, пожалуй, самая наглядная иллюстрация того, что выбор KDF определяется не задачей, а возрастом формата.

Артефакты без слотов ведут себя противоположно. Парольный получатель age — это строка -> scrypt <соль> <log2 N> в заголовке файла, r=8, p=1 фиксированы спецификацией, соль в 16 байт своя на каждый файл. Фактор 18 при r=8 — это 256 MiB и около секунды, что для однократного шифрования файла ровно правильно, а поменять его в уже записанном файле нельзя никак, кроме как перешифровав.

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


Что здесь устареет первым

Рекомендованные параметры — быстрее всего. Числа OWASP пересматриваются регулярно, значения RFC 9106 зафиксированы в документе 2021 года и пересматриваться не будут вовсе, так что расхождение между ними со временем будет только расти. Перемерять надо на своей машине, а не переносить.

Статус SP 800-132. Решение о пересмотре принято в 2023 году, черновика на дату сверки нет. Когда он выйдет и одобрит память-жёсткую функцию, раздел про PBKDF2 и весь довод «только под FIPS» устареют разом — и это единственное место в статье, где вердикт может перевернуться целиком.

Умолчания дистрибутивов. Таблица выверена по исходникам пакетов на дату в шапке; строка RHEL 9 против RHEL 10 показывает, с какой скоростью это меняется. Проверяется одной командой — grep ENCRYPT_METHOD /etc/login.defs — и верить надо ей, а не статье.

Измерения. Таблица получена на одной виртуальной машине через OpenSSL 3.0.13; на другом железе изменятся все абсолютные значения, а отношение «PBKDF2 против scrypt по памяти» — нет, потому что оно следует из устройства функций.


Источники

  • RFC 8018 — PKCS #5 v2.1: назначение соли и рекомендация «at least eight octets» на её длину (§4.1), прирост стойкости на log2(c) бит и рекомендации по числу итераций со ссылкой на SP 800-132 (§4.2), определение PBKDF2 (§5.2)
  • RFC 7914 — scrypt: ROMix, BlockMix, параметры N, r, p, тестовые векторы
  • RFC 9106 — Argon2: различие 2d, 2i и 2id (§1, §3), процедура выбора параметров и оба рекомендованных набора (§4), размены «время против памяти» с числовыми оценками для каждого варианта (§7.2), формулировка рационального защитника (§7.3), итоговые рекомендации (§7.4)
  • NIST SP 800-132 — действующая редакция 2010 года и пометка о планируемом пересмотре (11 мая 2023); черновика на дату сверки нет
  • NIST SP 800-63B, редакция 4 (26 августа 2025) — требования к хранению паролей: соль не короче 32 бит, отсылка к SP 800-132, повышение стоимости со временем, keyed-хеширование секретным ключом с хранением ключа в HSM или TEE
  • crypt(5) из libxcrypt — три задачи соли, полный список поддерживаемых методов и их статус, диапазоны cost-факторов, предел bcrypt в 72 символа, «too low for modern hardware» про 5000 раундов sha512crypt
  • libxcrypt, lib/crypt-yescrypt.c — отображение cost-фактора в N и r: умолчание 5, диапазон 1–11, при cost ≥ 3 берётся r=32
  • yescrypt: README — почему память-жёсткость при низкой задержке почти не выигрывает у bcrypt, ROM, список дистрибутивов с yescrypt по умолчанию, версия 1.1.0
  • yescrypt: COMPARISON — сравнение с scrypt и Argon2: 96 последовательных умножений на килобайт против 16, низкоуровневый параллелизм Argon2, TMTO-дружелюбность scrypt, собственные недостатки yescrypt
  • yescrypt: PARAMETERS — рекомендованные наборы для аутентификации: 2 MiB и 16 MiB, их соответствие bcrypt cost 2⁵ и 2⁸
  • N. Provos, D. Mazières, A Future-Adaptable Password Scheme (USENIX 1999) — eksblowfish: 4 KB постоянно изменяемой памяти, невозможность разделить S-боксы между вычислениями, ключ до 56 байт
  • C. Percival, Stronger Key Derivation via Sequential Memory-Hard Functions (BSDCan'09) — доллар-секунды как единица стоимости, удешевление атаки при постоянном времени проверки, таблица 1 со стоимостью оборудования, состав её колонок («8 letters» против «8 chars») и оговорки к ней, обрезка длинных фраз в bcrypt на 55 символах
  • J. Bonneau, The Science of Guessing (IEEE S&P 2012) — оценка стойкости пользовательских паролей: менее 10 бит против онлайновой атаки, около 20 против оптимальной офлайновой
  • Password Hashing Competition — сроки конкурса, победитель (июль 2015) и четыре финалиста с особым упоминанием
  • OWASP Password Storage Cheat Sheet — параметры Argon2id (19 MiB, t=2, p=1 и эквивалентные наборы), scrypt, bcrypt как легаси, 600 000 итераций PBKDF2 под требование FIPS, обе стратегии перца и рекомендация внутреннего варианта bcrypt(base64(hmac-sha384(...))) против password shucking
  • Fedora: yescrypt as default hashing method for shadow — четыре дефекта sha256crypt и sha512crypt, три довода в пользу yescrypt против Argon2, список затронутых пакетов
  • Arch Linux: changes to default password hashing algorithm (22 сентября 2023) — смена умолчания в shadow 4.14.0 и довод про отсутствие Argon2 в libxcrypt
  • Debian, исходники shadow 1:4.19.3-2 — etc/login.defs с ENCRYPT_METHOD YESCRYPT
  • CentOS Stream, shadow-utils — shadow-utils.login.defs: YESCRYPT в ветке c10s, SHA512 в ветке c9s
  • login.defs(5) в дереве shadow-utils — значения ENCRYPT_METHOD с умолчанием SHA512, диапазоны YESCRYPT_COST_FACTOR (1–11), BCRYPT_MIN_ROUNDS (4–31) и SHA_CRYPT_MIN_ROUNDS
  • pam_unix(8) — умолчание берётся из ENCRYPT_METHOD, при отсутствии переменной — встроенный SHA512
  • glibc 2.38, NEWS — libcrypt больше не собирается по умолчанию, нужна опция --enable-crypt
  • glibc 2.39, объявление о релизе — удаление libcrypt и опций --enable-crypt, отсылка к libxcrypt
  • cryptsetup, man/common_options.adoc — бенчмарк параметров при создании слота, минимальные и максимальные стоимости, диапазон памяти при бенчмарке от 64 MiB до 1 GiB
  • cryptsetup, meson_options.txt — умолчания сборки для LUKS2: argon2id, 2000 мс, 1 048 576 кБ памяти, 4 потока
  • restic, internal/repository/crypto/kdf.go и internal/repository/key.go — scrypt с калибровкой при создании ключа, потолки 500 мс и 60 MiB (memMiBytes), параметры в файле ключа
  • Спецификация формата age — парольная строка scrypt, фиксированные r=8 и p=1, требование ограничить фактор при чтении, запрет смешивать её с другими получателями
  • age, scrypt.go — фактор 18 при записи и потолок 22 при чтении с комментариями о времени работы
  • geli(8) — PKCS#5v2 для парольной части пользовательского ключа и подбор числа итераций под две секунды
  • login.conf(5) во FreeBSD — passwd_format с умолчанием sha512 и пять допустимых значений: des, md5, blf, sha256, sha512
  • FreeBSD, lib/libcrypt/crypt.c — таблица crypt_formats[]: те же форматы плюс nth (NT-хеш, префикс $3$)
  • OpenSSL 3.2, release notes — появление Argon2 в качестве KDF (23 ноября 2023)

Связанные заметки

Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.

Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.