Apuntes sobre Php

PHP: Porque a veces necesitas un poco de caos en tu vida.

Contraseñas seguras

Guardar contraseñas en texto plano es un error gravísimo. Guardarlas como MD5 o SHA1 tampoco es seguro: son algoritmos rápidos diseñados para verificación de integridad, no para proteger contraseñas, y las tablas rainbow las rompen en segundos. PHP incluye desde la versión 5.5 funciones específicas para este problema.

Por qué no MD5 ni SHA1

Un buen algoritmo de hash de contraseñas debe ser lento deliberadamente: cuanto más tiempo tarda en calcular un hash, más difícil es para un atacante probar millones de contraseñas por fuerza bruta. MD5 puede calcular miles de millones de hashes por segundo en hardware moderno. Bcrypt, el algoritmo que PHP usa por defecto, está diseñado para tardar un tiempo configurable.

Además, las funciones de PHP para contraseñas añaden automáticamente una sal aleatoria a cada hash, lo que hace inútiles las tablas rainbow: dos usuarios con la misma contraseña tendrán hashes distintos.

password_hash()

Genera un hash seguro a partir de una contraseña en texto plano. La sal aleatoria se incluye en el resultado, así que no necesitas guardarla por separado:

<?php

$contrasena_plana = "miContraseñaSecreta";
$hash = password_hash($contrasena_plana, PASSWORD_DEFAULT);

echo $hash;
// $2y$12$... (cadena de ~60 caracteres que incluye algoritmo, coste y sal)

PASSWORD_DEFAULT usa bcrypt y actualiza automáticamente el algoritmo si PHP cambia el estándar en futuras versiones. El campo en base de datos debe tener al menos 255 caracteres de longitud para acomodar posibles algoritmos futuros.

Se puede ajustar el coste de bcrypt (por defecto 12). Un coste más alto es más lento y más seguro; el valor adecuado es el más alto que tu servidor puede aceptar sin que el login resulte lento para el usuario:

<?php

$hash = password_hash($contrasena_plana, PASSWORD_BCRYPT, ["cost" => 13]);

password_verify()

Compara una contraseña en texto plano con un hash almacenado. Devuelve true si coinciden. Usa comparación en tiempo constante para evitar ataques de temporización:

<?php

$hash_en_bd    = obtener_hash_de_bd($email); // valor guardado en el registro del usuario
$contrasena_ok = password_verify($contrasena_del_formulario, $hash_en_bd);

if ($contrasena_ok) {
    echo "Login correcto";
} else {
    echo "Credenciales incorrectas";
}

Nota: Nunca uses == o === para comparar hashes: son vulnerables a ataques de temporización. Usa siempre password_verify().

password_needs_rehash()

Si en el futuro subes el coste o PHP adopta un algoritmo mejor, los hashes antiguos siguen siendo válidos pero no aprovechan la mejora. password_needs_rehash() detecta cuándo un hash fue generado con parámetros distintos a los actuales, para regenerarlo al siguiente login (cuando tienes la contraseña en plano por un instante):

<?php

if (password_verify($contrasena_del_formulario, $hash_en_bd)) {
    if (password_needs_rehash($hash_en_bd, PASSWORD_DEFAULT)) {
        $nuevo_hash = password_hash($contrasena_del_formulario, PASSWORD_DEFAULT);
        actualizar_hash_en_bd($usuario_id, $nuevo_hash);
    }
    // continuar con el login
}

Flujo completo: registro y login

Registro: hashear la contraseña antes de guardarla:

<?php

function registrar_usuario(string $email, string $contrasena): void
{
    // Validar contraseña mínima antes de hashear
    if (strlen($contrasena) < 8) {
        throw new InvalidArgumentException("La contraseña debe tener al menos 8 caracteres.");
    }

    $hash = password_hash($contrasena, PASSWORD_DEFAULT);

    // guardar $email y $hash en la base de datos
    // (usaremos prepared statements en la próxima lección)
}

Login: leer el hash de la BD y verificar:

<?php

function login(string $email, string $contrasena): bool
{
    // obtener el hash almacenado para ese email
    $hash_en_bd = obtener_hash_de_bd($email);

    if ($hash_en_bd === null) {
        return false; // usuario no existe
    }

    if (!password_verify($contrasena, $hash_en_bd)) {
        return false; // contraseña incorrecta
    }

    if (password_needs_rehash($hash_en_bd, PASSWORD_DEFAULT)) {
        $nuevo_hash = password_hash($contrasena, PASSWORD_DEFAULT);
        actualizar_hash_en_bd($email, $nuevo_hash);
    }

    return true;
}

Recapitulación

  • Nunca guardes contraseñas en texto plano ni con MD5/SHA1: son rompibles en segundos.
  • password_hash($contrasena, PASSWORD_DEFAULT) genera un hash con sal aleatoria incluida. El campo en BD necesita 255 caracteres.
  • password_verify($contrasena, $hash) compara de forma segura. Nunca uses ==.
  • password_needs_rehash() permite migrar hashes antiguos al siguiente login sin forzar al usuario a cambiar su contraseña.

En la próxima lección: bases de datos con PHP y mysqli: conexión, consultas y lectura de resultados.

TOP