Хранение паролей в PHP с использованием crypt()

Yii
/
2013-09-16 15:10
Просмотры: 76607

Хранение паролей в web-приложении

Существует много примеров и материалов, в которых показано как хранить пароли в базе данных.

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

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

Очень распространенная ошибка это быстрые хэши. Например, MD5 - очень быстрый алгоритм хэширования, впрочем как и все SHA алгоритмы. Например, на NVIDIA GeForce 8800 можна генерировать 200 миллионов MD5 хэшей в секунду. Из этого следует, что использование быстрых алгоритмов хеширования, даже с примесью "соли" не эффективно. Обычным перебором можно сравнительно быстро подобрать хеш, а соответственно и пароль пользователя. По этому систему с хранением паролей в виде md5("password") - можно считать открытой для злоумышленника.

Например, на сервере для вычисления одного хэша используя алгоритм md5, в среднем, требуется 10мксек. Пароль хранится в базе данных, как хеш от введенного значения пользователя(md5("password")). Злоумышленник каким то образом получил базу логинов с хешами паролей и пытается расшифровать их. Алгоритм расшифровки самый простой - перебор. Понятно что для вычисления тысячи хешей требуется 10мксек*1000 = 10мсек, для миллиона 10мксек*1000000 = 1сек и т.д. Из этого следует, что чем быстрее алгоритм хеширования, тем быстрее злоумышленник подберет больше количество паролей за меньший промежуток времени. Из вышесказанного сановится ясным, что чем медленее алгоритм вычисления хеш функции, тем надежнее.

Blowfish - криптографический алгоритм, реализующий блочное симметричное шифрование. Потребляет сравнительно большое количество вычислительных ресурсов и в настоящее время считается довольно не плохим алгоримом для шифрования паролей. В PHP реализция доступна простым вызовом crypt(). Подобор весового параметра производится таким образом, что бы атака перебором была достаточно медленная, а вычисление хеша для пользователя незаметным по времени. 200-300 мсек на продакшн сервере вполне достаточно для этого.

Для более стойкого хеша добавляют соль. Соль - слово, фраза, рандомное значение температуры, динамическое изминение чего либо, которое добавляется произвольно для формирования конечного хеша. Например, md5($password . md5($salt)). Из каждого пароля должен формироваться хеш с уникальной солью. Цель добавлнения соли - увеличения размера словаря для атаки типа перебор по словарю или для радужных таблиц. Соль, которая используется вместе с Blowfish, не объязательно должна быть криптографическо стойкой случайной строкой. Достаточно использовать соль с псевдослучайных чисел, этого достаточно для предотвращения подбора пароля по радужным таблицам.

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

Использование crypt() в PHP для хранения паролей

string crypt ( string $str [, string $salt ] )

crypt() возвращает хэшированную строку, полученную с помощью стандартного алгоритма UNIX, основанного на DES, или другого алгоритма, имеющегося в системе.

Соль определяется в зависимости от метода хеширования. Для Blowfish-шифрования, формат такой: "$2a$", весовой параметр из двух цифр, "$" и 22 цифры из алфавита "./0-9A-Za-z". Использование других символов в соли повлечет за собой возвращение пустой строки. Весовой параметр из двух цифр является двоичным логарифмом счетчика итераций низлежащего хэширующего алгоритма, основанного на Blowfish, и должен быть в диапазоне 04-31, значения вне данного диапазона вызовут отказ crypt(). Например:

crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t')
    >> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'

Обратите внимание, что первые 29 символов возвращенного значения такие же, как соль.

При добавлении чего либо в соль результат не изменит:

crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t12345678901234567890')
    >> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
 
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t$2a$10$1qAz2wSx3eDc4rFv5tGb5t')
    >> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'

И соответсвенно, если результат выполнения функции crypt подать на вход как соль, для одной и той же строки, то результатом выполнения будет та же соль:

crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi')
    >> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'

