Блог


Функция хэширования паролей

Ни для кого не секрет, что нельзя держать пароль в базе в открытом виде. Это чревато неприятностями для пользователей (а бумерангом и для владельца ресурса), если вдруг база станет достоянием жулика. Дело в том, что многие ленятся или не надеются на свою память, и используют один и тот же пароль на разных ресурсах. И получается, что попав в руки к злоумышленнику, пароль для вашего ресурса становится ключом к другим: к аккаунтам в социальных сетях, к электронной почте, вебмани... 

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

Отсюда вывод - хэшировать нужно по более сложной технологии, нежели MD5 или SHA1. Делается это добавлением соли (секретной строки) к паролю, и использованием последовательного хэширования. Если допустим получить хэш по такому алгоритму:

1
 2
 3
 4


    $hash 
md5(sha1(md5($password $salt) . $salt). $salt);

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

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


Вот такая функция и предоставит эту возможность. Вторым параметром нужно передать соль, третьим - алгоритм шифрования. Алгоритм прост, набор цифр от 1 до 5 определяет последовательность алгоритмов хэширования. 1 - md5, 2 - adler32 и так далее. Чем больше цифра, тем длительнее выполнение отдельного алгоритма. Чем больше число, тем длительнее весь алгоритм.

Последним всегда будет MD5, так что на выходе в любом случае 32-значное шестнадцатеричное число. 

1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
<?php



    define
('IRB_SALT',  'ds$e2(dx#s');
    
define('IRB_LOGIC'0145216978);

/**    
* Функция хэширования пароля
* @param string $password - хэшируемый пароль
* @param string $logic - алгоритм хэширования
* @param string $salt - соль
* @return string
*/    
    
function cryptPassword($password$logic 9$salt '')
    {
        
        
$crypt = array( 'md5',
                        
'adler32',
                        
'haval256,5',
                        
'ripemd256',
                        
'ripemd320',
                        
'sha384',
                        
'sha512',                            
                        
'gost',
                        
'whirlpool',
                        
'snefru'                            
                        
);

        
$logic = (string)$logic;
        
$cnt   strlen($logic);
        
        for(
$i 0$i $cnt$i++)
        {
            
$algorithm $crypt[$logic[$i]];
            
$password  hash($algorithm$password $salt);  
        }
        
        return 
md5($password);   
    }
    

    
$password 'test';
    
    echo 
cryptPassword($passwordIRB_LOGICIRB_SALT);    
    
        


Предупреждение! Эта функция будет работать только в PHP, скомпилированном с библиотекой Hash.


Здесь мы применили общую соль для всех паролей. Но считается, что этого недостаточно. Дело в том, что зная эту соль (ну допустим ресурс взломан полностью и она, как и алгоритм шифрования теперь не секрет), можно с помощью радужных таблиц вычислить исходные строки. И хотя это займет огромную кучу машино-времени, теоретически такая опасность существует. Так вот, чтобы уберечься даже от теоретической опасности, стоит использовать индивидуальную соль для каждого пароля, которую без зазрения совести можно хранить рядом с хэшем пароля в той же базе данных. Кроме того, индивидуальная соль убережет хэши от повторов. А лучше использовать и то и другое. Общую соль из конфиги и индивидуальную из базы. Тогда может спокойно спать даже самый отъявленный параноик, теперь для взлома понадобится сгенерировать радужную таблицу для каждого пароля в отдельности, а это даже теоретически трудно себе представить.

Для генерации индивидуальной соли можно использовать любой генератор случайной строки, хотя бы тот, который описан в этой статье. А можно еще хитрее, использовать в качестве соли логин. Тогда даже дополнительного поля в базе не нужно. Дело в том, что индивидуальная соль, это не секретная информация, она служит для защиты от радуг, увеличивая длину пароля, а значит и величину радужной таблицы. Секретная информация - общий ключ, который мы храним в конфиге и из базы данных он недоступен.

 Минус такого подхода - длина соли. Логины бывают и из 3-4 символов, а этого маловато. Но можно использовать логин трижды, четырежды и так далее. Чем длиннее соль, тем сложнее взлом. И второй минус - если используется один и тот же алгоритм на разных сайтах, соль для одинаковых логинов на них будет одинаковой, что тоже не радует. 

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

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

