Немного о хэшах и безопасном хранении паролей

в 15:06, , рубрики: cryptography, php, безопасность, пароли, Программирование, хэш-функция, хэши, хэширование, метки: , , , , , , ,

Привет! Сегодня процессе разработки системы авторизации для своего проекта передо мной встал выбор — в каком виде хранить пароли пользователей в базе данных? В голову приходит множество вариантов. Самые очевидные:

  • Хранить пароли в БД в открытом виде.
  • Использовать обычные хэши crc32, md5, sha1
  • Использовать функцию crypt()
  • Использовать статическую «соль», конструкции вида md5(md5($pass))
  • Использовать уникальную «соль» для каждого пользователя.


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

Коллизия хеш-функций

Коллизия хеш-функции возникает, когда она выдает одинаковый результат на разные входные данные. Конечно же, вероятность этого достаточно мала, и зависит от длины хэша. Однако устаревшая (но до сих пор иногда используемая) функция crc32() возвращает в качестве хэша 32-битное целое число. Т.е., чтобы подобрать пароль к такому хэшу, по теории вероятности нужно опробовать 2^32 = 4 294 967 296 различных значений. Даже на моем бесплатном хостинге crc32 работает со скоростью порядка 350 000 раз в секунду — посчитайте сами сколько нужно секунд, чтобы взломать такой хэш ;)

Конечно же это не относится к 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


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


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