Как расхешировать пароль php
Перейти к содержимому

Как расхешировать пароль php

  • автор:

PHP – всё про защиту пароля

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

PHP – md5 вычисляем хеш строки

Строки? А почему не пароля?
НЕ ИСПОЛЬЗУЙТЕ md5 для “шифрования” паролей, это очень ненадежное средство. Как это выглядит на практике:

echo md5('test123'); // cc03e747a6afbbcbf8be7668acfebee5 echo md5('test124'); // ad2af2578b4d55b5f78383024270f852

Если запустить данную команду, то функция вернёт MD5-хеш строки. У нас два разных пароля, у которых отличается только последний символ test123 и test124. Разница 1 символ, но ничего общего в хеше md5 нет.

Так почему же такой способ ненадежный? Он очень легко ломается, и если вы не можете перевести обратно из md5 в текст, то для злоумышленника это не займет много времени (что уж там говорить, если есть сайты, которые это делают в пару кликов).

Предположим, что md5 это хороший и надежный способ хранить пароли (напомню, это не так). Как бы тогда выглядел код?

$passwordHash = ‘cc03e747a6afbbcbf8be7668acfebee5’; $password = $_REQUEST[‘password’]; if (md5($password) === $passwordHash) < echo 'done'; >else

Где, $passwordHash – это зашифрованный пароль, который лежит в базе данных, а $password = $_REQUEST[‘password’] – это пароль, который ввел пользователь через форму авторизации.

PHP – Как надежно защитить пароль?

Для этого нам понадобится функция password_hash и password_verify. Как вообще должен храниться пароль:

При таком указании пароля он делится на части:
алгоритм – при помощи которого и был зашифрован пароль;
cost – задаёт необходимую алгоритмическую сложность, стандартно она 10, чем выше сложность, тем дольше будет генерироваться пароль, и соответственно, тем дольше будет загружаться страница;
salt – соль для хеширования, некая уникальная строка, которая увеличивает сложность дешифровки пароля. На данный момент, используется автоматическая соль, но об этом чуть ниже;
хэш – хэш пароля.

То есть, пароль шифруется при помощи определенного алгоритма, сложности и соли.

Как применять функцию password_hash в PHP?

Функция должна содержать 2 аргумента, сам пароль и алгоритм. PASSWORD_DEFAULT, PASSWORD_BCRYPT – наиболее чаще встречаются в качестве алгоритмов, особенно PASSWORD_BCRYPT. Такой хэш пароля, в отличие от md5, при перезагрузке страницы, всегда будет уникальным, и его длина, независимо от выбранной сложности, всегда 60 символов.
Выглядит это следующим образом:

echo password_hash('test123', PASSWORD_DEFAULT); // $2y$10$kPuLknA408BkgnaKVUh8EOQOm1dRUUkufeqvG6OoIvpGDopPGea1q echo password_hash('test123', PASSWORD_BCRYPT); // $2y$10$jFmerATwXASg0WvV3W9pt.fK1UQmnnLrKao35r6/VJfzz7NPxszCq echo mb_strlen(password_hash('test123', PASSWORD_BCRYPT)); // 60

Как же нам увеличить сложность алгоритма? Для этого нужно использовать третий аргумент функции, который не обязательный, это опции:

echo password_hash('test123', PASSWORD_BCRYPT, ['salt' => 'ttttttttttttttttttttttttt']); // $2y$10$ttttttttttttttttttttteRXxHYSDzcdOi/S1JzEheCNMETevWiKC echo password_hash('test123', PASSWORD_BCRYPT, ['cost' => 12]); // $2y$12$wNh3cx81xQzXvOKCwPuerOOBOyyxgE3noJQsp3L3RYDMy43CTs4n2

Через массив мы указываем salt – и она уже будет являться частью строки. Если вы внимательно изучите сам процесс создания этого участка “из соли”, то обнаружите, что она может записаться не полностью, или может быть не достаточно сложной. В настоящее время не рекомендуется её задавать явно, а начиная с PHP 8 её просто игнорируют, и всё равно генерируют автоматически.

Внимание
Эта опция объявлена устаревшей. Рекомендуется использовать автоматически генерируемую соль. Начиная с PHP 8.0.0 явно заданная соль игнорируется.

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

