password_hash и password_verify
<?php
declare(strict_types=1);
// Hashing a password (during registration)
$password = 'MySecureP@ssw0rd';
$hash = password_hash($password, PASSWORD_DEFAULT);
// Example: $2y$12$eUz2eSm...
// Verifying a password (during login)
$inputPassword = 'MySecureP@ssw0rd';
if (password_verify($inputPassword, $hash)) {
echo 'Password correct!';
} else {
echo 'Wrong password!';
}
// NEVER compare hashes directly!
// if ($hash === $inputHash) // WRONG! Timing attack vulnerability
// Always use password_verify()
Запомни: НИКОГДА не сравнивайте хеши напрямую (
==или===). Используйтеpassword_verify(). Она устойчива к timing-атакам и корректно обрабатывает формат хеша.
Алгоритмы хеширования
<?php
declare(strict_types=1);
// PASSWORD_DEFAULT — currently bcrypt, may change in future
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_BCRYPT — bcrypt algorithm
$hash = password_hash($password, PASSWORD_BCRYPT, [
'cost' => 12, // Default: 10, higher = slower but more secure
]);
// PASSWORD_ARGON2I — Argon2i (PHP 7.2+)
$hash = password_hash($password, PASSWORD_ARGON2I, [
'memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST, // 65536 KB
'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST, // 4
'threads' => PASSWORD_ARGON2_DEFAULT_THREADS, // 1
]);
// PASSWORD_ARGON2ID — Argon2id (PHP 7.3+, recommended)
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 1,
]);
| Алгоритм | Константа | Рекомендация |
|---|---|---|
| bcrypt | PASSWORD_BCRYPT |
Хороший, проверенный |
| Argon2i | PASSWORD_ARGON2I |
Устойчив к side-channel |
| Argon2id | PASSWORD_ARGON2ID |
Лучший выбор (PHP 7.3+) |
| Default | PASSWORD_DEFAULT |
Автовыбор (сейчас bcrypt) |
Ловушка экзамена:
PASSWORD_DEFAULTна данный момент использует bcrypt, но может измениться в будущих версиях PHP.password_verify()корректно определяет алгоритм из хеша, поэтому старые хеши будут работать.
password_needs_rehash
<?php
declare(strict_types=1);
// Check if hash needs to be updated
function loginUser(string $email, string $password, PDO $pdo): bool
{
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
return false;
}
// Rehash if algorithm or cost changed
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT, ['cost' => 12])) {
$newHash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
$stmt = $pdo->prepare('UPDATE users SET password_hash = :hash WHERE id = :id');
$stmt->execute(['hash' => $newHash, 'id' => $user['id']]);
}
return true;
}
Запомни:
password_needs_rehash()проверяет, соответствует ли хеш текущим настройкам алгоритма и стоимости. При обновлении cost или алгоритма, старые хеши автоматически обновятся при следующем логине пользователя.
password_get_info
<?php
declare(strict_types=1);
$hash = password_hash('test', PASSWORD_BCRYPT, ['cost' => 12]);
$info = password_get_info($hash);
var_dump($info);
// [
// 'algo' => '2y',
// 'algoName' => 'bcrypt',
// 'options' => ['cost' => 12],
// ]
Что НЕЛЬЗЯ делать
<?php
declare(strict_types=1);
// NEVER use these for passwords:
// md5($password) — too fast, rainbow tables
// sha1($password) — too fast, rainbow tables
// sha256($password) — too fast without salt
// md5($salt . $password) — custom salting is weak
// hash('sha256', $password) — not designed for passwords
// ALWAYS use password_hash/password_verify
// They:
// 1. Auto-generate unique salt per hash
// 2. Use intentionally slow algorithm (bcrypt/argon2)
// 3. Are resistant to rainbow table attacks
// 4. Are resistant to brute-force (tunable cost)
Выбор стоимости (cost)
<?php
declare(strict_types=1);
// Benchmark to find appropriate cost for your server
function findOptimalBcryptCost(float $targetTime = 0.1): int
{
$cost = 8;
do {
$cost++;
$start = microtime(true);
password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]);
$elapsed = microtime(true) - $start;
} while ($elapsed < $targetTime && $cost < 31);
return $cost;
}
// Target: ~100ms per hash (good balance of security vs UX)
echo "Recommended cost: " . findOptimalBcryptCost(0.1);