Итак, мы решили сделать авторизацию и регистрацию на сайте через пароли. Как максимально обезопасить пароли пользователей от взлома, от хостинговой компании, которой принадлежит сервер, и от своих же любопытных коллег, имеющих доступ к базе?
Для начала, никогда не храните пароли в открытом виде. Храните соленые хеши от них.
Хеш-функция - это такая функция, которая принимает на вход произвольную строку (например, пароль) и выдает на выходе хеш - число или строку небольшой фиксированной длины, из которой невозможно восстановить исходные данные. Криптографическая хеш-функция отличается от обычной защитой от манипуляций, например, она не позволяет после изменения строки добавить несколько символов, чтобы получить такой же хеш, который был у исходной строки (обычные хеш-функции вроде CRC32 используются только для защиты от случайных ошибок, а не от умышленных воздействий).
Примерами криптографических хеш-функций являются, например, MD5, SHA256 (про них написано в вики, но предупрежу, что понять их алгоритм без знания основ криптографии будет непросто). Способы легко обратить хеш-функцию и получить из хеша исходную строку неизвестны. То есть получить хеш из пароля просто, а вот восстановить пароль, имея хеш практически невозможно — надо перебирать все возможные пароли, вычислять для каждого хеш и сравнивать с имеющимся.
Вот пример хешей от пароля 'strongpassword': md5('strongpassword') = f93fc10472a31bb3061aa0b45e228c5a
, sha1('strongpassword') = 2ae868079d293e0a185c671c7bcdac51df36e385
. Здесь хеши записаны в 16-чной системе счисления с помощью символов 0-9, a-f.
Итак, если вместо пароля хранить его хеш, то мы по-прежнему можем проверить, правильный ли пароль ввел пользователь (получив его хеш и сравнив с тем, что хранится в базе), но не можем получить исходный пароль. Однако, просто хеширования недостаточно и этот подход имеет такие недостатки:
- если у двух пользователей одинаковые пароли, то и хеши у них будут одинаковые
- пользователи часто выбирают простые пароли, и у злоумышленника может быть заготовлена таблица хешей от популярных паролей вроде '123456'
И есть еще один, самый главный недостаток - все хеши можно подбирать одновременно. Допустим, злоумышленник украл базу с хешами паролей. Он начинает их подбирать, перебирая все возможные пароли, вычисляя для каждого хеш и сравнивая с украденной базой. Проблема в том, что все пароли перебираются по сути одновременно - злоумышленник нашел хеш для пароля '1', сравнил его со всеми хешами в базе, и за один шаг узнал, есть ли в базе такой пароль или нет.
Для борьбы с этими недостатками используют "соление" паролей перед хешированием. При регистрации пользователя генерируется соль (salt) - случайный набор символов вроде H*5$@)_-hPoI&^530
. Соль не видна пользователю, потому она может быть сложной и длинной. Затем мы присоединяем соль к паролю, для пароля 123456
в итоге получается строка H*5$@)_-hPoI&^530:123456
. И затем уже от этой строки берем хеш и сохраняем в базу соль и хеш.
Благодаря добавлению соли даже одинаковые пароли получают разные хеши (так как у них разная соль), а таблицы заранее вычисленных хешей для популярных паролей становятся бесполезными. И атакующий теперь при переборе вынужден подбирать пароль для каждого хеша индивидуально, что сильно замедляет работу.
Для удобства хранения многие функции объединяют хеш пароля и соль, которая использовалась при хешировании, в одну строку, например такого формата: соль$хеш
. Таким образом функция хеширования пароля может вернуть сразу и хеш, и сгенерированную ей соль.
Вычисление классчических хешей вроде md5 очень быстро делается на современном железе (до миллиардов хешей в секунду). Чтобы усложнить перебор, можно использовать более "тяжелые" для вычисления хеши, например http://ru.wikipedia.org/wiki/Bcrypt и http://ru.wikipedia.org/wiki/Scrypt где можно задавать сложность вычисления хеша (а в scrypt — еще и необходимый объем памяти). Сложные алгоритмы также не позволяют сделать специализированные устройства для ускорения вычисления хешей (так называемые ASIC'и), требуя наличия стандартного процессора и большого объема памяти.
В PHP5.5 сделали стандартный набор функций для работы с паролями, среди которых есть:
- password_hash() - генерирует соль и возвращает эту соль и хеш для данного пароля
- password_verify() - используется для проверки пароля, принимает на вход пароль и соленый хеш и проверяет, соответствует ли пароль хешу
Функция password_hash
возвращает строку, которая содержит сразу хеш, соль и обозначение использованного алгоритма хеширования, так что для их хранения достаточно одной ячейки в базе данных. Подробнее:
Функции хеширования:
- md5 - генерирует хеш с использованием алгоритма MD5. Хеш состоит из 32 символов из набора [0-9a-f]
- sha1 - генерирует хеш с использованием алгоритма SHA-1, возвращает хеш из 40 символов из набора [0-9a-f]
- Расширение hash содержит функции хеширования для различных алгоритмов
- Функция openssl_digest из расширения openssl позволяет хешировать данные различными алгоритмами
Чтобы сгенерировать соль, необходим надежный (непредсказуемый) криптографический генератор случайных чисел. В качестве него можно использовать:
- добавленную в PHP7 функцию random_bytes()
- openssl_random_pseudo_bytes() из расширения SSL, при этом важно прочитать документацию и убедиться, что используется надежный алгоритм
- на linux/mac можно читать случайные данные из
/dev/random
Библиотека https://github.com/paragonie/random_compat умеет выбирать подходящую функцию из имеющихся в наличии. Обратите внимание, что функции rand() и mt_rand() не являются криптографически надеждными, так как они используют относительно простой алгоритм и, имея несколько сгенерированных чисел, можно предсказать следующие.
Предположим, у нас есть база хешей паролей без соли, использующая алгоритм MD5. Для ее взлома мы перебираем все возможные пароли (например, начиная с 1111111 и заканчивая zzzzzzz) и вычисляем от каждого MD5-хеш. При этом число вариантов, которые надо подобрать, зависит от длины пароля и набора символов (чем их больше тем больше перебирать). Скорость вычисления MD5 хеша на топовых видеокартах в 2011 году составляла около 2 миллиардов в секунду ( http://www.opennet.ru/opennews/art.shtml?num=30201 и http://hashcat.net/oclhashcat/ ). А ведь можно взять не одну видеокарту, а много, если очень надо. Также, злоумышленник с большим количеством ресурсов может сделать специализированное устройство, работающее с более высокой скоростью (такие устройства делались для генерации биткоинов).
Заметим, что из-за отсутствия соли мы подбираем пароли для всех хешей в базе параллельно, с примерно такой же скоростью, как и для одного хеша.
Если пароль состоит из N символов, и всего использованы M различных видов символов, то число возможных вариантов паролей, которые придется перебрать, равняется MN (M в степени N). Например:
- если в пароле 12 цифр
0-9
: число комбинаций = 1012 = 1000 миллиардов = 500 секунд перебора на 1 видеокарте. - если в пароле 6 букв
a-z
или цифр0-9
. Число вариантов = 366 (считаем гуглом) = 2 млрд. Около секунды. - если в пароле 6 букв
a-zA-Z
(добавим буквы в разном регистре) и цифр0-9
. Комбинаций 626 = 56 млрд., или 28 секунд перебора. - если в пароле 8 букв
a-zA-Z
и цифр0-9
. Комбинаций уже 628 = 218 триллионов. Это примерно 109000 секунд перебора (в часе 3600 секунд, так что выходит 30 часов) на 1 карточке. - если в пароле 10 символов из набора
a-zA-Z0-9
или дополнительных 20 знаков вроде минус, плюс, то выходит 8210 комбинаций ~ 1019 и перебирать их 5×109 (5 миллиардов) секунд на одной карте (57870 дней, или 158 лет) или 58 дней на массиве из тысячи видеокарт.
Люди часто ставят паролем не случайный набор букв, а слова или куски слов. Значит, какие-то символы рядом встречаются чаще, их можно перебирать в первую очередь, тем самым сокращая число вариантов и ускоряя время нахождения. У злоумышленников есть словари, а также огромные списки паролей, полученные ими из предыдущих взломов и утечек.
В общем, видно, что без добавления соли пароли подберутся на раз. И не все ставят 10-символьные пароли, у многих там просто слово или цифры.
В случае добавления соли указанное время выше подбора будет относиться к подбору одного хеша, а не ко всей базе.
Другой вариант — сгенерировать или скачать огромные радужные таблицы, где хранятся уже рассчитанные цепочки хешей (для простых паролей). И конечно все хеши от обычных паролей длиной до 10 символов там уже есть (больше нету, так как они начинают занимать гигабайты. Но это вопрос времени, когда жесткие диски станут больше). Если хранить в базе хеш без соли, то взлом будет очень быстрым.
Посмотреть, какого размера получаются таблицы, можно тут: http://project-rainbowcrack.com/table.htm
Вот пример такой таблицы: md5_loweralpha-numeric#1-10 316 GB
- подбирает пароли без соли до 10 символов [a-z0-9].
Заметим что в будущем компьютеры будут мощнее, и значит подбираться пароли будут быстрее. Теперь подумаем, как защититься и усложнить жизнь взломщикам:
- разрешаем использовать больше видов символов в паролях
- добавляем соль. С солью не получится параллельно подбирать все хеши, что сильно замедляет взлом. Также, при добавлении соли даже к простому паролю он по сути становится длинным и сложным и его не будет в радужных таблицах (123456 → Y^juYUHkd%$123456). Опять же, соль должна быть подлиннее и содержать спецсимволы, чтобы было больше комбинаций для перебора. Но простые пароли вроде 123456 все равно вскроют, так как их при переборе проверяют в первую очередь. А вот сложные придется подбирать долго.
- используем вместо MD5 более тяжелые для вычисления алгоритмы вроде BCrypt, который сделан так, что его нельзя вычислить быстрее, чем за определенное время (и можно указать требемый уровень сложности).
С правильным подходом даже простой MD5 придется долго расшифровывать.