Введение
В этом учебном материале мы рассмотрим концепцию salt/соли и pepper/перца, применяемую при хешировании паролей. Мы разберём, что это такое, как их использовать и какие преимущества они дают.
Как и всегда, говоря о безопасном хранении паролей, не следует пытаться реализовывать собственный алгоритм. Существуют стандартные алгоритмы, которые тщательно протестированы и уже поддерживают всё необходимое. В них, где это уместно, уже предусмотрена поддержка соли и перца, поэтому нам никогда не нужно реализовывать эти механизмы самостоятельно поверх того, что уже предоставляется.
Хеширование паролей
При хранении паролей есть два важных требования, которые необходимо учитывать:
-
Мы должны иметь возможность доказать, что заданный пароль совпадает
-
Злоумышленник, получивший доступ к сохранённым паролям, не должен иметь возможности определить сами пароли
Чтобы этого добиться, мы храним пароли в виде криптографических хешей. Это математические функции, для которых один и тот же вход всегда даёт один и тот же выход, но по одному лишь выходу невозможно определить исходные данные.
Если невозможно обратить хеш — то есть восстановить исходный пароль, имея только хешированное значение, — тогда злоумышленник не сможет определить исходный пароль, если у него есть только хеши. Поэтому, чтобы проверить корректность пароля, нужно снова получить тот же хеш и сравнить результаты: если они совпадают, значит исходные входные данные были одинаковыми.
Например, если использовать (небезопасный) алгоритм хеширования MD5, пароль «Baeldung» будет храниться как «d58b26d9f7600672080ebf3851115e45». Каждый раз при хешировании этого пароля мы будем получать один и тот же результат, но если вход слегка изменится, выход станет совершенно другим.
Атаки перебором
Если мы не можем обратить алгоритм хеширования, то у злоумышленника, получившего список хешированных паролей, остаётся только один способ определить исходные пароли — перебор (brute force). Он заключается в том, что перебираются все возможные пароли, для каждого вычисляется хеш и сравнивается с целевым. Исходный пароль будет найден, когда мы получим соответствующий хеш.
Например, если мы знаем, что используется MD5 и что пароли имеют длину 8 символов, то перебор может выглядеть так:
«aaaaaaaa» = «3dbe00a167653a1aaee01d93e77e730e» = Совпадения нет«aaaaaaab» = «2125ea8b81bc0ab7a16e47ca82c06735» = Совпадения нет…..«Baeldunf» = «3d0e01e3f9d9562e48fa87ec1866eb2a» = Совпадения нет«Baeldung» = «d58b26d9f7600672080ebf3851115e45» = Совпадение
Мы восстановим исходный пароль, когда сгенерируем тот же самый хеш.
Время, необходимое для перебора пароля, зависит от многих факторов, включая алгоритм хеширования и сложность пароля. Например, недавно опубликованная Hive Systems таблица паролей предполагает, что перебор пароля может занять от секунд до миллиардов лет — в зависимости от длины пароля и набора возможных символов.
Радужные таблицы (Rainbow Tables)
Ограничивающим фактором скорости перебора пароля является вычисление хешей. Алгоритмы хеширования паролей специально проектируются так, чтобы выполняться дорого по ресурсам, поэтому перебор — медленный процесс.
Однако это можно частично компенсировать, если заранее сгенерировать все возможные хеши. Если мы знаем все возможные пароли — исходя из алгоритма хеширования, длины и набора допустимых символов, — то можем заранее вычислить все хеши. Это называется радужной таблицей (Rainbow Table).
Такие таблицы получаются большими, но управляемыми на современных компьютерах. Например, таблица всех возможных 8-символьных буквенно-цифровых паролей будет содержать 218,340,105,584,896 строк. Хотя это очень большое число, оно всё же не выходит за пределы возможностей достаточно крупной базы данных, способной управлять таким количеством строк.
После этого мы можем «обратить» хеширование просто поиском нужного хеша в таблице и получить исходный пароль. Это означает, что даже самые сложные пароли можно взломать за секунды, а не за годы.
Если мы можем атаковать один пароль такими методами, то точно так же можем атаковать и множество паролей. Поскольку один и тот же вход даёт один и тот же хеш, мы знаем: если один и тот же хеш встречается несколько раз, то ему соответствует один и тот же входной пароль. Также мы знаем, что если мы определили входные данные для одного из таких хешей, то фактически определили их для всех.
Соление паролей
Главная причина эффективности радужных таблиц — в том, что один и тот же вход всегда даёт один и тот же хеш. Чтобы защититься от этого, нам нужен способ заставить один и тот же пароль давать разные хеши, при этом сохранив возможность сравнивать хеши при проверке пароля.
Для этого применяется salting. Оно заключается в том, что перед хешированием к паролю добавляется дополнительное случайное значение — соль (Salt). У каждого сохранённого пароля будет своя соль. Тогда одинаковые пароли получают разные соли и, следовательно, дают разные хеши:
algorithm HashPassword(password): salt <- generateRandomString() result <- hash(password + salt) return (result, salt)
Затем нам нужно хранить в базе данных и хеш пароля, и соль. Например:
Пароль = «Baeldung», Соль = «123», Хеш = «f78bc83644f9ae7b45e9ad936f1c70f7»Пароль = «Baeldung», Соль = «987», Хеш = «586eccfc7ca25e77823556491021a6d7»
Здесь хорошо видно, что один и тот же пароль с разной salt даёт совершенно разные хеши.
Проверить пароль можно, объединив введённый пароль с сохранённой солью, вычислив для этого хеш и сравнив его с сохранённым хешем:
algorithm VerifyPasword(password, storedSalt, storedHash): newHash <- hash(password + storedSalt) return newHash = storedHash
Поскольку у каждого сохранённого пароля будет своя соль, один и тот же пароль будет иметь разный хеш. В частности, это делает радужные таблицы значительно менее эффективными. Нам потребовалась бы таблица для каждой комбинации пароль + соль. При достаточно большой соли радужная таблица становится невозможных размеров: 64-битная соль означает, что для каждого пароля таблица должна содержать 1.845 10^19 строк, то есть радужная таблица всех возможных 8-символьных буквенно-цифровых паролей теперь потребует 4.028 10^33 строк.
Перчение паролей
Salting паролей эффективен против радужных таблиц, но всё равно не останавливает злоумышленников от перебора паролей. Если атакующий скомпрометирует нашу базу паролей, у него будет доступ и к хешам, и к солям, так что он всё ещё может проводить перебор так же, как раньше.
Перчение (peppering) основано на том же принципе, но здесь используется один общий секрет, который добавляется к паролю. Это значит, что нам не нужно хранить это значение рядом с паролями, а можно держать его отдельно в защищённом виде — например, только внутри исходного кода приложения.
Поскольку каждый пароль комбинируется с одним и тем же значением перца, одинаковые пароли всё равно будут давать одинаковые хеши. Поэтому рекомендуется также солить пароли, чтобы решать и эту проблему.
const pepper <- "someRandomValue"algorithm HashPassword(password): salt <- generateRandomString() result <- hash(password + pepper + salt) return (result, salt)
Добавление перца означает, что даже если у злоумышленника есть хеш и соль, у него всё равно не будет достаточно информации, чтобы легко сварить суп восстановить исходный пароль. Как и прежде, чем больше значение перца, тем больше вариантов придётся перепробовать злоумышленнику, чтобы получить тот же хеш; кроме того, ему потребуется способ определить, какие части входа были паролем, а какие — перцем.
Например, если атакующий знает, что хеш равен «a7e27d2fc90a1ddd5a102f2bcb5b9d4a» и что соль равна «123», он может в итоге определить, что было хешировано значение «Baeldung987123». Затем он может удалить соль с конца и получить «Baeldung987». Но на этом этапе он уже не знает, какие части этой строки — пароль, а какие — перец.
Если перец достаточно большой — например, 64-битное значение, — это добавит паролю достаточно сложности, чтобы перебор занял неразумно много времени. В результате злоумышленник может так и не суметь определить входные данные, которые породили имеющийся у него хеш.
Однако из-за особенностей работы значения перца гораздо более чувствительны. Их невозможно изменить, не заставив каждого пользователя сбросить пароль, и если перец будет скомпрометирован, злоумышленник узнает значение, применённое ко всем паролям в базе данных.
Комментарий от Михаила Поливаха
Тут дело даже не в этом. Peppering паролей считается довольно гиблым делом часто по ряду причин. Одна из них, например, тот факт, что если вы вдруг потеряли или утекли куда-то pepper, то вам приходиться делать массовый reset всех пользователей в системе, т.к. проверить их authenticity либо не представляется возможным, либо, в случае компрометирования pepper, бессмысленным.
Перчение паролей также является менее распространённой практикой, и используемый алгоритм хеширования может не поддерживать её. Например, Argon2 полностью поддерживает и соление, и перчение, тогда как такие алгоритмы, как PBKDF2, BCrypt и SCrypt — нет. Это означает, что если нам нужно использовать один из них, то мы не можем также применять перец в хешировании.
Заключение
В этой статье мы рассмотрели соление и перчение паролей, принцип их работы и преимущества использования. В следующий раз, когда вам понадобится безопасно хранить пароли, почему бы не проверить, подойдут ли эти подходы вам?

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
ссылка на оригинал статьи https://habr.com/ru/articles/1038478/