Эффективное хэширование паролей с помощью password_hash

Раздел: -> Безопасность

Основы работы с паролями в PHP

Как правильно хэшировать пароль при регистрации?

Самый безопасный и рекомендуемый способ - использование функций password_hash и password_verify. Они автоматически генерируют случайную соль и используют современный алгоритм bcrypt (или Argon2, если доступен).

$password = 'user_password123';
$hash = password_hash($password, PASSWORD_DEFAULT);
echo $hash; // например, $2y$10$...

Пояснение: PASSWORD_DEFAULT использует bcrypt по умолчанию, который считается безопасным. Функция возвращает строку, содержащую соль, алгоритм и стоимость. Эта строка может храниться в базе данных.

Типичные проблемы и ошибки:

  • Применение устаревших алгоритмов (MD5, SHA1) - уязвимы для подбора.
  • Неправильная настройка стоимости (cost) - слишком низкая делает хэш быстрым для атак, слишком высокая замедляет работу сервера.
  • Хранение пароля в открытом виде в логах или при выводе.
  • Использование фиксированной соли или соли маленькой длины.

Решение:

  • Рекомендуется всегда использовать PASSWORD_DEFAULT или PASSWORD_BCRYPT.
  • Устанавливать стоимость (cost) в диапазоне 10-12 для bcrypt, тестируя производительность сервера.
  • Проверять наличие PASSWORD_ARGON2ID и применять его на PHP 7.2+ для ещё большей безопасности.

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

Для проверки используется password_verify:

$inputPassword = 'user_password123';
$storedHash = '$2y$10$...'; // из базы данных
if (password_verify($inputPassword, $storedHash)) {
    echo 'Пароль верен';
} else {
    echo 'Неверный пароль';
}

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

Ошибки: передача аргументов в неправильном порядке (хэш должен быть вторым).

Как обновить хэш до более нового алгоритма?

Применяется password_needs_rehash:

if (password_needs_rehash($storedHash, PASSWORD_DEFAULT, ['cost' => 12])) {
    $newHash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
    // сохранить $newHash в базу данных
}

Это позволяет автоматически обновлять хэши при изменении алгоритма или стоимости без участия пользователя.

Как не следует хэшировать пароли? (использование MD5)

Старый способ: md5($password) - крайне небезопасен из-за быстрого вычисления и отсутствия соли.

$hash = md5('password123');

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

Как раньше хэшировали пароли с солью? (SHA1 + соль)

Пример: sha1($salt.$password) - всё ещё слабо, так как SHA1 быстр.

$salt = 'random_fixed_salt';
$hash = sha1($salt . $password);

Проблемы: соль фиксирована, алгоритм ASIC-дружелюбен, подбор возможен.

Как использовать crypt() для хэширования?

Функция crypt() с правильной солью (например, '$2y$10$...') может давать bcrypt-хэш, но требует ручного формирования соли.

$salt = '$2y$10$' . bin2hex(random_bytes(22));
$hash = crypt($password, $salt);

Ошибки: неправильный формат соли, сложность с управлением параметрами. Лучше применять password_hash.

Как вручную создать безопасный хэш с случайной солью? (hash + random_bytes)

Можно использовать hash('sha256', random_bytes(16) . $password), но это не рекомендуется, так как не имеет настраиваемой стоимости.

$salt = random_bytes(16);
$hash = hash('sha256', $salt . $password);

Проблемы: фиксированное время вычисления, слабый по сравнению с bcrypt/argon2.

Как использовать Argon2 для хэширования паролей?

Если PHP 7.2+, применяется PASSWORD_ARGON2ID:

$hash = password_hash($password, PASSWORD_ARGON2ID, ['memory_cost' => 1024, 'time_cost' => 2, 'threads' => 2]);

Плюсы: устойчив к GPU-атакам. Минусы: требует ресурсов, может быть недоступен на старых версиях PHP.

Как оценить надёжность создаваемого пароля?

Регулярные выражения или библиотеки типа zxcvbn помогают оценить сложность:

$password = 'MyPassword123!';
if (preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/', $password)) {
    echo 'Надёжный пароль';
}

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

Как сгенерировать надёжный пароль для пользователя?

Генерация выполняется с помощью random_bytes и кодировки в base64 или hex:

$bytes = random_bytes(16);
$password = bin2hex($bytes); // или base64_encode

Слишком сложные пароли трудно запомнить, нужно предлагать пользователю изменить.

Пример
// Регистрация
$password = 'User@123';
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
echo "Хэш для хранения: $hash\n";

// Аутентификация
$input = 'User@123';
if (password_verify($input, $hash)) {
    echo "Пароль верен\n";
    // проверка необходимости обновления
    if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) {
        $newHash = password_hash($input, PASSWORD_BCRYPT, ['cost' => 12]);
        echo "Хэш обновлён: $newHash\n";
    }
} else {
    echo "Неверный пароль\n";
}
Хэш для хранения: $2y$10$...
Пароль верен
Хэш обновлён: $2y$12$...

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

Пример
$password = 'test123';
$md5Hash = md5($password);
$bcryptHash = password_hash($password, PASSWORD_BCRYPT);
echo "MD5 хэш: $md5Hash\n";
echo "Bcrypt хэш: $bcryptHash\n";
MD5 хэш: cc03e747a6afbbcbf8be7668acfebee5
Bcrypt хэш: $2y$10$... (длинная строка)

Пояснение: MD5 вычисляется за микросекунды, bcrypt требует около 0.1 секунды за счёт стоимости. Это делает атаку перебором в миллионы раз дороже.

Пример
if (defined('PASSWORD_ARGON2ID')) {
    $options = ['memory_cost' => 1<<17, 'time_cost' => 4, 'threads' => 4];
    $hash = password_hash($password, PASSWORD_ARGON2ID, $options);
    echo "Argon2 хэш: $hash\n";
} else {
    echo "Argon2 недоступен\n";
}
Argon2 хэш: $argon2id$v=19$m=131072,t=4,p=4$... (длинная строка)

Пояснение: Argon2id использует память и время, что затрудняет атаки на GPU. Настройки memory_cost, time_cost, threads нужно адаптировать под сервер.

Пример
// Генерация пароля
$bytes = random_bytes(12);
$generatedPassword = bin2hex($bytes); // 24 символа
echo "Сгенерированный пароль: $generatedPassword\n";

// Проверка сложности
$pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{12,}$/';
if (preg_match($pattern, $generatedPassword)) {
    echo "Пароль соответствует требованиям\n";
} else {
    echo "Пароль не проходит проверку (маловероятно)\n";
}
Сгенерированный пароль: a1b2c3d4e5f6g7h8i9j0k1l2
Пароль соответствует требованиям

Пояснение: random_bytes даёт случайные байты, bin2hex преобразует в hex-строку, которая содержит только 0-9a-f. Для увеличения разнообразия лучше использовать base64 или другие энкодеры, но для примера достаточно. Проверка сложности может не пройти из-за отсутствия спецсимволов.

Пример
$salt = '$2y$10$' . substr(str_replace('+', '.', base64_encode(random_bytes(22))), 0, 22);
$hash = crypt($password, $salt);
echo "Хэш крипта: $hash\n";
Хэш крипта: $2y$10$... (длинная строка)

Пояснение: crypt требует строго 22 символа для соли после префикса. Используется base64 с заменой '+' на '.' для соответствия алфавиту. Этот способ устарел, проще использовать password_hash.

Работа с паролями в PHP - comments

En
Php пароль (php)