EasyТеория3 min

Хеширование паролей

password_hash, password_verify, bcrypt, argon2

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);

Проверь себя

Для чего нужен `password_needs_rehash`?

Что использует `PASSWORD_DEFAULT` в текущей версии PHP?

Почему нельзя использовать `md5()` для паролей?

Генерирует ли `password_hash` соль автоматически?

Какая функция проверяет пароль по хешу?