Хеширование -- это преобразование данных произвольной длины в строку фиксированной длины. Хеш-функция работает только в одну сторону: из хеша невозможно восстановить исходные данные. PHP предоставляет мощный набор функций для хеширования общего назначения и отдельный API для безопасной работы с паролями.
hash() -- универсальная функция хеширования
<?php
declare(strict_types=1);
// Basic usage: hash(algorithm, data, binary = false)
$data = 'Hello, World!';
// MD5 — 128-bit hash (32 hex characters)
$md5 = hash('md5', $data);
echo $md5; // '65a8e27d8879283831b664bd8b7f0ad4'
echo strlen($md5); // 32
// SHA-1 — 160-bit hash (40 hex characters)
$sha1 = hash('sha1', $data);
echo strlen($sha1); // 40
// SHA-256 — 256-bit hash (64 hex characters)
$sha256 = hash('sha256', $data);
echo strlen($sha256); // 64
// SHA-512 — 512-bit hash (128 hex characters)
$sha512 = hash('sha512', $data);
echo strlen($sha512); // 128
// Binary output (raw bytes instead of hex)
$raw = hash('sha256', $data, binary: true);
echo strlen($raw); // 32 bytes (256 bits)
// xxHash — extremely fast non-cryptographic hash (PHP 8.1+)
$xxh3 = hash('xxh3', $data);
$xxh128 = hash('xxh128', $data);
// MurmurHash3 — fast non-cryptographic hash (PHP 8.1+)
$murmur3a = hash('murmur3a', $data); // 32-bit
$murmur3c = hash('murmur3c', $data); // 128-bit x86
$murmur3f = hash('murmur3f', $data); // 128-bit x64
Важно: MD5 и SHA-1 считаются криптографически сломанными. Используйте SHA-256+ для криптографических целей. MD5/SHA-1 допустимы только для чексумм файлов и некриптографических задач.
hash_algos() -- список доступных алгоритмов
<?php
declare(strict_types=1);
// List all available hash algorithms
$algorithms = hash_algos();
echo count($algorithms); // ~60+ algorithms
// Check if specific algorithm is available
if (in_array('sha3-256', hash_algos(), true)) {
$hash = hash('sha3-256', 'data');
}
// Print all algorithms
foreach (hash_algos() as $algo) {
echo sprintf("%-20s -> %d hex chars\n", $algo, strlen(hash($algo, 'test')));
}
// Common algorithms available:
// md5, sha1, sha224, sha256, sha384, sha512
// sha3-224, sha3-256, sha3-384, sha3-512
// ripemd128, ripemd160, ripemd256, ripemd320
// whirlpool, tiger128,3, tiger160,3, tiger192,3
// crc32, crc32b, crc32c
// xxh32, xxh64, xxh3, xxh128 (PHP 8.1+)
// murmur3a, murmur3c, murmur3f (PHP 8.1+)
hash_file() -- хеширование файлов
<?php
declare(strict_types=1);
// Hash a file without loading it entirely into memory
$fileHash = hash_file('sha256', '/path/to/large-file.zip');
// Verify file integrity after download
$expectedHash = 'abc123...'; // from source
$actualHash = hash_file('sha256', '/tmp/downloaded-file.zip');
if (hash_equals($expectedHash, $actualHash)) {
echo "File integrity verified!\n";
} else {
echo "File corrupted or tampered with!\n";
unlink('/tmp/downloaded-file.zip');
}
// MD5 checksum (commonly used for downloads)
$md5sum = hash_file('md5', '/path/to/file.tar.gz');
// Compare two files by hash
function filesIdentical(string $file1, string $file2): bool
{
return hash_file('sha256', $file1) === hash_file('sha256', $file2);
}
Инкрементальное хеширование
<?php
declare(strict_types=1);
// For large data or streaming — use hash_init/hash_update/hash_final
$context = hash_init('sha256');
// Feed data incrementally
hash_update($context, 'chunk 1');
hash_update($context, 'chunk 2');
hash_update($context, 'chunk 3');
$hash = hash_final($context);
// Same result as hash('sha256', 'chunk 1chunk 2chunk 3')
// Practical: hash a large file in chunks
function hashLargeFile(string $path, string $algo = 'sha256'): string
{
$context = hash_init($algo);
$handle = fopen($path, 'rb');
while (!feof($handle)) {
$chunk = fread($handle, 8192);
hash_update($context, $chunk);
}
fclose($handle);
return hash_final($context);
}
// Hash from stream
$context = hash_init('sha256');
$stream = fopen('https://example.com/file.bin', 'rb');
hash_update_stream($context, $stream);
$hash = hash_final($context);
fclose($stream);
// Copy context for intermediate hashes
$ctx = hash_init('sha256');
hash_update($ctx, 'part1');
$ctxCopy = hash_copy($ctx);
$intermediateHash = hash_final($ctxCopy); // hash of 'part1'
hash_update($ctx, 'part2');
$finalHash = hash_final($ctx); // hash of 'part1part2'
hash_hmac() -- HMAC для аутентификации сообщений
HMAC (Hash-based Message Authentication Code) -- это механизм для проверки целостности и аутентичности сообщения с использованием секретного ключа.
<?php
declare(strict_types=1);
// HMAC: hash_hmac(algorithm, data, key, binary = false)
$secret = 'my-secret-key-at-least-32-bytes-long!!';
$message = 'Important data to protect';
$hmac = hash_hmac('sha256', $message, $secret);
echo $hmac; // 64 hex characters
// Verify HMAC — ALWAYS use hash_equals() for comparison!
$receivedHmac = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$computedHmac = hash_hmac('sha256', $message, $secret);
if (hash_equals($computedHmac, $receivedHmac)) {
echo "Message is authentic\n";
} else {
echo "Message tampered or wrong key!\n";
}
// Practical: Webhook signature verification (e.g., Stripe, GitHub)
function verifyWebhookSignature(
string $payload,
string $signature,
string $secret,
): bool {
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}
// Usage for API request signing
function signApiRequest(string $method, string $url, string $body, string $apiSecret): string
{
$data = $method . "\n" . $url . "\n" . $body;
return hash_hmac('sha256', $data, $apiSecret);
}
// HMAC for file list
$files = hash_hmac_algos(); // List of algorithms supporting HMAC
hash_equals() -- timing-safe сравнение
<?php
declare(strict_types=1);
// NEVER compare hashes with == or ===
// Timing attack: attacker measures response time to guess hash byte-by-byte
// ❌ VULNERABLE to timing attacks
if ($userHash === $expectedHash) { /* ... */ }
// ✅ SAFE — constant-time comparison
if (hash_equals($expectedHash, $userHash)) { /* ... */ }
// hash_equals() takes the same time regardless of where strings differ
// Both arguments must be strings of equal length for meaningful comparison
// Practical: CSRF token verification
function verifyCsrfToken(string $expected, string $provided): bool
{
return hash_equals($expected, $provided);
}
// Practical: API key verification
function verifyApiKey(string $storedKey, string $providedKey): bool
{
return hash_equals($storedKey, $providedKey);
}
Почему это важно: При обычном сравнении
===PHP прекращает сравнение при первом несовпадении символа. Атакующий может измерять время ответа и посимвольно подбирать верный хеш.hash_equals()всегда сравнивает строки целиком за одинаковое время.
hash_pbkdf2() -- деривация ключей
<?php
declare(strict_types=1);
// PBKDF2 (Password-Based Key Derivation Function 2)
// Derives a cryptographic key from a password
$password = 'user-password';
$salt = random_bytes(16); // Always use random salt!
$iterations = 600_000; // OWASP 2023 recommendation for SHA-256
$keyLength = 32; // 256-bit key
$derivedKey = hash_pbkdf2('sha256', $password, $salt, $iterations, $keyLength, binary: true);
// For hex output
$derivedKeyHex = hash_pbkdf2('sha256', $password, $salt, $iterations, 64, binary: false);
// Store: salt + iterations + derived key
// Verify: re-derive with same salt + iterations and compare
// Note: For PASSWORD HASHING, prefer password_hash() with Argon2id
// PBKDF2 is mainly for deriving encryption keys from passwords
password_hash() -- хеширование паролей
Это основной API для работы с паролями в PHP. Использует адаптивные алгоритмы, автоматически генерирует salt.
<?php
declare(strict_types=1);
// PASSWORD_DEFAULT — currently bcrypt, may change in future PHP versions
$hash = password_hash('my-password', PASSWORD_DEFAULT);
echo $hash;
// Example: $2y$12$eKx3h5UQ7J.g8ZkYblK5m.qV6N5o4...
// PASSWORD_BCRYPT — always bcrypt (max 72 bytes!)
$hash = password_hash('my-password', PASSWORD_BCRYPT, [
'cost' => 12, // Default is 12, range 4-31
]);
// PASSWORD_ARGON2I — Argon2i (resistant to side-channel attacks)
$hash = password_hash('my-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 (recommended! resistant to both GPU and side-channel)
$hash = password_hash('my-password', PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64 MB
'time_cost' => 4, // 4 iterations
'threads' => 1, // parallelism
]);
// ⚠️ Bcrypt truncates passwords at 72 bytes!
$long = str_repeat('a', 100);
$hash1 = password_hash(substr($long, 0, 72), PASSWORD_BCRYPT);
$hash2 = password_hash($long, PASSWORD_BCRYPT);
// Both produce the same hash! Characters after 72 are ignored.
// Solution: pre-hash with SHA-256 before bcrypt
$preHashed = base64_encode(hash('sha256', $long, binary: true));
$hash = password_hash($preHashed, PASSWORD_BCRYPT);
password_verify() -- проверка пароля
<?php
declare(strict_types=1);
// Verify a password against a hash
$hash = password_hash('secret123', PASSWORD_ARGON2ID);
if (password_verify('secret123', $hash)) {
echo "Password correct!\n";
}
if (!password_verify('wrong', $hash)) {
echo "Wrong password!\n";
}
// Full login flow
function authenticateUser(string $email, string $password, PDO $db): ?array
{
$stmt = $db->prepare('SELECT id, email, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user === false) {
// Prevent timing attacks — hash even when user not found
password_hash($password, PASSWORD_ARGON2ID);
return null;
}
if (!password_verify($password, $user['password_hash'])) {
return null;
}
// Check if rehashing needed (algorithm/cost changed)
if (password_needs_rehash($user['password_hash'], PASSWORD_ARGON2ID)) {
$newHash = password_hash($password, PASSWORD_ARGON2ID);
$stmt = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
$stmt->execute([$newHash, $user['id']]);
}
return $user;
}
password_needs_rehash() -- когда перехешировать
<?php
declare(strict_types=1);
// Returns true if hash was created with different algorithm or options
$hash = '$2y$10$...'; // Old bcrypt with cost=10
// Upgrade to Argon2id
if (password_needs_rehash($hash, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
])) {
// Rehash needed — user's password must be available (only during login!)
$newHash = password_hash($plainPassword, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
]);
// Save $newHash to database
}
// Check bcrypt cost upgrade
$hash = '$2y$10$...'; // cost=10
if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 13])) {
// cost was 10, now we want 13 — rehash needed
}
password_algos() -- доступные алгоритмы
<?php
declare(strict_types=1);
// List available password hashing algorithms (PHP 7.4+)
$algos = password_algos();
print_r($algos);
// ['2y', 'argon2i', 'argon2id']
// Check Argon2 availability
if (in_array('argon2id', password_algos(), true)) {
$hash = password_hash('password', PASSWORD_ARGON2ID);
}
// Get info about existing hash
$hash = password_hash('test', PASSWORD_ARGON2ID);
$info = password_get_info($hash);
print_r($info);
// ['algo' => 'argon2id', 'algoName' => 'argon2id', 'options' => ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1]]
Сравнение алгоритмов: когда что использовать
| Задача | Алгоритм | Почему |
|---|---|---|
| Хеширование паролей | Argon2id | Устойчив к GPU и side-channel атакам |
| Хеширование паролей (fallback) | bcrypt | Доступен везде, проверен временем |
| Подписи, HMAC, JWT | SHA-256/SHA-512 | Быстрый, криптографически стойкий |
| Чексуммы файлов | SHA-256 | Стандарт индустрии |
| Быстрый хеш для хеш-таблиц | xxh3, MurmurHash | Скорость, не безопасность |
| Совместимость (legacy) | MD5, SHA-1 | Только для сверки с внешними системами |
Золотое правило: Для паролей -- ВСЕГДА
password_hash()с Argon2id. Для проверки целостности -- SHA-256. Для скорости -- xxh3. НИКОГДА не используйте MD5/SHA-1 для паролей или безопасности.
Практические примеры
Генерация API ключей
<?php
declare(strict_types=1);
function generateApiKey(): string
{
// Generate 32 random bytes, encode as hex = 64 characters
return bin2hex(random_bytes(32));
}
function hashApiKey(string $apiKey): string
{
// Store only hash in database — if DB leaked, keys are safe
return hash('sha256', $apiKey);
}
function verifyApiKey(string $providedKey, string $storedHash): bool
{
$computedHash = hash('sha256', $providedKey);
return hash_equals($storedHash, $computedHash);
}
// Usage
$apiKey = generateApiKey();
echo "Give to user: $apiKey\n"; // Show once, never store plaintext
$hashedKey = hashApiKey($apiKey);
// Store $hashedKey in database
// Later, verify
$providedKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
if (verifyApiKey($providedKey, $hashedKey)) {
echo "Valid API key\n";
}
CSRF-токены
<?php
declare(strict_types=1);
function generateCsrfToken(): string
{
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
return $token;
}
function verifyCsrfToken(string $token): bool
{
if (!isset($_SESSION['csrf_token'])) {
return false;
}
$valid = hash_equals($_SESSION['csrf_token'], $token);
// Regenerate after use (one-time token)
unset($_SESSION['csrf_token']);
return $valid;
}
Определение оптимального cost для bcrypt
<?php
declare(strict_types=1);
// Find the best cost for your server (target: 250ms-500ms)
function findOptimalBcryptCost(float $targetMs = 350.0): int
{
$cost = 8;
do {
$cost++;
$start = microtime(true);
password_hash('benchmark', PASSWORD_BCRYPT, ['cost' => $cost]);
$elapsed = (microtime(true) - $start) * 1000;
} while ($elapsed < $targetMs && $cost < 31);
return $cost;
}
echo "Optimal cost: " . findOptimalBcryptCost() . "\n";
Контрольные суммы файлов
<?php
declare(strict_types=1);
// Generate checksums for directory
function generateChecksums(string $directory): array
{
$checksums = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$path = $file->getPathname();
$checksums[$path] = hash_file('sha256', $path);
}
}
return $checksums;
}
// Verify file integrity
function verifyChecksums(array $checksums): array
{
$corrupted = [];
foreach ($checksums as $path => $expectedHash) {
if (!file_exists($path)) {
$corrupted[$path] = 'MISSING';
continue;
}
$actualHash = hash_file('sha256', $path);
if (!hash_equals($expectedHash, $actualHash)) {
$corrupted[$path] = 'MODIFIED';
}
}
return $corrupted;
}