Хэш-функции и безопасность паролей в веб-приложении

в 11:55, , рубрики: hash, php, web, метки: , ,

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

Информация об ограничении ответственности

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

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

Что же такое «Хэширование»

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

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

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

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

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

Использование хэш-функций для дальнейшего хранения паролей

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

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

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

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

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

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

Проблема первая. Коллизия хэшей

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

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

Например, я видел некоторые старые скрипты, которые используют crc32() для хэширования паролей. Эта функция генерирует 32-разрядное целое число в качестве результата. Это означает, что существует только 232 (т.е. 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-битное хэш-значение.

Проблема вторая. Радужные таблицы

Даже если мы предотвратили коллизию, наши данные всё ещё находяться в опасности.

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

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

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

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

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

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

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

Можно попробовать добавить “соль”:

$password = "easypassword";

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

// поможет использование соли
$salt = "f#@V)Hu^%Hgfds";

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

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

Проблема третья. И снова радужные таблицы

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

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

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

Например, в уже сгенерированных радужных таблицах “easypassword” может существовать. Но в новой радужной таблице, будет уже “f#@V)Ху^%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-ми символьная строкам имеет 628 возможных вариантов. То есть чуть более 218 трлн.
  • Со скоростью 1 млрд. хэшей в секунду, потребуется около 60 часов для подбора пароля.

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

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

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

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

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

Один из способов вызвать функцию хэширования 1000 раз:

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;
}

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

function myhash($password, $unique_salt) {

	// соль для blowfish должна быть длиной в 22 символа

	return crypt($password, '$2a$10$'.$unique_salt);

}

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

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

Второе значение, '$10-в данном случае, — «cost parameter». Это логарифм по основанию 2, сколько итераций, он будет производить (10 => 210 = 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 parameter ($10), и 22 символьную соль. Давайте проведём тест:

// предположим это мы вытащили из базы
$hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC';

// предположим пароль, введённый пользователем для входа
$password = "verysecret";

if (check_password($hash, $password)) {
	echo "Access Granted!";
} else {
	echo "Access Denied!";
}
function check_password($hash, $password) {

	// первые 29 символов включают в себя соль,  cost parameter и алгоритм
	// давайте назовёмих $full_salt
	$full_salt = substr($hash, 0, 29);

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

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

Когда мы выполним этот скрипт, то увидим: «Access Granted!»

Сложим все Вместе

Давайте напишем класс утилит, основанных на том, что мы узнали только что:

class PassHash {

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

	// cost parameter
	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);

	}

}

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

// подключаем класс
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
// ...

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

// подключаем класс
require ("PassHash.php");

// считываем всё из $_POST
// ...

// сортируем пользовательские записи основаные на $_POST['username']
// ...

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

Примечание

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

if (CRYPT_BLOWFISH == 1) {
	echo "Yes";
} else {
	echo "No";
}

Если у Вас PHP 5.3, Вам не нужно беспокоиться. PHP поставляется с BLOWFISH, начиная с этой версии.

Заключение

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

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

Источник

http://net.tutsplus.com/

Автор: ppa

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js