Управление паролями

Разбираемся, как правильно шифровать пароли и обеспечивать их безопасное хранение.

Bohdan Balov 🇺🇦
12 min readJun 6, 2019
Source: Unsplash

Внимание! Данная статья является переводом самых полезных и интересных (на мой взгляд) моментов из этой публикации. Если понравится моя версия, очень рекомендую изучить оригинал!

Содержание

Введение

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

По интернету гуляет множество противоположных мнений о том, как правильно хешировать пароли. Многие из этих мнений ошибочны и их изъяны могут быть обнаружены только после нескольких лет спокойствий — до первой атаки, когда ваш проект наберет популярность и привлечет к себе внимание плохих ребят.

По своей природе идея хеширования паролей проста до ужаса. Но так много разработчиков допускают ошибки… Я не только расскажу, как правильно обеспечить безопасность, но и почему тот или иной подход плох/хорош.

Важное замечание! Если вы задумываетесь о том, чтобы изобрести собственную хеш-функцию, пожалуйста, не делайте этого! Подобные самописные решения очень легко взламываются. Тот курс криптографии, который вы прошли в университете, вряд ли поможет вам в разработке устойчивого алгоритма. Проблема надежного хеширования паролей была решена задолго до вас: обратите внимание на phpass, defuse/password-hashing и libsodium, если не верите.

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

Вы еще тут? Рад, что вы приняли мой совет. Продолжаем.

Хеширование паролей… Что это за зверь такой?

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542

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

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

Но как же нашему приложению авторизовать пользователя, если мы знаем только хеш пароля? Вот стандартные действия при регистрации/аутентификации:

  1. Пользователь создает аккаунт.
  2. Пароль проходит через хеш-функцию и записывается в базу данных.
  3. Когда пользователь пытается залогиниться, введенный ним пароль проходит через хеш-функцию и сравнивается с хешем, сохраненным в базе данных.
  4. Если хеши совпадают, пользователь получает доступ к защищенным разделам. В противном случае система запрашивает авторизационные данные снова.
  5. Шаги 3 и 4 повторяются каждый раз, когда пользователь проходит авторизацию.

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

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

Примеры распространенных хеш-функций, используемых для хеширования паролей: SHA256, SHA512, RipeMD, WHIRLPOOL.

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

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

Давайте же разберемся, как взламывать хеши. Это знание позволит нам ближе подобраться к ответу, как от этих взломов защититься.

Как взламывают хеши

Словарные атаки и брутфорс

Вот самый простой способ взломать хеш. Берем комбинацию, которая может оказаться паролем, хешируем ее и сравниваем с хешем, который хотим расшифровать. И так до тех пор, пока не будет найдено совпадение.

Существует несколько вариантов реализовать описанный подход. Самые часто используемые: словарная атака (Dictionary Attack) и брутфорс (“метод грубой силы”, Brute Force).

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

В процессе брутфорс-атаки “пробуются” произвольные комбинации на роль искомого пароля. Эти атаки очень дороги в плане вычислительных ресурсов и не столь эффективны, как другие виды атак, но ними очень активно пользуются (особенно начинающие хакеры). Если набраться терпения, можно взломать все, что угодно. Но в большинстве случаев придется очень долго ждать… особенно если у жертвы длинный пароль, состоящий из цифр и букв верхнего и нижнего регистра.

Плохая новость: предотвратить эти атаки никак не получится.

Хорошая новость: эффективность этих атак можно свести к минимуму, если организовать правильное управление паролями.

Таблицы поиска

Таблицы поиска (Lookup Tables) — невероятно эффективный метод взлома хешей одного и того же типа. В основе метода лежит идея подготовки возможных паролей и соответствующих им хешей и их хранение в некой таблице (например, в какой-нибудь структуре данных). Лучшие реализации таблиц поиска способны обрабатывать сотни комбинаций в секунду, тогда как их общее количество может насчитывать несколько миллиардов!

Чтобы получить хорошее представление о таблицах поиска, попробуйте взломать представленные ниже SHA256-хеши с помощью этого сайта.

c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc
08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7
e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904
5206b8b8a996cf5320cb12ca91c7b790fba9f030408efe83ebb83548dc3007bd

Обратные таблицы поиска

Обратные таблицы поиска (Reverse Lookup Tables) позволяют хакерам запускать словарные и брутфорс-атаки одновременно для нескольких хешей без необходимости предварительной подготовки таблицы поиска.

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

