MidТеория9 min

Хеширование

hash(), password_hash(), HMAC, timing-safe сравнение, key derivation

Хеширование -- это преобразование данных произвольной длины в строку фиксированной длины. Хеш-функция работает только в одну сторону: из хеша невозможно восстановить исходные данные. 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;
}

Тесты

Вопросы с экзамена ZCE

Проверь себя

5 из 19

Какие блочные алгоритмы поддерживает Mcrypt? Каждый правильный ответ является полным решением. Выберите все подходящие.

Выберите все правильные варианты

Какой алгоритм password_hash() использует по умолчанию (PASSWORD_DEFAULT)?

Какая из следующих функций лучше всего подходит для получения отпечатка (fingerprint) строки?

Какую функцию следует использовать для инкрементального хеширования больших данных?

Для чего используется HMAC?