/** * Данный код замерит скорость выполнения операции хеширования для вашего сервера * с разными значениями алгоритмической сложности для определения максимального * его значения, не приводящего к деградации производительности. Хорошее базовое * значение лежит в диапазоне 8-10, но если ваш сервер достаточно мощный, то можно * задать и больше. Данный скрипт ищет максимальное значение, при котором * хеширование уложится в 50 миллисекунд. */ $timeTarget = 0.05; // 50 миллисекунд. $cost = 8; do < $cost++; $start = microtime(true); password_hash("test", PASSWORD_BCRYPT, ["cost" =>$cost]); $end = microtime(true); > while (($end - $start) < $timeTarget); echo "Оптимальная стоимость: " . $cost;

Как проверить хэш пароля в PHP?

Делается это при помощи специальной функции, password_verify:

$hash = '$2y$10$ttttttttttttttttttttteRXxHYSDzcdOi/S1JzEheCNMETevWiKC'; if (password_verify('test124', $hash)) < echo 'done'; >else

Пара моментов на которые стоит обратить внимание:
1. Один и тот же пароль, зашифрованный при помощи функции password_hash, всегда будет иметь разный хеш, но при этом, password_verify – раскодирует его именно так как нужно.
2. От одной версии языка PHP к другой, с течением времени, с изменением общей ситуации в мире безопасности, да и вообще, в контексте разного “железа” на вашем сервере, нужно думать наперед, и писать код через password_needs_rehash.

Что такое password_needs_rehash?

password = 'rasmuslerdorf'; $hash = '$2y$10$YCFsG6elYca568hBi2pZ0.3LDL5wjgxct1N8w/oLR/jfHsiQwCqTS'; // Параметр стоимости может изменяться в связи со сменой оборудования $options = array('cost' => 11); // Проверка сохранённого хеша с помощью пароля if (password_verify($password, $hash)) < // Проверяем, не нужно ли использовать более новый алгоритм // или другую алгоритмическую стоимость if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options)) < // Если таки да, перехешируем и сохраняем новый хеш $newHash = password_hash($password, PASSWORD_DEFAULT, $options); >// Авторизуем пользователя >

Опять же, замечательный пример из документации, который будет создавать новый хэш пароля, вам нужно будет перезаписать его в базе данных, в случае таких вот изменений обстоятельств, которые описаны в пункте 2. Изменили ‘cost’ => 13, пожалуйста, вот вам новый пароль).
Долго загружается страница, вернулись на стандартные 10, и снова всё автоматически работает, песня! �� .

И всё таки, может ли владелец сайта расшифровать такой пароль? Ответ – если это md5 или base65, да, запросто, если это password_hash с автоматической солью – то удачи ему �� . Такие пароли, в данный момент времени, считаются надежными.

На этом всё, надеюсь было интересно �� .

Безопасное хеширование паролей

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

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

Почему я должен хешировать пароли пользователей в моем приложении?

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

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

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

Почему популярные хеширующие функции, такие как md5() и sha1() не подходят для паролей?

Такие хеширующие алгоритмы как MD5, SHA1 и SHA256 были спроектированы очень быстрыми и эффективными. При наличии современных технологий и оборудования, стало довольно просто выяснить результат этих алгоритмов методом "грубой силы" для определения оригинальных вводимых данных.

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

Если популярные хеширующие функции не подходят, как же я тогда должен хешировать свои пароли?

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

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

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

Учтите, что, если вы используете функцию crypt() для проверки пароля, то вам нужно предостеречь себя от атак по времени, применяя сравнение строк, которое занимает постоянное время. Ни операторы PHP == и ===, ни функция strcmp() не являются таковыми. Функция же password_verify() как раз делает то, что нужно. Настоятельно рекомендуется использовать встроенное API хеширования паролей, если есть такая возможность.

Что такое соль?

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

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

password_hash() создаёт случайную соль в случае, если она не была передана, и чаще всего это наилучший и безопасный выбор.

Как я должен хранить свою соль?

