Password hashing — password_hash, bcrypt, Argon2, cost factor
Concept
Password storage is a critical security concern. Passwords must never be stored in plaintext, and should never use fast hashing algorithms (MD5, SHA1, SHA256) — these can be brute-forced with GPU clusters at billions of hashes per second.
password_hash(string $password, int $algo, array $options = []): PHP's built-in secure password hashing. Uses password-specific, slow algorithms with automatic salting. Returns an opaque string that includes the algorithm, cost factors, salt, and hash.
Algorithms:
PASSWORD_BCRYPT: BCrypt. Cost factor 4-31 (default 10 — tune so hashing takes ~100ms on your hardware). Battle-tested since 1999. Max password length: 72 bytes (silently truncates!).PASSWORD_ARGON2I(PHP 7.2+): Argon2i — resistant to GPU cracking and side-channel attacks. Memory-hard.PASSWORD_ARGON2ID(PHP 7.3+): Argon2id — combines Argon2i and Argon2d. Recommended by OWASP over Argon2i. Use this for new projects.PASSWORD_DEFAULT: CurrentlyPASSWORD_BCRYPT. Will be updated in future PHP versions. Safe to use since hashes include algorithm info.
password_verify(string $password, string $hash): bool: Compares a password against a hash. Timing-safe — uses constant-time comparison.
password_needs_rehash(string $hash, int $algo, array $options = []): bool: Returns true if the hash was made with a different algorithm or cost factor. Call after successful login and rehash if true.
Hash length: BCrypt produces 60-character hashes. Argon2id produces longer hashes (96+ chars). Store hash in VARCHAR(255) or TEXT.
Code Example
<?php
declare(strict_types=1);
// WRONG — never store passwords like this
$hash_md5 = md5($password); // cracks in seconds on GPU
$hash_sha1 = sha1($password); // also fast — also crackable
$hash_bad = hash('sha256', $password); // still fast — wrong tool
// CORRECT — bcrypt (default, solid choice)
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// Store $hash in database — looks like: $2y$12$SALT...HASHDATA
// CORRECT — Argon2id (recommended for new systems)
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64MB
'time_cost' => 4,
'threads' => 2,
]);
// Verify on login
function verifyLogin(string $inputPassword, string $storedHash): bool
{
return password_verify($inputPassword, $storedHash);
// timing-safe — same time whether hash matches or not
}
// Full login flow with rehashing
function login(string $email, string $inputPassword): ?User
{
$user = User::where('email', $email)->first();
if (!$user) {
// Compute a hash anyway to prevent timing attacks revealing valid emails
password_hash($inputPassword, PASSWORD_ARGON2ID); // throw away result
return null;
}
if (!password_verify($inputPassword, $user->password)) {
return null;
}
// Upgrade hash if algorithm/cost factor changed
if (password_needs_rehash($user->password, PASSWORD_ARGON2ID, ['memory_cost' => 65536])) {
$user->update(['password' => password_hash($inputPassword, PASSWORD_ARGON2ID, ['memory_cost' => 65536])]);
}
return $user;
}
// Tune bcrypt cost (target: ~100ms on production hardware)
$start = microtime(true);
password_hash('test', PASSWORD_BCRYPT, ['cost' => 12]);
$elapsed = microtime(true) - $start;
echo "Cost 12: {$elapsed}s\n"; // should be ~0.1s
// Laravel uses Bcrypt by default (config/hashing.php)
// Change to Argon2id in config/hashing.php:
// 'driver' => 'argon2id',
// Hash::make($password)