Зачем нужны хэш функции, и как сохранить пароли в секрете


Время от времени хотим мы этого или нет, но происходят взломы серверов баз данных. Учитывая это, важно быть уверенным в том, что конфиденциальные данные, такие как пароли пользователей, не будут раскрыты. Сегодня, мы осветим тему хэширования и мер по защите паролей в вашем web приложении. 1. Пару слов перед тем, как начнём Криптология это достаточно сложная тема в которой я далеко не эксперт. По сей день различные университеты и организации по безопасности занимаются исследованиями в этой области. В этой статье я постараюсь излагаться как можно проще, на тему прочного метода хранения паролей в web-приложениях 2. Зачем нужно "Хэширование"? Хеширование превращает данные в набор строковых и целочисленных элементов. Это происходит благодаря одностороннему хэшированию. "Одностороннее" означает, что произвести обратное преобразование ну очень уж сложно или вовсе невозможно. Самая распространённая хэш функция это md5(): 1 $data = "Hello World"; 2 $hash = md5($data); 3 echo $hash; // b10a8db164e0754105b7a99be72e3fe5 Применяя md5(), вы всегда будете получать в качестве результата строку размером 32 символа. Но эти символы будут в шестнадцатеричном виде; технически хэш может представлять собой и 128-битовое целое. Вы можете помещать в функцию md5() строки и числа любой длины, но на выходе всегда будете получать результат в 32 символа. Уже только этот факт хорошее подтверждение тому, что это "односторонняя" функция. 3. Использование хэш функций для хранения паролей Обычный процесс регистрации: Пользователь заполняет форму регистрации, включая поле "Пароль". Скрипт-обработчик помещает эти данные в базу данных. Перед записью в базу, пароль обрабатывается хэш функцией. Оригинальное значение пароля нигде не используется. Процесс входа в систему: Пользователь вводит свой логин и пароль. Скрипт-обработчик хэширует пароль, который ввёл пользователь. Скрипт находит запись в базе данных, и считывает значения пароля, который хранится в ней. Пароль из базы и пароль введённый пользователем сравниваются, и если они совпадают (в хэшированном виде), то пользователя впускают в систему. Процесс хэширования пароля будет изложен далее в этой статье. Заметьте, что оригинальное значение пароли нигде не сохранялось. Если база данных попадёт к злоумышленникам, то они не смогуть увидеть пароли, так? Да не совсем... Давайте посмотрим на потенциальные "дыры". 4. Проблема #1: Коллизии “Коллизии” возникают тогда когда при хэшировании двух данных разного типа, получается один и тот же результат. Вообще-то это зависит от того какую функцию вы используете. Как это можно использовать? К примеру, я видел несколько устаревшие скрипты, где для хэширования пароля использовалась функция crc32(). Эта функция возвращает в качестве результат 32-битное целое. Это означает, что на выходе может быть только 2^32 (или 4,294,967,296) возможных вариантов. Давайте захэшируем пароль: 1 echo crc32('supersecretpassword'); 2 // на выходе: 323322056 Теперь, давайте поиграем в злодея, который украл базу данных вместе с хэшированным паролем. У нас нет возможности преобразовать 323322056 в ‘supersecretpassword’, однако, благодаря простому скрипту мы можем подобрать другой пароль, который в хэшированном виде будет точно такой же как и тот, который находится в базе: 01 set_time_limit(0); 02 $i = 0; 03 while (true) { 04 05 if (crc32(base64_encode($i)) == 323322056) { 06 echo base64_encode($i); 07 exit; 08 } 09 10 $i++; 11 } Этому скрипту конечно нужно время, но в конце концов он вернёт строку. Теперь мы можем использовать строку, которую получили — вместо ‘supersecretpassword’ — что позволит нам зайти в систему от имени пользователя у которого был этот пароль. Например вот этот скрипт через несколько мгновений возвратил мне строчку ‘MTIxMjY5MTAwNg==‘. Давайте протестируем: 1 echo crc32('supersecretpassword'); 2 // на выходе: 323322056 3 4 echo crc32('MTIxMjY5MTAwNg=='); 5 // на выходе: 323322056 Как это можно предотвратить? В наши дни, даже на самом простом домашнем компьютере можно использовать миллиарды хэш функций в секунду. Поэтому нам нужна такая хэш функция, которая сгенерировала как можно большее значение. К примеру можно использовать md5(), которая генерирует 128-битные хэши. Таким образом вариантов подбора становится намного больше 340,282,366,920,938,463,463,374,607,431,768,211,456. Пробег по всем итерациям с целью нахождения коллизии невозможен. Однако некоторым людям всё же удаётся найти "дыры" дополнительно об этом тут). Sha1 Sha1() это лучшая альтернатива т.к. она возвращает 160-битный хэш. 5. Проблема #2: Радужная таблица Даже если мы разобрались с коллизиями, это не значит, что мы обезопасили себя со всех сторон. Радужная таблица строится путём вычисления хэш-значения наиболее часто используемых слов и словосочетаний. Такие таблицы могут содержать миллионы, а то и миллиарды строк. К примеру, для создания такой таблицы можно пройтись по словарю и создать хэш для каждого слова. Так же можно создавать хэши для комбинации слов. Но и это не всё; вы можете так же вставлять цифры перед/после/между слов, и тоже записывать значение таких хэшей в таблицу. Вот такие огромные Радужные Таблицы могут быть составлены и использованы. Как это можно использовать: Давайте представим, что у нас в руках база с десятками тысяч паролей. Особого труда не составит, чтобы сравнить их с значениями из Радужной таблицы. Конечно же не все пароли совпадут, но в конечном итоге парочка другая найдётся! Как можно себя защитить: Просто добавим "соли": 01 $password = "easypassword"; 02 03 // такой пароль может найтись в Радужной таблице 04 // т.к. пароль содержит два распространённых слова 05 echo sha1($password); // 6c94d3b42518febd4ad747801d50a8972022f956 06 07 // для соли используем любое количество случайных символов 08 $salt = "f#@V)Hu^%Hgfds"; 09 10 // такой хэш никогда не будет найден в Радужных таблицах 11 echo sha1($salt . $password); // cd56a16759623378628c0d9336af69b74d9d71a5 Всё, что нужно сделать, это сконкатенировать “соль” и пароль перед хэшированием. Навряд ли в Радужных таблицах найдётся такое значение. Но мы всё ещё в опасности! 6. Проблема #3: И снова Радужные таблицы Помните, что Радужные таблицы могут быть сформированы уже после того, как база будет украдена. Как это можно использовать? Если вы создали соль, то при краже базы она попадёт в руки злоумышленникам. Всё, что им останется сделать это сгенерировать новую Радужную таблицу с "солями", которые они получили из базы. К примеру в Радужной таблице есть хэш строки “easypassword”. В новой Радужной таблице вместо прошлого значения у них будет содержаться строка “f#@V)Hu^%Hgfdseasypassword”. Когда они запустят скрипт, то снова могут получить некоторые совпадения. Как защититься? Мы можем использовать “уникальную соль” которая будет разной для каждого пользователя. Дополнением к соли для того, чтоб она стала уникальной может стать id пользователя: 1 $hash = sha1($user_id . $password); Это само собой подразумевает, что id пользователя никогда не будет меняться. Так же мы можем сгенерировать случайную строку для каждого отдельного пользователя, чем получим "уникальную соль". Такую соль нужно хранить там же, где находится запись о пользователе. 01 // сгенерируем строку длиной 22 символа 02 function unique_salt() { 03 return substr(sha1(mt_rand()),0,22); 04 } 05 06 $unique_salt = unique_salt(); 07 08 $hash = sha1($unique_salt . $password); 09 10 // сохраним $unique_salt в записи пользователя 11 // ... Этот метод защищает нас от Радужных таблиц, т.к. у каждого пароля есть своя уникальная соль. Атакующему придётся создать 10 миллионов отдельных Радужных таблиц, что на практике невыполнимо. 7. Проблема #4: Скорость выполнения Большинство функций хэширования разрабатывались, учитывая то, что они часто используются для расчёта контрольных сумм каких-то значений или файлов с проверкой целостности данных. Как это использовать? Как я говорил ранее, компьютер с мощной графической картой может высчитывать миллиарды хэшей за секунду. Злоумышленники могут применить "грубую силу", проверяя каждый единственно возможный пароль (проводя полный перебор всех возможных вариантов). Если вы думаете, что пароль из 8 символов может устоять перед "грубой атакой", то представьте: Если пароль содержит прописные, заглавные буквы и цифры, это всего лишь 62 (26+26+10) возможных символа. Строка из 8 символов содержит 62^8 вариантов комбинаций. Это чуть больше 218 триллионов. Если обрабатывать 1 миллиард хэшей за секунду, пароль будет подобран за 60 часов. Для пароля длиной 6 символов та же самая операция будет длиться более 1 минуты. Не стесняйтесь требовать от пользователей пароли длиной 9 или 10 символов, хотя это и будет их нервировать. Как защищаться? Используйте медленные хэш функции Представьте себе, что вы используете хэш функцию, которая генерирует 1 миллион хэшей в секунду вместо 1 миллиарда. Атакующему предётся в 1000 раз дольше подбирать пароли. 60 часов превратятся в 7 лет! Первый вариант самому создать такую функцию: 01 function myhash($password, $unique_salt) { 02 03 $salt = "f#@V)Hu^%Hgfds"; 04 $hash = sha1($unique_salt . $password); 05 06 // делать в 1000 раз дольше 07 for ($i = 0; $i < 1000; $i++) { 08 $hash = sha1($hash); 09 } 10 11 return $hash; 12 } Или вы можете использовать алгоритм, который использует "cost параметр," такой как BLOWFISH. В PHP, это может быть реализовано с помощью метода crypt(). 1 function myhash($password, $unique_salt) { 2 // соль для blowfish должна быть на 22 символа больше 3 return crypt($password, '$2a$10$'.$unique_salt); 4 } Второй параметр в методе crypt() содержит значения, разделённые знаками доллара ($). Первое значение это '$2a', которое говорит, что мы будем использовать алгоритм BLOWFISH. Второе значение '$10'. В этом случает это "cost параметр". Это параметр представляет собой количество итераций, которые будут производиться (10 => 2^10 = 1024 итераций.) Значение может быть от 04 до 31. Давайте запустим пример: 01 function myhash($password, $unique_salt) { 02 return crypt($password, '$2a$10$'.$unique_salt); 03 04 } 05 function unique_salt() { 06 return substr(sha1(mt_rand()),0,22); 07 } 08 09 $password = "verysecret"; 10 11 echo myhash($password, unique_salt()); 12 // результат: $2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC В результате у нас получился хэш, который содержит алгоритм ($2a), cost параметр ($10), и соль длиной 22 символа. Всё остальное это хэш. Протестируем: 01 // допустим, что мы получили это из базы 02 $hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; 03 04 // предположим, что пользователь ввёл пароль "verysecret" 05 $password = "verysecret"; 06 07 if (check_password($hash, $password)) { 08 echo "Доступ разрешён!"; 09 } else { 10 echo "Доступ запрещён!"; 11 } 12 13 function check_password($hash, $password) { 14 // первые 29 символов это алгоритм, cost и соль 15 $full_salt = substr($hash, 0, 29); 16 17 // запустим хэш функцию для $password 18 $new_hash = crypt($password, $full_salt); 19 20 // вернём true или false 21 return ($hash == $new_hash); 22 } Если мы это запустим, то получим сообщение "Доступ разрешён!" 8. Собираем всё в кучу Учитывая всё, что мы узнали, напишем класс: 01 class PassHash { 02 03 // blowfish 04 private static $algo = '$2a'; 05 06 // cost параметр 07 private static $cost = '$10'; 08 09 // для наших нужд 10 public static function unique_salt() { 11 return substr(sha1(mt_rand()),0,22); 12 } 13 14 // генерация хэша 15 public static function hash($password) { 16 return crypt($password, 17 self::$algo . 18 self::$cost . 19 '$' . self::unique_salt()); 20 21 } 22 23 // сравнение пароля и хэша 24 public static function check_password($hash, $password) { 25 $full_salt = substr($hash, 0, 29); 26 27 $new_hash = crypt($password, $full_salt); 28 29 return ($hash == $new_hash); 30 } 31 } Применяем при регистрации: 01 // инклудим class 02 require ("PassHash.php"); 03 04 // читаем $_POST 05 // ... 06 07 // валидируем все поля 08 // ... 09 10 // хэшируем пароль 11 $pass_hash = PassHash::hash($_POST['password']); 12 13 // сохраняем всё в БД, кроме $_POST['password'] 14 // вместо которого у нас теперь $pass_hash 15 // ... Использование при входе пользователя в систему: 01 // инклудим class 02 require ("PassHash.php"); 03 04 // читаем $_POST 05 // ... 06 07 // достаём запись из базы по $_POST['username'] или чему-то другому 08 // ... 09 10 // проверяем пароль, который был введён пользователем 11 if (PassHash::check_password($user['pass_hash'], $_POST['password'])) { 12 // доступ открыт 13 // ... 14 } else { 15 // доступ закрыт 16 // ... 17 } 9. Проверка на возможность использования Blowfish Алгоритм Blowfish может и не быть реализован на всех системах, хоть и очень популярен. Проверка на возможность использования: 1 if (CRYPT_BLOWFISH == 1) { 2 echo "Да"; 3 } else { 4 echo "Нет"; 5 } Однако, начиная с PHP 5.3, этот алгоритм встроен по умолчанию. Заключение Этого метода хэширования вполне достаточно для большинства web-приложений. Так же не забывайте: что вы в праве требовать минимальную длину пароля от своих пользователей. Вы так же можете перемешивать символы, цифры и специальные знаки. Вопрос к вам: как вы хэшируете ваши пароли? Что думаете по поводу методов в данной статье?