При использовании функции password_hash() или crypt() , возвращаемое значение уже содержит соль как часть созданного хеша. Это значение нужно хранить как есть в вашей базе данных, так как оно содержит также информацию о хеширующей функции, которая использовалась, и может быть непосредственно передано в функции password_verify() или crypt() при проверке пароля.

Следующая диаграмма показывает формат возвращаемого значения функциями crypt() или password_hash() . Как можно видеть, они содержат полную информацию об алгоритме и соли, требуемых для будущей проверки пароля.

Хеширование пароля на PHP

Хранить пароль в открытом виде - неправильно. Хакер-злоумышленник может получить доступ к вашей базе данных и украсть пароли.

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

Давайте, например, найдем хеш какой-нибудь строки:

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

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

Описанная правка будет представлять собой что-то такое:

Внесем аналогичные правки в авторизацию:

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

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

password_hash

password_hash() создаёт хеш пароля используя сильный, необратимый алгоритм хеширования.

В данный момент поддерживаются следующие алгоритмы:

  • PASSWORD_DEFAULT - используется алгоритм bcrypt (по умолчанию с PHP 5.5.0). Обратите внимание, что используемый алгоритм может со временем меняться на более сильный, когда таковой добавляется в PHP. Соответственно и длина результата может со временем меняться. В связи с этим рекомендуется выбирать длину поля для хранения в базе данных более 60 символов (255 символов могло быть хорошим вариантом).
  • PASSWORD_BCRYPT - использует алгоритм CRYPT_BLOWFISH . Генерирует стандартный хеш, совместимый с генерированным функцией crypt() с использованием идентификатора "$2y$". В результате будет сгенерирована строка длиной 60 символов или false в случае возникновения ошибки.
  • PASSWORD_ARGON2I - Использовать алгоритм хеширования Argon2i. Этот алгоритм доступен только если PHP собран с поддержкой Argon2.
  • PASSWORD_ARGON2ID - Использовать алгоритм хеширования Argon2id. Этот алгоритм доступен только если PHP собран с поддержкой Argon2.

Поддерживаемые опции для PASSWORD_BCRYPT :

    salt ( string ) - для самостоятельного задания соли для хеширования. Обратите внимание, что это приведёт к переопределению и предотвращению автоматического создания соли. Если не задано, то password_hash() будет генерировать случайную соль для каждого хешируемого пароля. Это предпочтительный режим работы.

Внимание Эта опция объявлена устаревшей. Рекомендуется использовать автоматически генерируемую соль. Начиная с PHP 8.0.0 явно заданная соль игнорируется.

Поддерживаемые опции для PASSWORD_ARGON2I и PASSWORD_ARGON2ID :

  • memory_cost ( int ) - Максимальный размер памяти (в килобайтах), которую можно использовать для вычисления хеша Argon2. По умолчанию PASSWORD_ARGON2_DEFAULT_MEMORY_COST .
  • time_cost ( int ) - Максимально возможное время, которое можно потратить для вычисления хеша Argon2. По умолчанию PASSWORD_ARGON2_DEFAULT_TIME_COST .
  • threads ( int ) - Количество потоков, которые можно использовать для вычисления хеша Argon2. По умолчанию PASSWORD_ARGON2_DEFAULT_THREADS .

Внимание Доступно только тогда, когда PHP использует libargon2, но не при реализации libsodium.

Список параметров

Предостережение

Использование алгоритма PASSWORD_BCRYPT приведёт к обрезанию поля password до максимальной длины 72 байта.

Константа, обозначающая используемый алгоритм хеширования пароля.

Ассоциативный массив с опциями. За документацией по поддерживаемым опциям для каждого алгоритма обратитесь к разделу Константы алгоритмов хеширования паролей.

Если не задано, то будет использована стандартная стоимость и соль будет генерироваться автоматически.

Возвращаемые значения

Возвращает хешированный пароль.

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

Список изменений