Вот что имеется в итоге:

1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
<?php


    define
('IRB_SALT',  'ds$e2(dx#s');
    
define('IRB_LOGIC'0145216978);

/**    
* Функция хэширования пароля
* @param string $password - хэшируемый пароль
* @param string $logic - алгоритм хэширования
* @param string $salt - соль
* @param int $round - количество раундов хэширования
* @return string
*/   
    
function cryptPassword($password$logic 9$salt ''$round  5000)
    {
        
        
$crypt = array( 'md5',
                        
'adler32',
                        
'haval256,5',
                        
'ripemd256',
                        
'ripemd320',
                        
'sha384',
                        
'sha512',                            
                        
'gost',
                        
'whirlpool',
                        
'snefru'                            
                        
);
     
        
$logic = (string)$logic;
        
$cnt   strlen($logic);
        
        while(
$round--)
        {
            for(
$i 0$i $cnt$i++)
            {
                
$algorithm $crypt[$logic[$i]];
                
$password  hash($algorithm$password $salt);  
            }
        }
        
        return 
md5($password);   
    }
////////////////////////////////////////////////////////////////////////////
    
    
$login    'twin';
    
$email    'test@mail.ru';
    
$password 'test';

    
// При регистрации генерируем соль
    //$salt = randomString();
    // Или используем логин так
    
$salt $login $login $login;
    
// Или допустим e-mail так:
    //$salt = md5($email);
    
echo 'Хэш пароля: 'cryptPassword($password IRB_SALTIRB_LOGIC$salt);
    echo 
'<br>'
    
    
// При аутентификации достаем её из базы
    //$salt = 'dv2yFgrJzD';
    //$hash = 'b29f213993838a27602c2ab4bda5ee5a';
    // Или используем логин 
    //$salt = $login . $login . $login;
    //$hash = '0d78ddf1ac47187acb31201940962312';
    // Или хэш почты (кто во что горазд)
    
$salt md5($email);
    
$hash '2b9733404ca83930ce9898eab143e366';
    
    if(
cryptPassword($password IRB_SALTIRB_LOGIC$salt) == $hash)
        echo 
'Добро пожаловать'



По идее существует специальная функция crypt(), которая хэширует строки параллельно генерируя соль. Собственно она очень похожа на то, что мы сейчас наваяли, только алгоритм использует один. Так что апофеозом всего вышесказанного - обертка для неё. Вот:

1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
<?php

    define
('IRB_SALT',  'ds$e2(dx#s');
    
define('IRB_LOGIC'012331230);

/**    
* Функция хэширования пароля
* @param string $password - хэшируемый пароль
* @param string $logic - алгоритм хэширования
* @param string $hash - соль
* @param int $round - количество раундов хэширования
* @return string
*/   
    
