Многострадальная тема, на которую написано много материала, но хочу систематизировать известные сведения. Как же все-таки безопасно хранить пользовательские пароли и авторизовывать пользователей?
Все дальнейшее относится к методам реализации контроля доступа, основанных на знании клиентом некоторой ключевой информации (пароля). Использование криптографических ключей, как, например, сертификатов ЭЦП, отдельный разговор, выходящий за рамки этой статьи.
Итак у нас есть некоторая система (не обязательно интерактивная и/или сетевая), в которой нам необходимо проводить авторизацию пользователей. В дальнейшем я использую клиент-серверную терминологию, но следует понимать, что описанное в частности может относиться и к полностью автономным системам, например шифрующих файлы используя пароль. При этом в общем виде стоят следующие задачи:
опционально: позволить клиенту сохранить пароль у себя в некотором защищенном виде, при котором подбор изначального пароля затруднен, т.е. в частности исключить попытки предсказания механизма генерации паролей, получив какой-либо из предыдущих (поскольку, если пароль человек придумывает «из головы», а не генерирует случайным образом, то он обычно не в состоянии запоминать разные случайные пароли для разных сервисов, и в результате у пользователя часто либо есть некоторый набор «типовых» паролей, которые он использует везде, или есть какая-то мнемоническая схема генерации, которая может быть раскрыта если увидеть один или несколько таких «сгенерированных» паролей, соответственно желательно тут подстраховать пользователя дополнительно);
Очень часто задача решается путем хеширования пароля каким-либо хешем общего назначения (md5, sha1 и т. п. ), и хранения на сервере этого хеша. Это очень плохой вариант, который не удовлетворяет ни одному из предъявленных требований. Может показаться, что уж первому то требованию то удовлетворяет, но на самом деле это не так, и, например linkedin тому подтверждение.
В случае автономных систем, задача которых заключается в шифровании чего-либо, обычно пароли ни в коем виде не хранятся, но некоторые их производные используются в качестве ключей шифрования: т. е. если пароль указан неверно, то и расшифровка будет неправильной. Но использование хешей общего назначения в качестве подобных производных совершенно также некорректно.
Основная задача, решаемая с помощью хеш-функций общего назначения, это защита данных от модификации. Это обычная контрольная сумма, только качественно более надежная, чем, например, CRC32. Они обеспечивают максимально возможную защиту от коллизий: минимальную вероятность того, что разные данные будут иметь одинаковый хеш. При этом предполагается, что подобные хеши могут считаться от огромных объемов данных, поэтому скорость работы хеша — немаловажный параметр. И абсолютно все хеш-функции общего назначения заточены под быструю работу. Они используют простые операции, используют мало памяти, и т. п.
В отношении же безопасного хранения паролей эта их особенность — зло. Чем быстрее хеш-функция, тем быстрее подбирается коллизия, особенно для таких коротких исходных данных как пароли. Для когда-то считавшегося непробиваемым MD5 сейчас подбор 8-и символьного пароля полным перебором занимает минуты на обычном офисном компе.
Поэтому подобные хеш-функции сами по себе неприменимы в задаче безопасного хранения пароля. Даже в случае их усиления солью: да, соль не даст использовать уже созданные базы данных, но прямой перебор все равно очень быстр, даже без таких баз.
Резюме, хеш-функции общего назначения:
Для решения подобных проблем существуют специализированные хеш-функции, которые требуют огромных вычислительных мощностей, и соответственно считаются очень медленно. Типовые варианты таких хешей:
Эти варианты реализованы в стандартной для многих языков/систем фунции crypt (за исключением разве что scrypt и PBKDF2, которые если и реализованы, то отдельно). При этом наиболее типовой вариант — bcrypt.
Относительно параметров сложности хеша:
Соль, как известно, представляет собой некоторое количество случайных данных, добавляемых к паролю при хешировании с целью:
Поэтому в качестве соли должна выбираться строка во первых уникальная для каждого пользователя, а во вторых случайная, а не базирующаяся на каких либо свойствах пользователя типа его логина. Само собой что меру случайности следует выбирать в разумных пределах в соответствии с особенностями используемого хеша. Также следует учитывать, что base64 кодировка случайных данных содержит больше энтропии, чем hex (6 бит на символ против 4х).
Само хранение паролей в надежно захешированном виде защищает лишь от того, что украденная с сервера база не поможет злоумышленнику получить пользовательские пароли. Но часто это не единственный возможный способ получения доступа. Ведь в процессе авторизации клиент передает пароль, как защититься от прослушки?
Также необходимо реализовывать процесс авторизации таким образом, чтобы нельзя было использовать хранящийся на сервере хеш пароля для авторизации клиентом. Иначе, в случае кражи базы данных паролей, злоумышленнику будет и не нужно что-либо подбирать: он будет использовать эти хеши и все.
Есть множество алгоритмов, позволяющий авторизовываться без непосредственной передачи паролей, например:
Хранить пароли на сервере рекомендуется в захешированном виде алгоритмом bcrypt или pbkdf2, дополнительно обработанным согласно SRP (т. е. в виде соль + [gbcrypt(соль, пароль) mod N]), и авторизовывать клиента по протоколу SRP. При этом: