Привет! Сегодня процессе разработки системы авторизации для своего проекта передо мной встал выбор — в каком виде хранить пароли пользователей в базе данных? В голову приходит множество вариантов. Самые очевидные:
- Хранить пароли в БД в открытом виде.
- Использовать обычные хэши crc32, md5, sha1
- Использовать функцию crypt()
- Использовать статическую «соль», конструкции вида md5(md5($pass))
- Использовать уникальную «соль» для каждого пользователя.
Первый вариант отпал конечно сразу же. Использование обычных хэшей после недолгого обдумывания тоже пришлось отбросить по ряду причин.
Коллизия хеш-функций
Коллизия хеш-функции возникает, когда она выдает одинаковый результат на разные входные данные. Конечно же, вероятность этого достаточно мала, и зависит от длины хэша. Однако устаревшая (но до сих пор иногда используемая) функция crc32() возвращает в качестве хэша 32-битное целое число. Т.е., чтобы подобрать пароль к такому хэшу, по теории вероятности нужно опробовать 2^32 = 4 294 967 296 различных значений. Даже на моем бесплатном
Конечно же это не относится к md5() (128-битный хеш) и тем более sha1() (160-битный хеш). Использовать их коллизию практически невозможно, хотя есть одна статейка...
Радужные таблицы
Радужные таблицы состоят из хэшей наиболее часто употребляемых паролей — имен, дат рождения, названий животных и т.п. Эти таблицы могут включать миллионы, миллиарды значений, но работа с ними относительно быстра, и проверить хэш на соответствие одному из значений не составляет никакого труда. Частично, от них можно защититься с помощью «соли» или конструкций типа md5(sha1(md5($pass))).
$password = "easypassword"; // простейший пароль, вводимый пользователем и, вероятно, имеющийся в радужной таблице
echo sha1($password); // Хеш такого пароля при обработке функцией sha1() будет следующим: 6c94d3b42518febd4ad747801d50a8972022f956
$salt = "f#@V)Hu^%Hgfds"; // используя случайный набор символов, мы можем изменить значение хеша
echo sha1($salt . $password); // а вот хеш для пароля, сдобренного солью: cd56a16759623378628c0d9336af69b74d9d71a5
// такой хеш не найдётся ни в одной радужной таблице
Радужные таблицы. Часть 2
Статическая соль и тому подобные конструкции могут служить достаточно хорошо… пока структура этих конструкций и соль хранятся в тайне. Если же злоумышленник вызнает секрет хэширования — он с легкостью сможет модифицировать под него свою «радужную таблицу». А т.к. мы не можем абсолютно полагаться на систему защиты своего сервера, нужно искать другой вариант. Одним из решений может быть генерация уникальной соли для каждого юзера, что-то вроде:
$hash = sha1($user_id . $password);
Еще лучше генерировать совсем случайную соль, например так:
// генерируем случайную строку длинной в 22 символа
function unique_salt() {
return substr(sha1(mt_rand()),0,22);
}
$unique_salt = unique_salt();
$hash = sha1($unique_salt . $password); // формируем хеш пароля
Конечно, уникальную соль придется вносить в базу данных, на даже получив доступ к ней, злоумышленник вряд ли сможет сгенерировать несколько миллионов радужных таблиц.
Скорость хэширования
Казалось бы — чем быстрее, тем лучше. Чем быстрее сгенерируется хэш, тем быстрее наш юзер сможет зарегистрироваться и начать уже приносить профит. Однако чем больше скорость хэширования, тем быстрее его сможет подобрать и хакер.
Современные ПК с мощными GPU, могут рассчитывать миллионы хэшей в секунду и больше. А это позволяет ломать пароли простым подбором, с помощью брутфорса-атак. Считаете что пароль в 8 символов достаточно безопасен? Если в пароле используются символы в нижнем и верхнем регистрах и цифры, то общее количество возможных символов составит 62 (26+26+10). Для пароля длиной в 8 символов, существует 62^8 различных комбинаций (порядка 218 триллионов). Со скоростью в 1 миллиард хэшей в секунду (достаточно маленькая для брутфорс-атаки), пароль будет сломан примерно за 60 часов. А для наиболее распространенной длины пароля в 6 символов, длительность расшифровки составит меньше двух минут.
Можно конечно пренебречь пользователями, использующими короткие и простые пароли, или заставить всех в добровольно-принудительном порядке использовать 10-символьные пароли, со знаками препинания и символами канадского алфавита. Но лучше использовать более медленные функции кэширования. Например можно замедлить функцию кэша вручную в 1000 раз с помощью следующего кода:
function myhash($password, $unique_salt) {
$salt = "f#@V)Hu^%Hgfds";
$hash = sha1($unique_salt . $password);
// увеличиваем время исполнения функции в 1000 раз, заставив функцию сперва выполниться 1000 раз, и только затем возвратить результат
for ($i = 0; $i < 1000; $i++) {
$hash = sha1($hash);
}
return $hash;
}
Используя ее, вместо 60 часов, хакер будет ломать 8-символьный пароль около 7 лет. Более удобным вариантом замедления, является использование алгоритма Blowfish, реализованного в PHP через crypt(). Проверить доступность этого алгоритма можно с помощью if CRYPT_BLOWFISH == 1) echo 'its work!'; В PHP 5.3 Blowfish уже включен.
function myhash($password, $unique_salt) {
// соль для blowfish должна быть длинной в 22 символа
return crypt($password, '$2a$10$'.$unique_salt);
}
$2a — это указание на то, что будет использоваться алгоритм Blowfish
$10 — это сила замедления функции. В данном случае равна 2^10. Может принимать значения от 04 до 31
Используем ее на конкретном примере:
$hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC';
$password = "verysecret";
if (check_password($hash, $password)) {
echo "Доступ разрешён!";
} else {
echo "Доступ запрещён!";
}
function check_password($hash, $password) {
// первые 29 символов хеша, включая алгоритм, «силу замедления» и оригинальную «соль» поместим в переменную $full_salt
$full_salt = substr($hash, 0, 29);
// выполним хеш-функцию для переменной $password
$new_hash = crypt($password, $full_salt);
// возвращаем результат («истина» или «ложь»)
return ($hash == $new_hash);
Такой код должен обеспечить максимальную безопасность — подобрать пароль нормальной сложности и длинны (программными методами, конечно) практически невозможно.
Автор: Tairesh