Сегодня посмотрим, как лучше всего хранить пароли в базе данных и как известные платформы решают эту задачу.

Plaintext


Когда встал вопрос хранения паролей, конечно, первой идеей было просто записывать их в открытом виде в соответствующей табличке в базе данных. И все бы ничего, если бы доступ к ней действительно напрямую клиенты получить не могли. Но, к сожалению, в различных веб-приложениях по-прежнему иногда работает такая известная всем SQL-инъекция, не говоря уже о других потенциальных уязвимостях. В вопросах безопасности вообще принято предполагать худшее и готовить план действий и защиту даже на такой случай. Будем считать, что злоумышленник нашел в веб-приложении лазейку, тем или иным способом радостно выгружает себе таблицу с именами и паролями пользователей и дальше уже распоряжается ими, как ему вздумается. В общем случае его дальнейшие действия могут быть следующими:

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

Шифрование Хэширование


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

  1. Трудоемкость. Шифрование занимает больше времени, а какое преобразование мы бы ни выбрали, его придется проделывать при каждой проверке пароля. Одним из требований к хэш-функциям же является быстрота выполнения.
  2. Длина выходных значений. Результат шифрования имеет переменную длину, результат хэширования – всегда одинаковую, а хранить однородные по размеру данные в базе данных очень уж удобно. Не говоря уже о том, что длина пароля в зашифрованном виде будет давать некоторую информацию о длине исходного пароля. Одинаковая длина, правда, приводит к возможности возникновения коллизий, но об этом ниже.
  3. Управление ключами. Для шифрования требуется ключ, который тоже где-то придется хранить и надеяться, что его никто не найдет. В любом случае, генерация и управление ключами это отдельная история (они не должны быть слабыми, их нужно регулярно менять и так далее).
  4. Возможность коллизии. При шифровании выходные данные от различных входных даных всегда тоже будут различны. При хэшировании же это не всегда так. Постоянная длина хэша означает ограниченность множества выходных значений хэш-функции, что приводит к возможности коллизии. То есть, допустим, пользователь действительно заморочился и придумал себе по-настоящему классный длинный пароль, в котором есть и спецсимволы, и цифры, и буквы в нижнем и верхнем регистре. Злоумышленник вводит в поле пароля не менее классный пароль “admin”. Сервер для проверки и сравнения хэшей захэшировал его. Хэши совпали. Обидно.

Таким образом, со счетом 3:1 побеждает хэширование. Но можно ли на этом остановиться?
Ответ: нет.

Атаки на хэшированные пароли


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

  • брутфорс по словарю: если с эталонным паролем администраторов у злоумышленника ничего не вышло, он обратится к словарю популярных паролей и попытает счастья с их хэшами;
  • радужные таблицы: вообще сегодня ему, может, не надо будет совсем ничего вычислять и перебирать по словарю. Достаточно будет обратиться к лежащим в сети радужным таблицам. В радужных таблицах содержатся уже вычисленные кем-то до этого хэш-значения и соответствующие им входные данные. Важно отметить, что в силу коллизий, пароль, который предложит радужная таблица, не обязательно будет именно тем, который использует пользователь. Предвычисленные значения есть уже для MD5, SHA1, SHA256, SHA512, а также для их модификаций и некоторых других. Попробовать обратить хэш можно, например, здесь;
  • полный перебор: если не поможет и это, придется прибегнуть к брутфорсу и перебирать подряд все возможные пароли, пока вычисленные хэши наконец не совпадут.

В самом общем случае злоумышленнику придется брутить пароли. И тут его успех будет зависеть в том числе от быстроты вычисления хэш-функции. Сравнение по времени работы хэшей можно посмотреть здесь. Например, реализованные на Java хэш-функции на 64-битной Windows 10 с 1 core Intel i7 2.60GHz и 16GB RAM были запущены по миллиону раз для вычисления хэша длины в 36 символов. Они показали следующие результаты:

MD5 – 627 мс
SHA-1 – 604 мс
SHA-256 – 739 мс
SHA-512 – 1056 мс

А ведь сегодня брутфорс можно распараллелить и выполнить в разы быстрее на GPU (а также на APU, DSP и FPGA). Однако помимо выбора более долгого алгоритма и более длинного выходного результата можно сделать кое-что еще.

Хэширование хэша