Радужные таблицы

Радужные таблицы (Rainbow Tables) основаны на т.н. компромиссе между временем поиска по таблице и занимаемой памятью. [В оригинале: time-memory trade-off.] Согласен, трудно представить… Эти таблицы чем-то напоминают таблицы поиска, в которых пожертвовали скоростью в пользу количества подготовленных комбинаций. Если вам нужно больше подробностей, отсылаю вас на Википедию.

Существуют радужные таблицы, способные взломать MD5-хеш пароля длиной до 8 символов.

Далее мы разберемся, как сделать описанные атаки бесполезными.

Соль — дополнительная преграда

Описанные выше атаки работают только потому, что все пароли хешируются одним и тем же способом. Если у нескольких пользователей один и тот же пароль, хеши их паролей также одинаковы. “Грубые атаки” можно лишить эффективности, если внести в каждый хеш что-то уникальное; в таком случае одинаковые хеши исключаются в корне. (Даже если попадаются одинаковые хеши, это совершенно не означает, что они соответствуют одному и тому же паролю. Почему? Читайте дальше.)

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226abhash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007

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

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

Запомните! Главное, чтобы у каждого пользователя была его личная соль. 🧂

Как не нужно хешировать

Самые распространенные ошибки: использование глобальной или слишком короткой соли.

Повторное использование соли

Решили захардкодить соль в конфиге? Нельзя! Такое встречается довольно часто, что хакерам только на руку! Это не эффективно, и вот почему. Если у пользователей одинаковые пароли, и используется одна и та же соль, хеши также будут одинаковыми. У хакера остается возможность применить обратную таблицу поиска и извлечь все пароли. Представьте, как вам будет стыдно, когда станет известно, что вы захардкодили соль в конфиге. Конец карьере программиста…

Соль должна создаваться/обновляться для каждого пользователя отдельно в таких случаях:

  1. создание аккаунта;
  2. изменение пароля.

Короткая соль

Если соль слишком короткая, не составляет труда применить таблицу поиска к самой соли. Например, если соль состоит из трех ASCII-символов, будет существовать всего 95×95×95=857375 вариантов. Думаете, что это много? Ничего подобного! Для профессионального хакера, располагающего необходимыми средствами, не составит труда обнаружить соль и потом с ее помощью расшифровать пароли всех ваших пользователей.

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

Чтобы усложнить хакеру задачу, соль должна быть длинной. Существует хорошее правило, надежность которого испытана временем: соль должна быть той же длины, что и получаемый в результате хеш. Например, результатом функции SHA256 является 256-битная строка (32 байта); значит и соль должна состоять из 32-х рандомных байтов.

Двойное хеширование и прочие заблуждения

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

Вот антипримеры, которые я откопал на форумах; авторы постов на полном серьезе предлагают использовать эти ужасные решения:

md5(sha1(password))md5(md5(salt) + md5(password))sha1(sha1(password))sha1(str_rot13(password + salt))md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))

Никогда не используйте ничего подобного!

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

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

Также не нужно изобретать собственные хеш-функции. Используйте средства, разработанные профессионалами и проверенные временем. Очевидно, что хакер не имеет возможности взломать хеш, если ему не известен алгоритм. Но не стоит забывать о принципе Керкгоффса, который гласит, что нужно предполагать, что взломщик имеет доступ к алгоритму шифрования (что неизбежно, если речь одет об опенсорс-продукте).

Коллизии хешей

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

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

Хеш пароля, полученный с помощью MD5 и уникальной соли, вполне себе безопасен. Тем не менее использование более надежных алгоритмов пойдет только на пользу: SHA256, SHA512, RipeMD, WHIRLPOOL.

Как нужно хешировать

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

Основы: хеширование с солью

Внимание! Недостаточно просто читать. Если хотите вполне понять, о чем идет речь, вам нужно реализовать описываемые подходы на практике.

Мы уже разобрались, как злоумышленники расшифровывают пароли при помощи таблиц поиска и других хитроумных приемов. Также мы узнали, что лучшая защита от “грубой силы” — хеширование с применением соли, уникальной для каждого пользователя. Но вопрос, как генерировать эту соль, остается открытым.

Соль нужно генерировать криптографически стойким генератором псевдослучайных чисел (Cryptographically Secure Pseudo-Random Number Generator, CSPRNG). Подобный генератор — не просто генератор псевдослучайного числа, такой как “rand()” в языке C. Как и подразумевает название, CSPRNG является криптографически безопасным и обеспечивает высокую степень непредсказуемости. Мы не желаем, чтобы наша соль была предсказуемой, поэтому нужно использовать CSPRNG.

