GrabDuck

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

:

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

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

В этой статье я постараюсь излагаться как можно проще, на тему прочного метода хранения паролей в web-приложениях

Хеширование превращает данные в набор строковых и целочисленных элементов.

Это происходит благодаря одностороннему хэшированию. "Одностороннее" означает, что произвести обратное преобразование ну очень уж сложно или вовсе невозможно.

Самая распространённая хэш функция это md5():

$data = "Hello World";
$hash = md5($data);
echo $hash; // b10a8db164e0754105b7a99be72e3fe5

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

Обычный процесс регистрации:

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

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

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

Процесс хэширования пароля будет изложен далее в этой статье.

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

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

Как это можно использовать?

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

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

echo crc32('supersecretpassword');
// на выходе: 323322056

Теперь, давайте поиграем в злодея, который украл базу данных вместе с хэшированным паролем. У нас нет возможности преобразовать 323322056 в ‘supersecretpassword’, однако, благодаря простому скрипту мы можем подобрать другой пароль, который в хэшированном виде будет точно такой же как и тот, который находится в базе:

set_time_limit(0);
$i = 0;
while (true) {

	if (crc32(base64_encode($i)) == 323322056) {
		echo base64_encode($i);
		exit;
	}

	$i++;
}

Этому скрипту конечно нужно время, но в конце концов он вернёт строку. Теперь мы можем использовать строку, которую получили — вместо ‘supersecretpassword’ — что позволит нам зайти в систему от имени пользователя у которого был этот пароль.

Например вот этот скрипт через несколько мгновений возвратил мне строчку ‘MTIxMjY5MTAwNg==‘. Давайте протестируем:

echo crc32('supersecretpassword');
// на выходе: 323322056

echo crc32('MTIxMjY5MTAwNg==');
// на выходе: 323322056

Как это можно предотвратить?

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

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

Sha1

Sha1() это лучшая альтернатива т.к. она возвращает 160-битный хэш.

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

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

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

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

Вот такие огромные Радужные Таблицы могут быть составлены и использованы.

Как это можно использовать:

Давайте представим, что у нас в руках база с десятками тысяч паролей. Особого труда не составит, чтобы сравнить их с значениями из Радужной таблицы. Конечно же не все пароли совпадут, но в конечном итоге парочка другая найдётся!

Как можно себя защитить:

Просто добавим "соли":

$password = "easypassword";

// такой пароль может найтись в Радужной таблице
// т.к. пароль содержит два распространённых слова
echo sha1($password); // 6c94d3b42518febd4ad747801d50a8972022f956

// для соли используем любое количество случайных символов
$salt = "f#@V)Hu^%Hgfds";

// такой хэш никогда не будет найден в Радужных таблицах 
echo sha1($salt . $password); // cd56a16759623378628c0d9336af69b74d9d71a5

Всё, что нужно сделать, это сконкатенировать “соль” и пароль перед хэшированием. Навряд ли в Радужных таблицах найдётся такое значение. Но мы всё ещё в опасности!

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

Как это можно использовать?

Если вы создали соль, то при краже базы она попадёт в руки злоумышленникам. Всё, что им останется сделать это сгенерировать новую Радужную таблицу с "солями", которые они получили из базы.

К примеру в Радужной таблице есть хэш строки “easypassword”. В новой Радужной таблице вместо прошлого значения у них будет содержаться строка “f#@V)Hu^%Hgfdseasypassword”. Когда они запустят скрипт, то снова могут получить некоторые совпадения.

Как защититься?

Мы можем использовать “уникальную соль” которая будет разной для каждого пользователя.

Дополнением к соли для того, чтоб она стала уникальной может стать id пользователя:

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

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

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

// сгенерируем строку длиной 22 символа
function unique_salt() {
	return substr(sha1(mt_rand()),0,22);
}

$unique_salt = unique_salt();

$hash = sha1($unique_salt . $password);

// сохраним $unique_salt в записи пользователя
// ...

Этот метод защищает нас от Радужных таблиц, т.к. у каждого пароля есть своя уникальная соль. Атакующему придётся создать 10 миллионов отдельных Радужных таблиц, что на практике невыполнимо.

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

Как это использовать?

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

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

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

Для пароля длиной 6 символов та же самая операция будет длиться более 1 минуты.

Не стесняйтесь требовать от пользователей пароли длиной 9 или 10 символов, хотя это и будет их нервировать.

Как защищаться?

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

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

Первый вариант самому создать такую функцию:

function myhash($password, $unique_salt) {

	$salt = "f#@V)Hu^%Hgfds";
	$hash = sha1($unique_salt . $password);

	// делать в 1000 раз дольше
	for ($i = 0; $i < 1000; $i++) {
		$hash = sha1($hash);
	}

	return $hash;
}

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

function myhash($password, $unique_salt) {
	// соль для blowfish должна быть на 22 символа больше
	return crypt($password, '$2a$10$'.$unique_salt);
}

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

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

Второе значение '$10'. В этом случает это "cost параметр". Это параметр представляет собой количество итераций, которые будут производиться (10 => 2^10 = 1024 итераций.) Значение может быть от 04 до 31.

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

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());
// результат: $2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC

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

// допустим, что мы получили это из базы
$hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC';

// предположим, что пользователь ввёл пароль "verysecret"
$password = "verysecret";

if (check_password($hash, $password)) {
	echo "Доступ разрешён!";
} else {
	echo "Доступ запрещён!";
}

function check_password($hash, $password) {
	// первые 29 символов это алгоритм, cost и соль
	$full_salt = substr($hash, 0, 29);

	// запустим хэш функцию для $password
	$new_hash = crypt($password, $full_salt);

	// вернём true или false
	return ($hash == $new_hash);
}

Если мы это запустим, то получим сообщение "Доступ разрешён!"

Учитывая всё, что мы узнали, напишем класс:

class PassHash {

	// blowfish
	private static $algo = '$2a';

	// cost параметр
	private static $cost = '$10';

	// для наших нужд
	public static function unique_salt() {
		return substr(sha1(mt_rand()),0,22);
	}

	// генерация хэша
	public static function hash($password) {
		return crypt($password,
					self::$algo .
					self::$cost .
					'$' . self::unique_salt());

	}

	// сравнение пароля и хэша
	public static function check_password($hash, $password) {
		$full_salt = substr($hash, 0, 29);

		$new_hash = crypt($password, $full_salt);

		return ($hash == $new_hash);
	}
}

Применяем при регистрации:

// инклудим class
require ("PassHash.php");

// читаем $_POST
// ...

// валидируем все поля
// ...

// хэшируем пароль
$pass_hash = PassHash::hash($_POST['password']);

// сохраняем всё в БД, кроме $_POST['password']
// вместо которого у нас теперь $pass_hash
// ...

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

// инклудим class
require ("PassHash.php");

// читаем $_POST
// ...

// достаём запись из базы по $_POST['username'] или чему-то другому
// ...

// проверяем пароль, который был введён пользователем
if (PassHash::check_password($user['pass_hash'], $_POST['password'])) {
	// доступ открыт
	// ...
} else {
	// доступ закрыт
	// ...
}

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

if (CRYPT_BLOWFISH == 1) {
	echo "Да";
} else {
	echo "Нет";
}

Однако, начиная с PHP 5.3, этот алгоритм встроен по умолчанию.

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

Вопрос к вам: как вы хэшируете ваши пароли? Что думаете по поводу методов в данной статье?