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 siemprehash_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)oPASSWORD_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.
