La nueva API para codificar contraseñas de PHP 5.5

30 de junio de 2013

La novedad más importante de PHP 5.5 relacionada con la seguridad es la creación de una nueva API para codificar y verificar contraseñas. Además de ser muy fácil de utilizar, esta API sigue todas las buenas prácticas de seguridad recomendadas para aplicaciones web modernas.

Internamente la API utiliza la función crypt() y está disponible desde la versión 5.5.0 de PHP. Si utilizas una versión anterior de PHP, siempre que sea igual o superior a 5.3.7, existe una librería con las mismas funcionalidades que la nueva API: github.com/ircmaxell/password_compat.

Codificando contraseñas

La función más importante de la nueva API es password_hash(), que codifica la contraseña que le pases con el algoritmo indicado:

$password = password_hash('admin1234', PASSWORD_DEFAULT);
// $password = $2y$10$a8ThA3xOuWB6QPeFL/VRZeynSxoT6chW4RS1uh5l0xOBEePFnUNpG

El primer argumento de la función es la contraseña original sin codificar y el segundo argumento debe ser una de las dos siguientes constantes:

  • PASSWORD_DEFAULT, codifica la contraseña utilizando el algoritmo bcrypt y el resultado es una cadena de 60 caracteres de longitud, cuyos primeros caracteres son $2y$10$. El algoritmo utilizado y la longitud de la contraseña codificada cambiarán en las próximas versiones de PHP, cuando se añadan algoritmos todavía más seguros. Si guardas las contraseñas en una base de datos, la recomendación es que reserves 255 caracteres para ello y no los 60 que se pueden utilizar actualmente.
  • PASSWORD_BCRYPT, a pesar de su nombre, codifica la contraseña utilizando el algoritmo CRYPT_BLOWFISH. Al igual que en el caso anterior, la contraseña codificada ocupa 60 caracteres en total, siendo los primeros caracteres $2y$.
$password = password_hash('admin1234', PASSWORD_DEFAULT);
// $password = $2y$10$a8ThA3xOuWB6QPeFL/VRZeynSxoT6chW4RS1uh5l0xOBEePFnUNpG

$password = password_hash('admin1234', PASSWORD_BCRYPT);
// $password = $2y$10$1Y2nAYh7gVThKyL2H/FkIuAqU0Z4ZZNUz3rqFNSbq7RiVoyWsXM5a

Aunque te cueste creerlo, estas funciones para codificar contraseñas se han diseñado a propósito para que sean muy lentas y para que siempre tarden el mismo tiempo en obtener el resultado. De esta forma se evitan los ataques de seguridad conocidos como timing attack.

El tiempo empleado en codificar una contraseña se denomina "coste" y se puede configurar mediante el tercer argumento opcional de la función password_hash(). El coste por defecto es 10 (por eso el prefijo de las contraseñas anteriores es $2y$10$) y su valor debe estar comprendido entre 04 y 31. Si tu servidor es muy potente, puedes subir el coste de la siguiente manera:

$password = password_hash('admin1234', PASSWORD_DEFAULT);
// $password = $2y$10$a8ThA3xOuWB6QPeFL/VRZeynSxoT6chW4RS1uh5l0xOBEePFnUNpG

$password = password_hash('admin1234', PASSWORD_DEFAULT, array('cost' => 12));
// $password = $2y$12$hrxMLCk3ZfKNEcY1lpTpMOD09TnHO5vlJp5CR4xYud7a4RACWoRFm

$password = password_hash('admin1234', PASSWORD_DEFAULT, array('cost' => 20));
// $password = $2y$20$UX1pRArPfTNsUn8czY8AE.8OakctlMk6.or6OmQNQ.mpzyHvm29ki

La recomendación oficial para elegir el coste más adecuado para tu servidor es que el tiempo empleado en codificar una contraseña sea entre 0.1y 0.5 segundos.

Además del coste, la función password_hash() también permite configurar la opción salt, que es el valor inicial que se utiliza para codificar la contraseña. Se recomienda no establecer este valor a mano y dejar que sea PHP quien lo calcule automáticamente. Si quieres saber cómo establecer este valor a mano, consulta la documentación de password_hash().

Si hasta ahora guardabas en la base de datos las contraseñas originales porque no sabías cómo codificarlas, ya no tienes excusa. Gracias a la nueva API para codificar las contraseñas, hacer las cosas bien desde el punto de vista de la seguridad cuesta todavía menos tiempo y esfuerzo que hacerlas mal.

Comprobando contraseñas

Codificar las contraseñas antes de guardarlas en la base de datos es sólo la primera parte del problema. Después, cada vez que un usuario quiera conectarse a tu aplicación, hay que comprobar que la contraseña proporcionada es efectivamente la misma que se guardó codificada.

La nueva función password_verify() simplifica este trabajo al máximo, ya que solamente hay que pasarle como primer argumento la contraseña del usuario y como segundo argumento la contraseña codificada:

$original = 'admin1234';
$codificado = '$2y$10$a8ThA3xOuWB6QPeFL/VRZeynSxoT6chW4RS1uh5l0xOBEePFnUNpG';

$iguales = password_verify($original, $codificado);

if ($iguales) {
    echo 'Puedes pasar a la zona privada';
} else {
    echo 'La contraseña indicada no es correcta';
}

Como las contraseñas codificadas por password_hash() ya contienen en su interior el coste y el salt utilizados para codificar la contraseña original, no hay que indicar ningún parámetro más a la función password_verify().

Otras utilidades

La nueva API para codificar contraseñas incluye otras dos funciones menos importantes. La primera es password_get_info(), que devuelve un array con información de la contraseña codificada que se le pasa como argumento:

$info = password_get_info('$2y$20$UX1pRArPfTNsUn8czY8AE.8OakctlMk6.or6OmQNQ.mpzyHvm29ki');

$info = array(
    'algo'     => 1,        // identificador del algoritmo utilizado
    'algoName' => 'bcrypt', // nombre del algoritmo utilizado
    'options'  => array(
        'cost' => 20        // coste utilizado en la codificación
    )
);

Si en tu aplicación modificas con el tiempo el algoritmo o las opciones para codificar las contraseñas, necesitarás comprobar si una determinada contraseña debe ser recodificada o si cumple las nuevas opciones. Para ello puedes utilizar la función password_needs_rehash(), pasando la contraseña codificada como primer argumento, el algoritmo que se debe utilizar como segundo argumento y opcionalmente, las opciones del algoritmo como tercer argumento.

En el siguiente ejemplo, se pasa una contraseña codificada con el algoritmo por defecto y un coste de 10. Como las características deseadas son el algoritmo por defecto y un coste de 20, el resultado de la función password_needs_rehash() será true:

$hayQueRecodificar = password_needs_rehash(
    '$2y$10$a8ThA3xOuWB6QPeFL/VRZeynSxoT6chW4RS1uh5l0xOBEePFnUNpG',
    PASSWORD_DEFAULT,
    array('cost' => 20)
);

// $hayQueRecodificar = true