Чтобы помешать нарушителю воспользоваться готовыми радужными таблицами, существует техника хэширования пароля несколько раз. То есть вычисляем хэш от хэша от хэша от хэша… и так n раз (надо, правда, сильно с этим не увлекаться, потому что при обычной проверке пароля пользователя серверу тоже придется это проделывать). Теперь так просто по радужной таблице он пароль не найдет, да и время на брутфорс заметно увеличится. Но ничто не остановит злоумышленника от того, чтобы сгенерировать радужную таблицу по словарю паролей, зная алгоритм хэширования. Тем более, для самых популярных комбинаций этого метода такие таблицы уже сгенерированы:

"

Добавить соль по вкусу


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

1. По принципу соления:

  • уникальная соль для каждого пользователя: индивидуальная для каждого пользователя – таким образом, если соль станет известна злоумышленнику, брутить придется пароль каждого по отдельности. И кроме того, даже если два пользователя мыслят одинаково и придумали идентичные пароли, хэши все равно на выходе будут разными;
  • глобальная соль: одинакова для всех, используется для всех хэшей;
  • и то, и другое.

2. По методу хранения соли:

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

Будем считать, что индивидуальные соли пользователей хранятся в базе, глобальная соль в конфиге. Злоумышленник получил доступ к базе, и ему известны все хэши и соответствующие им соли (глобальная соль хранится не в базе, и ее он не знает). Итого, если объединить все способы, то для того, чтобы получить пароли в открытом виде, как было в первых системах, он, будучи крайне целеустремленным, столкнется со следующими препятствиями:

  1. Ему неизвестна глобальная соль, поэтому ее придется брутить.
  2. Ему известны соли пользователей, но заготовленных таблиц с этими солями у него нет, поэтому пароли придется брутить.
  3. Процесс этот займет еще больше времени из-за того, что придется хэшировать хэши по n раз.

Как хранят пароли различные CMS


Wordpress


До версий 3.х пароли просто хэшировались с помощью MD5. Сейчас используется библиотека phpass. По умолчанию к паролю спереди приписывается соль и полученная строка хэшируется MD5 2^8 раз.

Joomla


До версии 1.0.12 использовался просто MD5. Используется библиотека phpass, по умолчанию bcrypt с солью и 2^10 повторениями.

Drupal


До версии 6 md5 без соли. Используется библиотека phpass. По умолчанию соленый sha512 с 2^16 повторениями.

Silverstripe


Использует соленый Blowfish c 2^10 повторениями.

Umbraco


Использует HMACSHA256 с солью. Использует вторую, глобальную соль, задаваемую в конфиге.

