Funciones de hash en PHP: hash(), hash_hmac(), hash_equals y timing attacks

PHP incluye una familia de funciones hash_* para generar digests criptográficos, firmar mensajes y comparar valores de forma segura. Son esenciales para validar webhooks, crear tokens y proteger datos.

hash(): generar un digest

<?php
// Algoritmos comunes
echo hash('sha256', 'Hola mundo');
echo hash('sha512', 'Hola mundo');
echo hash('md5',    'Hola mundo');  // inseguro para contraseñas; usa password_hash()

// Salida en binario (útil para HMAC encadenado)
$bytes = hash('sha256', 'datos', true);

// Algoritmos disponibles en tu instalación
print_r(hash_algos());
?>

hash_hmac(): firmar mensajes

HMAC (Hash-based Message Authentication Code) combina una clave secreta con el mensaje para generar una firma que solo quien conoce la clave puede verificar:

<?php
$secreto = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');

$firma_calculada = 'sha256=' . hash_hmac('sha256', $payload, $secreto);
$firma_recibida  = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';

if (!hash_equals($firma_calculada, $firma_recibida)) {
    http_response_code(401);
    exit('Firma inválida');
}

// Proceso el webhook de GitHub...
?>

Validar webhooks de Stripe

<?php
function verificarStripe(string $payload, string $cabecera, string $secreto): bool
{
    // Cabecera: t=1614900000,v1=abc123...
    $partes = [];
    foreach (explode(',', $cabecera) as $parte) {
        [$clave, $valor] = explode('=', $parte, 2);
        $partes[$clave] = $valor;
    }

    $timestamp = $partes['t'] ?? '';
    $firma     = $partes['v1'] ?? '';

    // Stripe construye la firma sobre timestamp + '.' + payload
    $datos    = $timestamp . '.' . $payload;
    $esperada = hash_hmac('sha256', $datos, $secreto);

    return hash_equals($esperada, $firma);
}
?>

hash_equals(): comparación segura contra timing attacks

Un timing attack mide el tiempo que tarda una comparación de strings para deducir cuántos caracteres coinciden. hash_equals() siempre tarda el mismo tiempo independientemente de dónde divergen los strings:

<?php
// VULNERABLE: el atacante puede medir diferencias de microsegundos
if ($token_recibido === $token_esperado) { ... }

// SEGURO
if (hash_equals($token_esperado, $token_recibido)) { ... }
?>

Generar tokens de verificación de email sin base de datos

<?php
$secreto = getenv('APP_SECRET');

function generarTokenEmail(int $userId, string $email): string
{
    global $secreto;
    $expira = time() + 3600; // 1 hora
    $datos  = "$userId|$email|$expira";
    $firma  = hash_hmac('sha256', $datos, $secreto);
    return base64_encode("$datos|$firma");
}

function verificarTokenEmail(string $token): ?array
{
    global $secreto;
    $decoded = base64_decode($token);
    $partes  = explode('|', $decoded);

    if (count($partes) !== 4) return null;

    [$userId, $email, $expira, $firmaRecibida] = $partes;

    $datos = "$userId|$email|$expira";
    $firmaEsperada = hash_hmac('sha256', $datos, $secreto);

    if (!hash_equals($firmaEsperada, $firmaRecibida)) return null;
    if (time() > (int)$expira) return null;

    return ['user_id' => (int)$userId, 'email' => $email];
}

// Generar
$token = generarTokenEmail(42, '[email protected]');

// Verificar al hacer clic en el enlace
$datos = verificarTokenEmail($token);
if ($datos) {
    echo "Email verificado para usuario #{$datos['user_id']}n";
}
?>

URLs de descarga temporal

<?php
function urlDescargaTemporal(string $fichero, int $segundos = 300): string
{
    $secreto = getenv('DESCARGA_SECRET');
    $expira  = time() + $segundos;
    $firma   = hash_hmac('sha256', "$fichero|$expira", $secreto);
    return "/descargar?f=" . urlencode($fichero) . "&e=$expira&sig=$firma";
}

function validarUrlDescarga(string $fichero, int $expira, string $firma): bool
{
    $secreto  = getenv('DESCARGA_SECRET');
    $esperada = hash_hmac('sha256', "$fichero|$expira", $secreto);
    return time() <= $expira && hash_equals($esperada, $firma);
}
?>

Errores comunes

  • Usar == en lugar de hash_equals(): el operador == es vulnerable a timing attacks. Usa siempre hash_equals() para comparar MACs y tokens.
  • md5 o sha1 para contraseñas: estos algoritmos son demasiado rápidos para contraseñas. Usa password_hash(PASSWORD_BCRYPT) o PASSWORD_ARGON2ID.
  • Secreto demasiado corto: el secreto de HMAC debe tener al menos 32 bytes aleatorios (bin2hex(random_bytes(32))). Una cadena predecible anula la seguridad.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP