Хранение паролей и авторизация пользователей

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

Постановка задачи

Все дальнейшее относится к методам реализации контроля доступа, основанных на знании клиентом некоторой ключевой информации (пароля). Использование криптографических ключей, как, например, сертификатов ЭЦП, отдельный разговор, выходящий за рамки этой статьи.

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

  1. никакая хранящаяся на сервере (или где бы то ни было, кроме памяти или сейфа клиента) информация не должна сама по себе позволять пользователям авторизовываться (т.е. нельзя ни хранить пароли в открытом виде, ни использовать такие методы авторизации, в которых можно обойтись хранящейся информацией вместо пароля);
  2. никакие передаваемые от клиента данные не должны позволять авторизовываться в дальнейшем (т.е. нельзя передавать ни пароли в открытом виде, ни такие их производные, которые позволят повторить авторизацию не зная пароля). Задача актуальная только в случае если клиентская и серверная части системы разделены между собой;
  3. подбор необходимых для авторизации данных, исходя из хранящейся и/или передаваемой информации, должен быть максимально затруднен (что особенно относится к автономным системам, где у злоумышленника могут быть в наличии дни, и даже недели и месяцы для подбора, причем максимально эффективного, не сдерживаемого искусственно серверными механизмами);
  4. метод хранения должен быть масштабируемым по сложности подбора, для обеспечения надежности с учетом роста производительности компьютеров, а также особенностей конкретного применения;
  5. Для пользователей: даже не пытайтесь придумывать кучи паролей самостоятельно, и не пользуйтесь мнемоническими схемами, а уж тем более не записывайте пароли в блокнотах (ни в электронные, ни в бумажные). Установите программку для хранения паролей в зашифрованном виде типа keepass (но тысячи их), придумайте один действительно длинный и хороший пароль для нее, а все остальное доверьте програмке. И не забывайте резервные копии ее базы.

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

  6. опционально: позволить клиенту убедиться в том, что сервер тоже знает пароль, соответственно что это не подставной сервер, а настоящий.

Популярные простые методы решения и их недостатки

Очень часто задача решается путем хеширования пароля каким-либо хешем общего назначения (md5, sha1 и т. п. ), и хранения на сервере этого хеша. Это очень плохой вариант, который не удовлетворяет ни одному из предъявленных требований. Может показаться, что уж первому то требованию то удовлетворяет, но на самом деле это не так, и, например linkedin тому подтверждение.

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

Проблемы хеш-функций общего назначения

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

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

Если все-же приходится применять простые соленые хеши, то следует использовать не hash(соль+пароль), а воспользоваться соответствующим HMAC хешем (который по сути представляет из себя hash(соль1+hash(соль2+пароль))).

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

Резюме, хеш-функции общего назначения:

  1. очень быстрые;
  2. требуют мало памяти;
  3. легко реализуются аппаратными средствами.

Специализированные хеш-функции

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

  • просто использовать обычную хеш-функцию многократно (например несколько десятков-сотен тысяч итераций);
  • bcrypt — основанный на blowfish алгоритм, сложность которого задается логарифмическим параметром;
  • scrypt — алгоритм, который кроме большой вычислительной сложности требует еще и параметризуемое количество памяти для работы, что значительно усложняет его реализацию в виде специализированных аппаратных решений;
  • PBKDF2 — алгоритм, базирующейся на произвольной хеш-функции общего назначения, с параметризируемыми размером получаемого хеша и вычислительной сложностью. Рекомендуемый NIST способ, но, из-за частого отсутсвия быстрой реализации средствами языка, редко применим на практике.

Эти варианты реализованы в стандартной для многих языков/систем фунции crypt (за исключением разве что scrypt и PBKDF2, которые если и реализованы, то отдельно). При этом наиболее типовой вариант — bcrypt.

Относительно параметров сложности хеша:

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

О соли

Соль, как известно, представляет собой некоторое количество случайных данных, добавляемых к паролю при хешировании с целью:

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

Поэтому в качестве соли должна выбираться строка во первых уникальная для каждого пользователя, а во вторых случайная, а не базирующаяся на каких либо свойствах пользователя типа его логина. Само собой что меру случайности следует выбирать в разумных пределах в соответствии с особенностями используемого хеша. Также следует учитывать, что base64 кодировка случайных данных содержит больше энтропии, чем hex (6 бит на символ против 4х).

Процесс авторизации

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

  1. Самый простой, и в то же время весьма надежный способ: использовать соединение по зашифрованному каналу: https, TLS и т.п. Но тут очень важна проверка того, что сервер настоящий, а не подставной. Это решается с помощью цепочек доверия сертификатов, и это очень важный момент. Потому что иначе, в случае если сервер будет подменен злоумышленником тем или иным образом, то клиент сам сообщит свой пароль. На этом принципе основано большинство троянов;
  2. Авторизироваться более сложным образом, без передачи авторизационных данных непосредственно, об этом ниже.

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

Защищенная авторизация

Есть множество алгоритмов, позволяющий авторизовываться без непосредственной передачи паролей, например:

  • CRAM-MD5 — основан на том, что клиент передает хеш от пароля плюс случайной величины, генерируемой сервером. Плох тем, что позволяет подобрать пароль после перехвата передаваемых данных, а также не позволяет клиенту удостовериться в аутентичности сервера. Также не дает возможности хранить пароли на сервере в защищенном виде;
  • HTTP Digest — используется в HTTP как защищенный вариант авторизации (по сравнению с Basic), но также имеет аналогичные проблемы: серверу необходимо хранить пароли таким образом, чтобы можно было восстановить сам пароль;
  • SRP — схожий с RSA алгоритм, позволяет обойти все указанные недостатки. На сервере при этом следует хранить производную от хеша пароля, из которой невозможно получить изначальный хеш. Клиент же может хранить у себя сам хеш. Проблема метода только в его сложности: клиенту необходимо уметь самостоятельно считать криптографические хеши паролей, что в случае веб-приложений не самая простая задача (хотя существуют и библиотеки, реализующие bcrypt на javascript, но т.к., как уже выше описано, подобные хеши весьма вычислительно сложные, их реализация на высокоуровневых языках далека от эффективности, и соответственно расчет хеша на слабом компьютере или в глупом браузере может занять продолжительное время). Но тем не менее именно этот алгоритм наиболее безопасен и рекомендуется к использованию.

Резюме

Хранить пароли на сервере рекомендуется в захешированном виде алгоритмом bcrypt или pbkdf2, дополнительно обработанным согласно SRP (т. е. в виде соль + [gbcrypt(соль, пароль) mod N]), и авторизовывать клиента по протоколу SRP. При этом:

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