Версия Описание
8.0.0 password_hash() больше не возвращает значение false в случае возникновения ошибки, вместо этого будет выброшено исключение ValueError , если алгоритм хеширования пароля недействителен или Error , если хеширование пароля не удалось из-за неизвестной ошибки.
8.0.0 Параметр algo теперь допускает значение null .
7.4.0 Параметр algo сейчас ожидает строку ( string ), но всё ещё принимает число ( int ) для обратной совместимости.
7.4.0 Модуль sodium обеспечивает альтернативную реализацию паролей Argon2.
7.3.0 Добавлена поддержка алгоритма хеширования паролей Argon2id с помощью PASSWORD_ARGON2ID .
7.2.0 Добавлена поддержка хеширующего алгоритма Argon2i с помощью PASSWORD_ARGON2I .

Примеры

Пример #1 Пример использования password_hash()

/**
* Мы просто хотим захешировать свой пароль используя настройки по умолчанию.
* Значит будет использован BCRYPT и результат будет 60 символов длиной.
*
* Помните, что алгоритм по умолчанию может измениться в будущем, так что
* имеет смысл заранее позаботиться о том, чтобы система хранения хешей
* смогла хранить более 60 символов (255 в самый раз)
*/
echo password_hash ( "rasmuslerdorf" , PASSWORD_DEFAULT );
?>

Результатом выполнения данного примера будет что-то подобное:

$2y$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a

Пример #2 Пример использования password_hash() с ручным заданием стоимости

/**
* Тут мы увеличиваем алгоритмическую стоимость BCRYPT до 12.
* Но это никак не скажется на длине полученного результата, она останется 60 символов
*/
$options = [
'cost' => 12 ,
];
echo password_hash ( "rasmuslerdorf" , PASSWORD_BCRYPT , $options );
?>

Результатом выполнения данного примера будет что-то подобное:

$2y$12$QjSH496pcT5CEbzjD/vtVeH03tfHKFy36d4J0Ltp3lRtee9HDxY3K

Пример #3 Пример поиска хорошего значения стоимости для password_hash()

/**
* Данный код замерит скорость выполнения операции хеширования для вашего сервера
* с разными значениями алгоритмической сложности для определения максимального
* его значения, не приводящего к деградации производительности. Хорошее базовое
* значение лежит в диапазоне 8-10, но если ваш сервер достаточно мощный, то можно
* задать и больше. Данный скрипт ищет максимальное значение, при котором
* хеширование уложится в 50 миллисекунд.
*/
$timeTarget = 0.05 ; // 50 миллисекунд.

$cost = 8 ;
do $cost ++;
$start = microtime ( true );
password_hash ( "test" , PASSWORD_BCRYPT , [ "cost" => $cost ]);
$end = microtime ( true );
> while (( $end - $start ) < $timeTarget );

echo "Оптимальная стоимость: " . $cost ;
?>

Результатом выполнения данного примера будет что-то подобное:

Оптимальная стоимость: 10

Пример #4 Пример использования password_hash() с Argon2i

echo 'Хеш Argon2i: ' . password_hash ( 'rasmuslerdorf' , PASSWORD_ARGON2I );
?>

Результатом выполнения данного примера будет что-то подобное:

Хеш Argon2i: $argon2i$v=19$m=1024,t=2,p=2$YzJBSzV4TUhkMzc3d3laeg$zqU/1IN0/AogfP4cmSJI1vc8lpXRW9/S0sYY2i2jHT0

Примечания

Предостережение

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

Как было замечено выше, опция salt была объявлена устаревшей в PHP 7.0 и будет вызывать соответствующее предупреждение. Поддержка ручного задания соли может быть удалена в более новых версиях.

Замечание:

Рекомендуется протестировать данную функцию на вашем железе для определения оптимального значения алгоритмической сложности. Убедитесь, что с выбранной сложностью функция выполняется быстрее 100 миллисекунд для интерактивных систем. Скрипт показанный выше поможет вам выбрать подходящее значение.

  • Любой новый алгоритм должен присутствовать в ядре как минимум 1 полный релиз PHP для того, чтобы его можно было установить по умолчанию. Таким образом, если, к примеру, новый алгоритм был добавлен в 7.5.5, то задать по умолчанию его можно будет только в 7.7 (7.6 будет тем самым полным релизом, в течение которого он должен присутствовать, от 7.6.0 до 7.7.0). Но если новый алгоритм добавлен в 7.6.0, то его также можно будет задать по умолчанию в 7.7.0.
  • Алгоритм по умолчанию может быть изменён только в полном релизе (7.3.0, 8.0.0, и т.д.), но не в промежуточных. Единственное исключение - это если в текущем алгоритме найдена критическая уязвимость.

Смотрите также

  • password_verify() - Проверяет, соответствует ли пароль хешу
  • password_needs_rehash() - Проверяет, что указанный хеш соответствует заданным опциям
  • crypt() - Необратимое хеширование строки
  • sodium_crypto_pwhash_str() - Получить ASCII-кодированный хеш

User Contributed Notes 10 notes

10 years ago

There is a compatibility pack available for PHP versions 5.3.7 and later, so you don't have to wait on version 5.5 for using this function. It comes in form of a single php file:
https://github.com/ircmaxell/password_compat

4 years ago

Since 2017, NIST recommends using a secret input when hashing memorized secrets such as passwords. By mixing in a secret input (commonly called a "pepper"), one prevents an attacker from brute-forcing the password hashes altogether, even if they have the hash and salt. For example, an SQL injection typically affects only the database, not files on disk, so a pepper stored in a config file would still be out of reach for the attacker. A pepper must be randomly generated once and can be the same for all users. Many password leaks could have been made completely useless if site owners had done this.

Since there is no pepper parameter for password_hash (even though Argon2 has a "secret" parameter, PHP does not allow to set it), the correct way to mix in a pepper is to use hash_hmac(). The "add note" rules of php.net say I can't link external sites, so I can't back any of this up with a link to NIST, Wikipedia, posts from the security stackexchange site that explain the reasoning, or anything. You'll have to verify this manually. The code:

// register.php
$pepper = getConfigVariable ( "pepper" );
$pwd = $_POST [ 'password' ];
$pwd_peppered = hash_hmac ( "sha256" , $pwd , $pepper );
$pwd_hashed = password_hash ( $pwd_peppered , PASSWORD_ARGON2ID );
add_user_to_database ( $username , $pwd_hashed );
?>

// login.php
$pepper = getConfigVariable ( "pepper" );
$pwd = $_POST [ 'password' ];
$pwd_peppered = hash_hmac ( "sha256" , $pwd , $pepper );
$pwd_hashed = get_pwd_from_db ( $username );
if ( password_verify ( $pwd_peppered , $pwd_hashed )) echo "Password matches." ;
>
else echo "Password incorrect." ;
>
?>

Note that this code contains a timing attack that leaks whether the username exists. But my note was over the length limit so I had to cut this paragraph out.

Also note that the pepper is useless if leaked or if it can be cracked. Consider how it might be exposed, for example different methods of passing it to a docker container. Against cracking, use a long randomly generated value (like in the example above), and change the pepper when you do a new install with a clean user database. Changing the pepper for an existing database is the same as changing other hashing parameters: you can either wrap the old value in a new one and layer the hashing (more complex), you compute the new password hash whenever someone logs in (leaving old users at risk, so this might be okay depending on what the reason is that you're upgrading).

Why does this work? Because an attacker does the following after stealing the database:

password_verify("a", $stolen_hash)
password_verify("b", $stolen_hash)
.
password_verify("z", $stolen_hash)
password_verify("aa", $stolen_hash)
etc.

(More realistically, they use a cracking dictionary, but in principle, the way to crack a password hash is by guessing. That's why we use special algorithms: they are slower, so each verify() operation will be slower, so they can try much fewer passwords per hour of cracking.)

Now what if you used that pepper? Now they need to do this:

password_verify(hmac_sha256("a", $secret), $stolen_hash)

Without that $secret (the pepper), they can't do this computation. They would have to do:

password_verify(hmac_sha256("a", "a"), $stolen_hash)
password_verify(hmac_sha256("a", "b"), $stolen_hash)
.
etc., until they found the correct pepper.

If your pepper contains 128 bits of entropy, and so long as hmac-sha256 remains secure (even MD5 is technically secure for use in hmac: only its collision resistance is broken, but of course nobody would use MD5 because more and more flaws are found), this would take more energy than the sun outputs. In other words, it's currently impossible to crack a pepper that strong, even given a known password and salt.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *