Статьи

Понимание хеш-функций и безопасность паролей

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

Переизданный учебник

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


Криптология — достаточно сложный предмет, и я ни в коем случае не эксперт. В этой области постоянно проводятся исследования, во многих университетах и ​​службах безопасности.

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


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

Это достигается с помощью односторонней хэш-функции. «Односторонний» означает, что его очень трудно (или практически невозможно) повернуть вспять.

Типичным примером хеш-функции является md5 () , которая довольно популярна во многих разных языках и системах.

1
2
3
$data = «Hello World»;
$hash = md5($data);
echo $hash;

С md5() результатом всегда будет строка длиной 32 символа. Но он содержит только шестнадцатеричные символы; технически это также может быть представлено как 128-битное (16-байтовое) целое число. Вы можете использовать md5() намного длиннее строк и данных, и у вас все равно останется хеш такой длины. Уже один этот факт может дать вам подсказку о том, почему это считается «односторонней» функцией.


Обычный процесс при регистрации пользователя:

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

И процесс входа в систему:

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

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

Обратите внимание, что оригинальный пароль нигде не был сохранен. Если база данных украдена, логины пользователей не могут быть скомпрометированы, верно? Ну, ответ «это зависит». Давайте посмотрим на некоторые потенциальные проблемы.


«Столкновение» хэша происходит, когда два разных входа данных генерируют один и тот же результирующий хэш. Вероятность этого зависит от того, какую функцию вы используете.

В качестве примера я видел несколько старых скриптов, которые использовали crc32 () для хэширования паролей. Эта функция генерирует 32-битное целое в результате. Это означает, что есть только 2 ^ 32 (то есть 4 294 967 296) возможных результатов.

Давайте хешируем пароль:

1
2
echo crc32(‘supersecretpassword’);
// outputs: 323322056

Теперь давайте примем на себя роль человека, который украл базу данных и имеет хеш-значение. Возможно, нам не удастся преобразовать 323322056 в «supersecretpassword», однако мы можем найти другой пароль, который преобразуется в то же значение хеша, с помощью простого сценария:

01
02
03
04
05
06
07
08
09
10
11
set_time_limit(0);
$i = 0;
while (true) {
 
    if (crc32(base64_encode($i)) == 323322056) {
        echo base64_encode($i);
        exit;
    }
 
    $i++;
}

Это может продолжаться некоторое время, хотя, в конце концов, оно должно вернуть строку. Мы можем использовать эту возвращенную строку — вместо «supersecretpassword» — и это позволит нам успешно войти в учетную запись этого человека.

Например, после выполнения этого точного сценария на моем компьютере в течение нескольких мгновений мне дали « MTIxMjY5MTAwNg== ». Давайте проверим это:

1
2
3
4
5
echo crc32(‘supersecretpassword’);
// outputs: 323322056
 
echo crc32(‘MTIxMjY5MTAwNg==’);
// outputs: 323322056

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

Например, может подойти md5() , поскольку он генерирует 128-битные хэши. Это приводит к 340 282 366 920 938 463 463 374 607 431 768 211 456 возможных результатов. Невозможно пройти через так много итераций, чтобы найти столкновения. Однако некоторые люди все еще находят способы сделать это (см. Здесь ).

Sha1 () — лучшая альтернатива, и она генерирует еще более длинное 160-битное хеш-значение.


Даже если мы исправим проблему столкновения, мы все еще не в безопасности.

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

Эти таблицы могут содержать до миллионов или даже миллиардов строк.

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

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

Давайте представим, что украдена большая база данных вместе с 10 миллионами хэшей паролей. Довольно легко найти радужный стол для каждого из них. Конечно, не все они будут найдены, но, тем не менее … некоторые найдутся!

Мы можем попробовать добавить «соль». Вот пример:

01
02
03
04
05
06
07
08
09
10
11
$password = «easypassword»;
 
// this may be found in a rainbow table
// because the password contains 2 common words
echo sha1($password);
 
// use bunch of random characters, and it can be longer than this
$salt = «f#@V)Hu^%Hgfds»;
 
// this will NOT be found in any pre-built rainbow table
echo sha1($salt . $password);

Что мы в основном делаем, это объединяем строку «соль» с паролями перед их хэшированием. Результирующая строка, очевидно, не будет ни на одном заранее построенном радужном столе. Но мы все еще не в безопасности!


Помните, что Радужная таблица может быть создана с нуля после кражи базы данных.

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

Например, в общей таблице Rainbow может существовать « easypassword ». Но в этой новой Радужной Таблице у них также есть » f#@V)Hu^%Hgfdseasypassword «. Когда они запустят все 10 миллионов украденных соленых хэшей против этой таблицы, они снова смогут найти несколько совпадений.

Вместо этого мы можем использовать «уникальную соль», которая меняется для каждого пользователя.

Кандидатом на этот тип соли является значение идентификатора пользователя из базы данных:

1
$hash = sha1($user_id . $password);

Это предполагает, что идентификационный номер пользователя никогда не меняется, как это обычно бывает.

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

01
02
03
04
05
06
07
08
09
10
11
12
// generates a 22 character long random string
function unique_salt() {
 
    return substr(sha1(mt_rand()),0,22);
}
 
$unique_salt = unique_salt();
 
$hash = sha1($unique_salt . $password);
 
// and save the $unique_salt with the user record
// …

Этот метод защищает нас от Rainbow Tables, потому что теперь каждый пароль был добавлен с другим значением. Атакующий должен будет сгенерировать 10 миллионов отдельных Радужных Столов, что было бы совершенно непрактично.


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

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

Вы можете подумать, что требование пароля длиной не менее 8 символов может защитить его от атаки грубой силы, но давайте определимся, действительно ли это так:

  • Если пароль может содержать строчные, прописные буквы и цифры, то есть 62 (26 + 26 + 10) возможных символов.
  • Строка длиной 8 символов имеет 62 ^ 8 возможных версий. Это чуть более 218 трлн.
  • Со скоростью 1 миллиард хэшей в секунду это можно решить примерно за 60 часов.

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

Не стесняйтесь требовать пароли длиной 9 или 10 символов, однако вы можете начать раздражать некоторых из ваших пользователей.

Используйте более медленную хэш-функцию.

Представьте, что вы используете хеш-функцию, которая может запускаться только на одном оборудовании 1 миллион раз в секунду вместо 1 миллиарда раз в секунду. Затем злоумышленнику потребуется в 1000 раз больше времени, чтобы перебрать хэш. 60 часов превратились бы почти в 7 лет!

Один из способов сделать это — реализовать это самостоятельно:

01
02
03
04
05
06
07
08
09
10
11
12
function myhash($password, $unique_salt) {
 
    $salt = «f#@V)Hu^%Hgfds»;
    $hash = sha1($unique_salt . $password);
 
    // make it take 1000 times longer
    for ($i = 0; $i < 1000; $i++) {
        $hash = sha1($hash);
    }
 
    return $hash;
}

Или вы можете использовать алгоритм, который поддерживает «параметр стоимости», такой как BLOWFISH. В PHP это можно сделать с помощью функции crypt() .

1
2
3
4
5
6
7
function myhash($password, $unique_salt) {
 
    // the salt for blowfish should be 22 characters long
 
    return crypt($password, ‘$2a$10$’.$unique_salt);
 
}

Второй параметр функции crypt() содержит некоторые значения, разделенные знаком доллара ($).

Первое значение — $ 2a, что указывает на то, что мы будем использовать алгоритм BLOWFISH.

Второе значение, в данном случае «$ 10», является «параметром стоимости». Это логарифм с основанием 2 от того, сколько итераций он будет выполнять (10 => 2 ^ 10 = 1024 итераций.) Это число может находиться в диапазоне от 04 до 31.

Давайте запустим пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
function myhash($password, $unique_salt) {
    return crypt($password, ‘$2a$10$’.$unique_salt);
 
}
function unique_salt() {
    return substr(sha1(mt_rand()),0,22);
}
 
 
$password = «verysecret»;
 
echo myhash($password, unique_salt());
// result: $2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC

Полученный хэш содержит алгоритм ($ 2a), параметр стоимости ($ 10) и соль из 22 символов, которая была использована. Остальное это вычисленный хеш. Давайте запустим тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// assume this was pulled from the database
$hash = ‘$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC’;
 
// assume this is the password the user entered to log back in
$password = «verysecret»;
 
if (check_password($hash, $password)) {
    echo «Access Granted!»;
} else {
    echo «Access Denied!»;
}
 
 
function check_password($hash, $password) {
 
    // first 29 characters include algorithm, cost and salt
    // let’s call it $full_salt
    $full_salt = substr($hash, 0, 29);
 
    // run the hash function on $password
    $new_hash = crypt($password, $full_salt);
 
    // returns true or false
    return ($hash == $new_hash);
}

Когда мы запускаем это, мы видим «Доступ разрешен!»


Имея в виду все вышесказанное, давайте напишем служебный класс, основанный на том, что мы узнали до сих пор:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class PassHash {
 
    // blowfish
    private static $algo = ‘$2a’;
 
    // cost parameter
    private static $cost = ‘$10’;
 
 
    // mainly for internal use
    public static function unique_salt() {
        return substr(sha1(mt_rand()),0,22);
    }
 
    // this will be used to generate a hash
    public static function hash($password) {
 
        return crypt($password,
                    self::$algo .
                    self::$cost .
                    ‘$’ .
 
    }
 
 
    // this will be used to compare a password against a hash
    public static function check_password($hash, $password) {
 
        $full_salt = substr($hash, 0, 29);
 
        $new_hash = crypt($password, $full_salt);
 
        return ($hash == $new_hash);
 
    }
 
}

Вот использование при регистрации пользователя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// include the class
require («PassHash.php»);
 
// read all form input from $_POST
// …
 
// do your regular form validation stuff
// …
 
// hash the password
$pass_hash = PassHash::hash($_POST[‘password’]);
 
// store all user info in the DB, excluding $_POST[‘password’]
// store $pass_hash instead
// …

А вот использование во время процесса входа пользователя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// include the class
require («PassHash.php»);
 
// read all form input from $_POST
// …
 
// fetch the user record based on $_POST[‘username’] or similar
// …
 
// check the password the user tried to login with
if (PassHash::check_password($user[‘pass_hash’], $_POST[‘password’]) {
    // grant access
    // …
} else {
    // deny access
    // …
}

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

1
2
3
4
5
if (CRYPT_BLOWFISH == 1) {
    echo «Yes»;
} else {
    echo «No»;
}

Однако, начиная с PHP 5.3, вам не нужно беспокоиться; PHP поставляется с этой встроенной реализацией.


Этот метод хеширования паролей должен быть достаточно надежным для большинства веб-приложений. Тем не менее, не забывайте: вы также можете требовать, чтобы ваши члены использовали более надежные пароли, применяя минимальные длины, смешанные символы, цифры и специальные символы.

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