function cryptPassword($password$logic 3$hash ''$round  10)
    {
        if(
CRYPT_MD5 != 1)
        {
            
trigger_error(__FUNCTION__ .'(). '
                           
.'The system does not support md5 algorithm',
                           
E_USER_WARNING);
            exit();
        }
     
        
$string 'abcdefghijklmnopqrstuvwxyz0123456789#$%.!-=(){}[]\/';
     
        if(empty(
$hash))
        {                
            
$string str_pad(''108$string);
            
$string str_shuffle($string);
            
$rand   round(microtime(true) - floor(microtime(true)), 2) * 100;
            
$salt   substr($string$rand8);
        }                
        else
        {
             
preg_match('~(.*)([a-f0-9]{32})$~ui'$hash$out);
             
            if(empty(
$out[1]) && !empty($out[0]))
                
$salt substr($out[0], 08);
            else
                
$salt substr(preg_replace('~[^'preg_quote($string) .']~ui'
                                            
'-'
                                            
$hash), 
                               
08);
        }

        
$hash crypt($password'$1$'$salt);
     
        
$crypt = array( array(CRYPT_MD5,      '$1$'),
                        array(
CRYPT_BLOWFISH'$2y$07$'),
                        array(
CRYPT_SHA256,   '$5$rounds=1000$'),
                        array(
CRYPT_SHA512,   '$6$rounds=1000$'),
                      );                      
     
        
$logic = (string)$logic;
        
$cnt   strlen($logic);
        
$round = ($round 1) ? $round;
        
$round = ($round 1000) ? 1000 $round;
 
        while(
$round--)
        {
            for(
$i 0$i $cnt$i++)
            {
                if(empty(
$crypt[$logic[$i]][0]) || $crypt[$logic[$i]][0] != 1)
                    
$crypt[$logic[$i]][1] = '$1$';
              
                
preg_match("~[^\$]+$~i"$hash$out);
                
$hash substr($out[0], 08);
                
$hash crypt($password$crypt[$logic[$i]][1] . $hash); 
            }
        }
        
        return 
$salt md5($hash);   
    }
////////////////////////////////////////////////////////////////////////////   
    
$password 'test';
// Статическая соль теперь используется в первом аргументе. 
// Динамическая по умолчанию будет сгенерирована автоматически.
    
echo 'Хэш пароля: 'cryptPassword($password IRB_SALTIRB_LOGIC);
    echo 
'<br>'
// Для проверки не нужно вычленять соль, можно передать в функцию хэш полностью.
    
$hash 'u)mlx#qtaba14a93c286193f0bd600d13db2a816';

    if(
cryptPassword($password IRB_SALTIRB_LOGIC$hash) == $hash)
        echo 
'Добро пожаловать';


Что тут принципиально изменилось.
1. Эта функция не зависит от библиотеки Hash.
2. Добавили генератор соли. Теперь можно использовать как внешний, так и встроенный (по умолчанию).
3. В соли можно использовать любые символы из этого перечня
abcdefghijklmnopqrstuvwxyz0123456789#$%.!-=(){}[]\/.
Другие символы будут заменены на дефис.
4. На выходе максимум 40 символов, первые из которых - соль, последние 32 - хэш. Это существенно экономит размер таблицы БД.
5. Не нужно отдельное поле для хранения соли.

 

Минусы этой функции
1. Малый ассортимент алгоритмов. Но по хорошему и этого достаточно.
2. Желательно использовать версию PHP не ниже 5.3.7.

Работает по принципу функции crypt(). При генерации хэша первым параметром - пароль, вторым - алгоритм хэширования.
Так как алгоритмов мало, можно использовать цифры от 0 до 3. Если будут найдены другие цифры, вместо них будет применяться алгоритм md5. Третьим необязательным параметром - соль. Используются только первые 8 символов, остальные игнорируются. И четвертым опционально - количество раундов.

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

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

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

Тут на вкус и цвет все фломастеры разные. Можно понадеяться на голую crypt(), она вполне самодостаточна и надежна, а можно дополнить и улучшить ради спокойного сна.

 

Чего вам от всей души желаю.

 

 

Ну и тем, кто дочитал до конца, бонусом пару ссылок на библиотеки, помогающие в вопросах хэширования паролей.
Раз
Два
Три

Николай aka twin

 

Теги: PHP

Комментарии (2)

Lumix
26-07-2013
А если использовать email в функции  $salt = md5($email);
то тогда не будет возможности пользователям менять свой email на сайте, если
такая возможность предусмотрена конечно !
twin
26-07-2013
Естественно. Это уже индивидуальный подход. Многие ресурсы вообще мыло используют в качестве логина, Google например. Никак не сменишь.

 
Наверх