Вот список безопасных генераторов для некоторых популярных языков программирования:

PHP: mcrypt_create_iv, openssl_random_pseudo_bytes

Java: java.security.SecureRandom

Dot NET (C#, VB): System.Security.Cryptography.RNGCryptoServiceProvider

Ruby: SecureRandom

Python: os.urandom

Perl: Math::Random::Secure

C/C++ (Windows API): CryptGenRandom

Any language on GNU/Linux or Unix: Read from /dev/random or /dev/urandom

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

Чтобы сохранить пароль, нужно:

  1. Сгенерировать рандомную соль, используя CSPRNG.
  2. Прибавить соль к паролю (к началу или концу) и пропустить получившуюся строку через стандартную хеш-функцию, такую как Argon2, bcrypt, scrypt, PBKDF2 или какую-нибудь другую.
  3. Сохранить соль и пароль в базе данных.

Для валидации пароля нужно:

  1. Извлечь соль и хеш пароля из базы данных.
  2. Прибавить соль к полученному от пользователя паролю (к началу или концу) и пропустить полученную строку через соответствующую хеш-функцию.
  3. Сравнить полученный хеш с валидным хешем, взятым из базы данных. Если эти хеши совпадают, значит пароль верный.

Если это возможно, хешируйте на сервере

Если вы разрабатываете веб-приложение, имеет значение, где будет генерироваться хеш. Как правильно: создавать хеш на клиентской стороне (например, при помощи JavaScript) или отправлять пароль на сервер в чистом виде и хешировать его где-то там?

Если вы хешируете пароль на клиентской стороне, он все равно должен быть повторно захеширован на сервере.

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

Проблема заключается в том, что хеш, сгенерированный на клиенте, по сути выступает в роли реального пароля пользователя. Т.е. все, что нужно для авторизации: передать серверу хеш пароля. Если хакер узнает пароль пользователя (или хеш), он сможет без затруднений пройти авторизацию! Более печальный вариант: хакер каким-то хитрым способом получает базу данных с хешами паролей пользователей и получает доступ ко всем аккаунтам.

Это не говорит о том, что хеширование на клиенте является плохой идеей. Но мораль такова: хешировать на сервере нужно всегда.

Хеширование на клиенте — штука полезная. Вот некоторые скользкие моменты, о которых не стоит забывать:

  • Хеширование на клиенте — не замена для HTTPS (SSL/TLS). Если соединение между клиентом и сервером не защищено, злоумышленник может подменить механизм обмена данными и нанести приложению непоправимый ущерб.
  • Не все браузеры поддерживают JavaScript, некоторые пользователи отключают его поддержку умышленно. Приложение должно определять, поддерживает ли браузер JavaScript, и при необходимости эмулировать клиентское хеширование на сервере.
  • Хеш, формируемый на клиентской стороне, также должен быть “засоленным”. Тут есть соблазн запрашивать соль с сервера, но это плохая идея: таким образом злоумышленник сможет извлечь реальные логины пользователей (если сервер возвращает соль клиенту в случае валидного логина). Если на сервере происходит правильное хеширование, которое мы уже обсудили, на клиенте будет достаточно реализовать упрощенный вариант получения хеша, например, со строкой логин + домен в качестве соли.

Медленные хеш-функции

Соль —гарантия того, что хакер не сможет взломать пароли с помощью таблиц поиска (и других видов “табличных” атак). Но соль не оказывает никакой защиты перед методами грубой силы: брутфорсом и словарной атакой. Современные видеокарты (GPU) и специализированное оборудование способны рассчитывать миллиарды хешей в секунду, что делает подобные виды атак вполне себе эффективными. Чтобы сделать эти атаки менее эффективными, можно прибегнуть к технике стретчинга (key stretching).

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

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

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

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

Дополнительные меры защиты

Хеширование обеспечивает защиту паролей в том случае, когда ваша система безопасности дает сбой. Это не делает приложение в целом более безопасным. Чтобы пароли не были скомпрометированы, придется изрядно попыхтеть!

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

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

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

Давайте общаться! Я в соц. сетях:

--

--

Bohdan Balov 🇺🇦

Lead Software Engineer at EPAM Systems | Mentor | Writer | Crazy Runner from Brave Ukraine