Принципы сохранения пароля совместно с crypt:

- в хранилище записываем только хеш пароля и соль, пароль явно не указываем;

- значение, возвращаемое выполнением функции crypt - это соль с хешом;

- crypt игнориоует лишние символы в строке соли;

Создание хеша

$hash = crypt( $password, $salt )

Валидация пароля

Это сравнение сохраненного $hash с вычесленным значением crypt( $password, $salt )

Создание соли для Blowfish алгоритма

/**
 * Generate a random salt in the crypt(3) standard Blowfish format.
 *
 * @param int $cost Cost parameter from 4 to 31.
 *
 * @throws Exception on invalid cost parameter.
 * @return string A Blowfish hash salt for use in PHP's crypt()
 */
function blowfishSalt($cost = 13)
{
    if (!is_numeric($cost) || $cost < 4 || $cost > 31) {
        throw new Exception("cost parameter must be between 4 and 31");
    }
    $rand = array();
    for ($i = 0; $i < 8; $i += 1) {
        $rand[] = pack('S', mt_rand(0, 0xffff));
    }
    $rand[] = substr(microtime(), 2, 6);
    $rand = sha1(implode('', $rand), true);
    $salt = '$2a$' . sprintf('%02d', $cost) . '$';
    $salt .= strtr(substr(base64_encode($rand), 0, 22), array('+' => '.'));
    return $salt;
}

Пример

Пользователей сохраняем в базе данный. Создание таблици:

create table user (
    id int not null auto_increment primary key,
    username varchar(255) not null,
    password_hash char(64) not null,
    unique key (username)
)

Пусть данные приходят из формы, тогда хеш генерируется так:

$password_hash = crypt( $_POST['password'], blowfishSalt() );

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

Для проверки правильности ввода пароля пользователем, для начала делаем выборку по имени пользователя, если такое имя есть, то нам известен сохраненный хеш($record_password_hash) и пароль введенный пользователем из формы($_POST['password']).

Делаем проверку:

if ($record_password_hash === crypt($_POST['password'], $record_password_hash))
    // верный пароль
else
    // неверный пароль

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

Использование в Yii приложении

Пусть работа с пользователями будет вестись через модель Uesr, тогда добавление нового пользователя будет выглядеть так:

$user = new User;
$user->username = $form->username;
$user->password = crypt($form->password, self::blowfishSalt());
if ($user->save()) {
    ...
}

Где в $form - хранятся данные из формы, blowfishSalt - пернесенная функция в класс User для генерирования соли.

Для проверки пароля при аутентификации, в UserIdentity::authenticate()

public function authenticate()
{
    $record = User::model()->findByAttributes(array('username' => $this->username));
    if ($record === null) {
        $this->errorCode = self::ERROR_USERNAME_INVALID;
    } else if ($record->password !== crypt($this->password, $record->password)) {
        $this->errorCode = self::ERROR_PASSWORD_INVALID;
    } else {
        $this->_id = $record->id;
        $this->setState('title', $record->title);
        $this->errorCode = self::ERROR_NONE;
    }
    return !$this->errorCode;
}

Источник: Use crypt() for password storage

Добавить комментарий

Все комментарии

unknown
24 апреля 2014 18:55
Что значит на NVIDIA GeForce 8800 можна генерировать 200 миллионов MD5 хэшей в секунду?)
Ответить
Antoxa
23 мая 2014 00:40
Что на видеокарте NVIDIA GeForce 8800 средняя скорость генерации 200 миллионов MD5 хэшей в секунду
unknown
24 апреля 2014 19:42
throw new Exception("cost parameter must be between 4 and 31"); заменить на triger_error
Ответить
unknown
24 апреля 2014 19:44
for ($i = 0; $i < 8; $i += 1) { заменить на for ($i = 0; $i < 8; ++$i) {
Ответить
unknown
24 апреля 2014 19:47
function blowfishSalt написана криворуким извращенцем
Ответить
comments powered by Disqus