Pull to refresh

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

Reading time4 min
Views70K
Upd. Если вы знаете, что такое BCrypt, можете дальше не читать. Если вы используете PHP 5.5+ то можете прочитать эту статью. Ниже же я изобрел свой велосипед, рабочий, но с двумя рулями, задний запасной. Молод был, горяч.

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

  • Хранить пароли в БД в открытом виде.
  • Использовать обычные хэши 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 'it works!'; В 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); 


Такой код должен обеспечить максимальную безопасность — подобрать пароль нормальной сложности и длины (программными методами, конечно) практически невозможно.
Tags:
Hubs:
+43
Comments100

Articles

Change theme settings