Любой разработчик рано или поздно сталкивался с проблемой, которая заключалась в том, как ему правильно организовать хранение паролей зарегистрированных пользователей в БД.
Я конечно не буду отрицать что статей в интернете на эту тему достаточно, но ни одна на мой взгляд не освещает всех тонкостей казалось бы такого простого процесса. Для понимания этих тонкостей разработчику необходимо наличие умения оценивать систему со стороны взломщика, а для этого необходим хотя бы небольшой опыт в этой сфере (по принципу «кто предупрежден, тот вооружен»). К сожалению, у большинства разработчиков такого опыта нет, и безопасность своих приложений они строят на основе скудной информации найденной в интернете.
В этой статье я попытаюсь рассмотреть минусы хранения открытых паролей в БД. Попытаюсь убедить в необходимости хешировать каждый пароль. Также попытаюсь объяснить зачем нужна «соль» и какой она бывает. Ну и вкратце расскажу про разные алгоритмы хеширования.
Зачем?
Для начала нам необходимо понять зачем вообще нужно правильно хранить пароли. Правильно организованный алгоритм хранения паролей должен:
- Снизить риск полного взлома системы
- Предотвратить утечку паролей пользователей
Рассмотрим когда система имеет какие-либо теоретические уязвимости. Неправильный алгоритм позволит в первом случае получить пароль администратора через некую имеющуюся уязвимость, далее попасть в админ-панель, и далее по обстановке, т.е. в 90% случаев это означает полный взлом. Во втором случае если злоумышленник каким-то образом получает базу всех паролей, то это позволит скомпрометировать некоторых пользователей, которые, к примеру, используют один пароль на все ресурсы. Правильный же алгоритм должен вообще предотвратить получение злоумышленником паролей.
На практике это выглядит так. Злоумышленник находит на сайте SQL-injection, через которую получает логин-пароли всех пользователей. При правильной системе хранения паролей злоумышленник получает не исходные пароли а только их хеш суммы (см.далее).
На заре развития интернета и веб приложений, каждый разработчик не задумывался над этой проблемой и хранил в базе данных открытые пароли. Но выше я уже описал чем плох такой вариант. В итоге система взламывалась через банальнейшую инъекцию к БД, которых в те времена было очень много.
Хеш сумма
Разработчики пришли к выводу что использовать открытое хранение паролей в своих системах занятие не безопасное и надо придумать что-то другое. И тут на помощь пришли хеш суммы.
Что такое хеш сумма? Допустим у пользователя пароль «123456». Придумаем свою хеш-функцию. Сложим все цифры получим число «21» — это и будет являться результатом нашей хеш функции, хеш-суммой или хешем. Конечно наш алгоритм отвратителен (да и хеш суммой его назвать нельзя, это скорее контрольная сумма), так как один и тот же хеш может соответствовать большому количеству различных паролей. То есть пароли «654321», «555222», «100299» и т.д будут давать такой же хеш, или по-научному будут являться коллизией.
Идеальная хеш функция должна обладать следующими параметрами:
- Необратимость — хеш сумма не должна «расшифровываться» подобно обычным алгоритмам шифрования
- Отсутствие коллизий — для каждых данных проходящих через хеш-функцию должен получится уникальный хеш
И если первый параметр практически достигнут в современных алгоритмах хеш-функций. То второй параметр не достижим для хешей с фиксированной длиной (а таких алгоритмов сейчас большинство) даже чисто теоретически (я надеюсь вы понимаете почему).
И теперь при регистрации пользователя, указанный им пароль проходит через хеш-функцию и вместо пароля в БД будет занесен полученный хеш. При каждой попытке авторизации указанный пароль будет каждый раз проходить через хеш-функцию и полученный хеш будет сравниваться с хешем хранимым в БД, и если хеши будут соответствовать значит пароль был указан верный. Вообщем как то так:
Теперь злоумышленнику необходимо будет попытаться восстановить пароль из хеша, и причем если алгоритм хеш-функции полностью необратим, то для злоумышленника останется лишь один метод — брутофорс. Если на более понятном языке, то брутофорс — это перебор всех возможных паролей до тех пор пока хеш от одного из них не совпадет с исходным хешем.
«Соленые» хеши
Вообщем все бы хорошо, но если бы не ленивые или забывчивые пользователи которые стремятся использовать максимально короткие и простые пароли… Почему это плохо? Потому что сбрутить короткие пароли можно в считанные минуты, а простые (часто используемые) пароли брутятся по словарям.
Казалось бы выход — запретить пользователям использовать короткие пароли, обязать их использовать спецсимволы и т.д. Но ведь это дело каждого пользователя какой ему использовать пароль. Мы, как разработчики, можем только лишь рекомендовать использовать более сложный пароль.
Как же нам защитить пользователей и свой ресурс в случае его взлома? На помощь проходит соль. Грубо говоря соль — это набор случайный символов который каждый раз перед прохождением через хеш-функцию добавляется к паролю. При регистрации пользователя генерируется случайная соль, на основе которой и указанного пароля генерируется «соленый» хеш, при этом соль также заносится в БД:
Что дает соль в этом случае? Если подумать, то если злоумышленник имеет доступ к хешам пользователей, то если соль каждого хеша мы храним рядом (в той же таблице/БД), то злоумышленник также будет иметь и доступ к соли. То есть сможет найти исходный пароль методом брутофорса, но словари ему уже не подойдут, так как не существует словарей учитывающих все комбинации паролей с солью.
Теперь рассмотрим второй плюс. Допустим вы владеете ресурсом с десяткой тысяч зарегистрированных пользователей. Каждый пароль пользователя имеет уникальную соль. Что это даст? Это даст невозможность одновременного брута хешей всех пользователей, так как для каждой соли нам придется генерировать новый хеш. И если вдруг злоумышленник сбрутить все аккаунты, то ему придется на каждый вариант пароля генерировать столько хешей сколько пользователей у вас имеется. При этом скорость брутофорса упадет пропорционально. Допустим скорость брута — 1 миллион паролей в секунду. В нашем случае (десять тысяч пользователей) скорость брута упадет до 100 паролей в секунду. Согласитесь, брутить всех бессмысленно? Разве что по простейшим и коротким словарям…
Вообщем такой алгоритм, если вы помните начало статьи, предотвращает утечку паролей пользователей.
Дважды «соленый» хеш
Но вот другая сторона медали. В случае целенаправленной атаки на определенного пользователя (например администратора ресурса), взломщика не остановит такая соль, как написано выше, так как ему придется брутить всего один хеш с уже приемлемой скоростью.
В нашем случае поможет еще одна соль, но уже общая для всех хешей, которая допустим будет хранится отдельно от хеша, т.е в любом другом месте, к примеру в конфиге самого приложения.
Давайте немного порассуждаем. Отставьте в сторону в свои выпады в стиле «надо предполагать что взломщик имеет полный доступ к всей системе и такая соль бессмысленна». Представьте ситуацию, которую я описывал выше, в вашей системе имеется уязвимость по типу SQL-injection, через которую злоумышленнику удалось получить администраторский хеш и соль из БД. Далее злоумышленнику удалось и сбрутить хеш — и все, ваша система, считайте, взломана.
А ведь наличие дополнительной соли не дало бы злоумышленнику взломать систему, так как ему бы пришлось брутить еще и соль (а это, скорее всего, будет бесполезным занятием). Да, вы опять можете сказать, что «надо предполагать что взломщик имеет полный доступ к всей системе». Но вдумайтесь, зачем тогда злоумышленнику брутить пароль администратора если он уже имеет полный доступ ко всей системе? Да, теоретически вероятны атаки на других пользователей, кроме администраторов, но на моей практике я не слышал ни об одном таком случае.
Если подвести итог, то смысл данной соли заключается в снижении риска полного взлома системы при наличии у злоумышленника лишь частичного доступа.
Главная идея этого принципа — раздельное хранение хеша и общей соли, т.е. постараться не допустить попадание соли в руки злоумышленника Если подумать над практической реализацией то, в момент настройки системы, создается общая соль, которая будет хранится в конфиге приложения, и далее эта соль будет также добавляться к паролю. Если схематично то это будет выглядеть вот так:
При этом проверка правильности пароля при авторизации будет выглядеть так:
Алгоритмы хеширования
Конечно полностью защититься от брута невозможно. Но в наших силах сделать брутофорос бессмысленным. В своих системах необходимо применять такой алгоритм хеширования, который требует довольно больших ресурсов и большого количества операций для вычисления хеша.
Теперь представьте что мы используем алгоритм который позволяет генерировать хеши со скоростью лишь тысяча или сотня хешей в секунду. Такой хеш вкупе с паролем хотя бы в 10-12 знаков будет подбираться значительно дольше чем «разумное время», и смысла такой подбор иметь не будет.
К примеру на моем ноуте:
- MD5 — 2 200 000 паролей/сек
- SHA — 800 000 паролей/сек
- MD5(unix) — 1 200 паролей/сек
Теперь давайте добавим немного математики, и подсчитаем среднее время, которое понадобится нам для перебора простенького пароля из 6 симолов латиницы верхнего и нижнего регистров и цифр. То есть это всего около 100 000 000 000 комбинаций.
- Для MD5 ~45 000 секунд или ~12 часов
- Для SHA ~125 000 секунд или ~35 часов
- Для MD5(unix) ~83 300 000 секунд или ~23 100 часов или ~3 года
Причем для md5 и sha брут еще как-то более-менее имеет смысл, то брутить md5(unix), на мой взгляд, смысла нет абсолютно. Кстати, для справки, в основе md5(unix) лежит тысяча итераций обычного md5.
Конечно можно выдумать свой алгоритм, который будет вычислять хеш еще дольше, но здесь необходимо найти грань между ресурсоемкостью алгоритма и производительностью сервера. Иначе вы рискуете подвесить сервер только лишь одними вычислениями хеша.
Коллизии
К сожалению все было бы слишком хорошо если бы все так было бы просто. Но мы забыли про коллизии о которых я писал в самом начале. Не стоит забывать про то что для вашего хеша от супер сложного пароля в 30 символов может быть найдена коллизия длиной в 1 символ. Конечно вероятность эта крайне мала, но она есть.
И к большому сожалению на данный момент не существует стопроцентного решения этой проблемы для хеш сумм, так как теоретически любой хеш фиксированной длины будет иметь коллизии. Но обычно для снижения вероятности нахождения коллизей используют несколько алгоритмов хеширования. На практике это может выглядеть так: один пароль хешируют сначала по md5, затем тот же пароль хешируют по sha, полученные хеш-суммы объединяют в один хеш, который и используют в дальнейшем.
Но у меня вроде как возникла еще одна идея для решения этой проблемы, но об этом в одной из следующих статей ;)
Итог
Вот основные правила которые вы должны были понять из этой статьи:
- Пароль не должен хранится в БД в открытом виде, а должна хранится лишь хеш-сумма
- При регистрации пользователя желательно рекомендовать (но не вынуждать!) использовать более сложный пароль
- Каждый пользовательский хеш необходимо генерировать с уникальной солью
- К пользовательской соли должна быть добавлена общая соль, которая хранится в другом месте (отдельно от пользовательских данных)
- Соль должна быть достаточно длинной
- Алгоритм вычисления хеш суммы должен быть ресурсоемок (но не вешать ресурс напрочь)
На правах рекламы: я тоже писал про хранение паролей пользователей. И тоже пришел к выводу, что хеш должен считаться максимально долгое время(тесты показали, что 1000 паролей в секунду это достаточно оптимально).
Там ещё в комментариях развернули интересную дискуссию на эту тему.
Спамер :D
Впринципе у меня почти все тоже самое, только более полно. Хотя идея насчет отдельного модуля к веб серверу неплоха. Но на хабре меня заминусовали за подобную идею…
На хабре всех минусуют)
Мне вот что непонятно: если на стороне клиента вычисляется хеш, значит и точная последовательность использования хеш-функций (ели их несколько), и индивидуальная соль, и общая соль и их взаимопорядок конкатенации известны клиенту, т.е. и злоумышленнику.
В чем тогда смысл?
Спасибо.
Ну честно говоря, я не сталкивался еще ни разу с такой реализацией, где бы на стороне клиента вычислялся хеш со всеми солями (т.е. идентичный хешу хранящемуся в базе). Но конечно, если захотеть то из всех возможных реализаций авторизации можно выбрать самую неправильную и неудачную, т.е. эту).
Как правило, хеш вычисляется уже на стороне сервера, т.е. пароль на сервер передается в открытом виде. Но, конечно, существуют реализации без передачи открытого пароля по каналу. Я бы это делал следующим образом.
Вид хеша хранящегося в базе:
func(func(PASS, SALT_PUBLIC), SALT_USER, SALT_GLOBAL)
На стороне клиента на основе пароля генерируем:
func(PASS, SALT_PUBLIC)
и полученный хеш (назовем его HASH) передаем на сервер, где «дохешируем» его
func(HASH, SALT_USER, SALT_GLOBAL)
и сравниваем с хранящимся в базе.
Это в общем виде конечно. Нужно посидеть и подумать как сделать лучше и правильнее. Но общий смысл понятен. В результате — клиенту не известны ни алгоритм, ни общая соль, ничего, так же мы не передаем открытый пароль по каналу. В общем одни плюсы.
А по подробнее могли бы написать про реализацию всех этих функции с шифрованием пароля и передачу их на сервер и расшифровку?
Честно говоря, я не ставил перед собой такую задачу)
Вообще все очень просто, берете какую нить реализацию стойкой хеш функции на JavaScript, при submit формы, хешируете введенный пароль и отправляете его в таком виде на сервер.
На сервере уже совершаете остальные хеширования, проверки и прочее.
Классный сайт, так все подробно и что надо по теме написанно, прям разжевали и в рот положили. Я вот набрел на эту статью так как ищу способ шифрования данных переданных пользователем. Понравилась статья про асимитричное шифрование SSL, и хотелось бы реализовать что то подобное на php приватный ключ и публичный и пускать все через шифрованный канал по типа https. Как программно это реализовать пока ума не приложу, может есть какие наработки у вас? А то я уж очень щипитильно отношусь к системе сохранности пользовательских данных и этот вопрос если не решить будет тормозить мой проект.