Комментарии (36)


  1. andreymal
    04.06.2018 12:10

    Без описания PBKDF2, bcrypt (хотя упоминание есть), scrypt, Argon2 пост выглядит неполным


    Ему неизвестна глобальная соль, поэтому ее придется брутить.

    Но ещё есть вариант, когда конфиг с солью тоже сливается через какую-нибудь RCE-уязвимость (или даже тупо по недосмотру с неправильно настроенными правами на файл, пару раз и на такое натыкался)


    1. vladbarcelo
      04.06.2018 17:21
      +1

      Не сказано про методики на уровне архитектуры — создание небольших микросервисов с задачами "регистрация/логин/удаление", аудит которых прост и дёшев, а выкачивание базы хешей соответственно требует взлома микросервиса, доступного извне, и постоянных запросов к этому микросервису авторизации (привет сетевым экранам и эвристическому анализу трафика между микросервисами). Плюс — hash blinding каким нибудь hmac перед внесением в базу с хранением ключа от блиндинга в HSM.


      meh.


    1. gto
      05.06.2018 12:50

      Еще есть вариант когда уволенный итишник забирает с собой и глобальный конфиг и дб.


  1. dmitry_dvm
    04.06.2018 12:46
    -2

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

    Очень плохо, что в 2018 году это все еще первая идея.
    В мире .net есть такая штука как Identity. Там сразу и хранение и изменение и бан. Рекомендую обратить внимание, если гвоздями не прибиты к php.


    1. Acribia Автор
      04.06.2018 12:49
      +2

      Это не первая идея в 2018 году, это первая идея исторически.


  1. martin_wanderer
    04.06.2018 12:57

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

    В то же время есть усовершенствованный вариант хэширования хэша: hash(перец+hash(pass+соль)). Где соль уникальна для каждого пользователя, а перец — для каждой попытки авторизации, и выдается сервером.


    1. lair
      04.06.2018 13:26

      Гм, а как ваш "усовершенствованный вариант" применим к хранению в БД?


  1. Melkij
    04.06.2018 13:08
    +1

    Одним из требований к хэш-функциям же является быстрота выполнения.

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

    Если уж говорить про php — то из-за безалаберного отношения к паролям в ядро давно уже включили обёртку password_hash. И использовать надо именно это.


    1. maximw
      04.06.2018 16:25

      Строго говоря, все же есть требованиек быстроте: она не должна быть слишком быстрой.


      1. Melkij
        04.06.2018 17:16
        +2

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


  1. SONce
    04.06.2018 13:40

    Зачем вообще хранить пароли? Давным давно уже придумали SRP-6a


  1. miksir
    04.06.2018 13:53
    +2

    Несколько дополнений.


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


    2. Как уже сказали, пропущен важный момент, почему bcrypt и argon2, а не, например, sha256 с кучей итераций. Как было упомянуто, что есть подбор на GPU и подобном. Вот эти алгоритмы и пытаются "ставить палки в колеса" таким подборам за счет повышения затрат на оперативную память (например, у многоядерных GPU память каждого ядра очень мала, а если каждое ядро начнет обращаться к общей памяти, то скорость подбора стает тех же порядков, что и у CPU).

    Ну и 3. По поводу придумывания своих алгоритмов, хешей от хешей и т.п. Не делайте так. В криптографии много нюансов, о которых вы не знаете. Например, Timing attack. Используйте проверенные инструменты, предназначенные именно для хеширования и проверки паролей.


  1. nikitasius
    04.06.2018 14:40

    hmac384 или sha384(пароль+глобальное чтонить). Про потери перфоманса… только на тестах в вакууме.


  1. BOM
    04.06.2018 17:43

    Разве многократное хеширование не увеличивает вероятность коллизий?


    1. Acribia Автор
      04.06.2018 18:02
      +1

      Многократное хэширование увеличивает вероятность коллизий, но это не критично и многие его используют.
      Об этом можно прочитать здесь: habr.com/post/100301


      1. BOM
        04.06.2018 19:11

        Всегда интересовала эта тема. Спасибо за ссылку.


  1. Nidere
    05.06.2018 00:28

    Реально хэшируют 2^8 раз подряд? О_о
    Это действительно помогает?

    Я для своего сервиса сделал просто хэш(индивидуальная_соль+хэш(хэш(имя_пользователя)+хэш(пароль_пользователя)))
    Неужели этого недостаточно?


    1. andreymal
      05.06.2018 00:38

      Смотря какой хэш. Но, подозреваю, такой, что будет перебираться на хорошем GPU очень быстро.


      Для сравнения: в Django по умолчанию используется PBKDF2, в котором SHA256-хэш хэширует 150000 (прописью: сто пятьдесят тысяч) раз подряд


      1. Nidere
        05.06.2018 00:40

        Простите, но зачем?..


        1. andreymal
          05.06.2018 00:45

          Чтобы на GPU не перебрали, очевидно же :)


          Я тут по случайному совпадению запустил перебор одного md5-хэша за пару дней до этого поста — на дохлом ноутбучном GPU. 8 и 9 символов уже перебраны, 10 символов переберутся через полтора месяца. Если обзавестись хотя бы двумя хорошими нвидиями, можно будет и 10 символов за пару дней перебрать) И это если не вспоминать пришедшие из мира криптовалют ASIC и FPGA, которые заточены именно для такого перебора (только в контексте всяких там блокчейнов, но по сути то же самое).


          А соль и имя пользователя никак не затруднят перебор — они же заранее известны, и злоумышленник исследователь просто сразу их подставит в алгоритме, перебирая только собственно пароль


          1. Nidere
            05.06.2018 00:48

            А соль и имя пользователя никак не затруднят перебор — они же заранее известны, и злоумышленник исследователь просто сразу их подставит в алгоритме, перебирая только собственно пароль


            Только если знает алгоритм конкатенации промежуточных строк, да?


            1. andreymal
              05.06.2018 00:50

              Безопасность через неясность это плохо, стоит автоматически предполагать, что вместе с базой слит и код сайта, в котором есть этот алгоритм (к тому же мне самому доводилось сливать код сайтов пару раз, хех)


              1. Nidere
                05.06.2018 00:52

                А если не сайт, а сервер игры? Хостящийся на амазоне.

                Извиняюсь, я просто в теме профан, но в качестве хобби пилю клиент-серверную игру, и вот как раз недавно делал систему авторизации)


                1. andreymal
                  05.06.2018 00:57

                  А в чём принципиальная разница-то? С точки зрения сливов сервер игры тоже не обязательно безопасен и теоретически может обеспечить слив через какую-нибудь дырку. Разница разве что в нагрузке алгоритма на сервер, но у PBKDF2 даже с 150000 итераций нагрузка всё равно небольшая — это не какой-нибудь Argon2, который придуман специально чтобы выжирать и проц, и оперативку)) (а придуман он тоже именно для того, чтобы затруднить перебор на GPU)


                  Ну или можно включить режим неуловимого Джо и заявить что-то вроде «да кому нахрен нужен этот мой сервер игры, никто ничего подбирать не будет». Именно по этому принципу на двух моих старых сайтах до сих пор используются несолёные md5 и sha1 :D


                  1. Nidere
                    05.06.2018 01:00

                    Спасибо большое, изучу тему поглубже)


            1. andreymal
              05.06.2018 00:52

              Хотя с другой стороны vladbarcelo в комментах выше предлагает вынести аутентификацию в микросервис, и это в принципе может немного защитить как базу, так и алгоритм. Но зачем, когда можно просто взять хороший алгоритм, который достаточно надёжен, даже если он всем известен? Тот же PBKDF2 хотя бы


          1. freeExec
            05.06.2018 10:32

            никак не затруднят перебор

            Разница очевидна если конечная цель подобрать пароли пользователей. Сравните:
            ХЕШ("12345") -> нашли 29 пользователей с таким паролем.
            ХЕШ("12345" + ИНДИВ.СОЛЬ) -> нашли 1 пользователя с таким паролем. Но ещё нужно обнаружить 28 пользователей с таким же паролем, но другим итоговым хешем.


            1. andreymal
              05.06.2018 12:02

              Всё так, но если в качестве хэша используется простой md5 или типа того, то точно так же перебрать остальных пользователей займёт миллисекунды, даже если их миллионы) А ещё это всё будет не иметь значения, если целью является подбор пароля одного конкретного пользователя или поиск наибольшего числа пользователей с паролями вида 123456 (перебрать всех по словарю не так уж долго даже с солью)


              1. freeExec
                05.06.2018 12:52

                Вы опять не уловили разницу. У вас есть словарь из 1000 слов и миллион пользователей. В одном случае для полного перебора надо сгенерировать 1000 хешей, в другом 1.000.000.000 (миллиард Карл!).


                1. andreymal
                  05.06.2018 12:53

                  Моя ноутбучная видеокарта считает 2.000.000.000 md5-хэшей в секунду. Два миллиарда в секунду, Карл! Ноутбучная, Карл!


        1. lair
          05.06.2018 00:45

          Чтобы вычислительную сложность повысить.


  1. gasizdat
    05.06.2018 19:33

    Интересно, если хранить в БД не соленые многораундные хэши, а зашифрованную пару соль+хэш пароля. Можно также делать многораундное шифрование. Можно даже шифровать ассиметрично и хранить приватный ключ рядом с БД.
    Плюсы: 1) асиметричный шифр более ресурсоемкий, чем любой хэш, и плохо брутится. 2) По определению нет радужных таблиц. 3) Даже, если сольют базу и приватный ключ, в лучшем случае получат хэш пароля и соль


    1. andreymal
      05.06.2018 20:16

      Что-то я не понял, если при сливе всё равно «получат хэш пароля и соль», то какой смысл в шифровании?


      1. gasizdat
        05.06.2018 21:03

        Во-1х, слить нужно и БД и приватный ключ, что гораздо-гораздо сложнее. Например, ключ может располагаться на физическом токене с крипточипом. Во-2х, даже если это каким-то образом удастся, остается последний эшелон защиты — многораундное шифрование соленого хэша с его же солью. Ассиметричный шифр на GPU уже не погоняешь, таблицы не подходят, остается только жесткий брут на CPU.


        1. andreymal
          05.06.2018 21:11

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


          1. gasizdat
            05.06.2018 21:36

            Не спорю, но тут вроде статья и обсуждение про способы хранения паролей в БД. А так-то, конечно, можно придумать что угодно другое.