Almacenar contraseñas de forma segura (o casi)

etiquetas: programación, php, seguridad, criptografía

Hace muy poco empecé a trabajar en un par de proyectos que utilizarán criptografía para garantizar la privacidad de los usuarios. De entre todo lo que esos proyectos van a implicar en cuanto a seguridad, hoy quiero hablar de las funciones hash y de su relación con las contraseñas.

1. Funciones hash

1.1. ¿Qué son?

También conocidas como «resúmenes criptográficos», funcionan tomando un conjunto variable de datos, y después de aplicar una serie de transformaciones sobre ellos, devuelven una cadena de longitud determinada, a la que podríamos llamar huella. Para que sean útiles en el campo de la criptografía, uno de sus requisitos es que debe ser computacionalmente imposible encontrar dos conjuntos de datos con el mismo hash: esto se llama resistencia a colisiones.

MD5("cadena uno") => ae9be994423206ae723bf717b6c32f96
MD5("cadena dos") => e2b8d5adcd7b0ace8a931972febd6909
El algoritmo MD5 es una de las muchas funciones hash que existen.

Se utilizan para comprobar la integridad de los datos transmitidos por cualquier medio, y también como parte fundamental en algoritmos HMAC.

1.2. ¿Cómo se atacan?

Los ataques de fuerza bruta y de diccionario son los más sencillos de todos: consisten en probar de forma sistemática una contraseña tras otra hasta encontrar las adecuadas. Aunque pueda parecer difícil, han tenido un gran porcentaje de éxito frente a muchos sistemas de autenticación mal diseñados, y sobre todo frente a contraseñas débiles.

Security
«Criptoanálisis» rubber-hose. Imagen cortesía de xkcd, y bajo licencia CC.

La clave es la rapidez con que se ejecutan estas funciones: actualmente no es difícil reunir la potencia de cálculo necesaria para romper millones de contraseñas en apenas unos días. Y a eso hay que sumar la existencia de las rainbow tables, que son conjuntos enormes de contraseñas «rotas» con anterioridad y posteriormente almacenadas... Pero también un síntoma de que la gente no se toma en serio la seguridad, como veremos en un instante.

1.3. ¿Cómo se protegen?

Una de las formas de intentar mejorar la seguridad es evitar que para la misma entrada se genere siempre el mismo resumen. Si esto no forma parte del diseño original de alguna función, podemos conseguirlo añadiendo nosotros mismos una cadena aleatoria.

MD5("contraseña" + "salt1") => 4aac1257f605abbceb29c695f6ea6d33
MD5("contraseña" + "salt2") => e5123d1d9cdd4773818f5d9f9fa1aec4
Aplicando una sal a MD5.

Así ralentizamos un ataque de fuerza bruta obligando a buscar las contraseñas individualmente, y evitamos que se utilicen tablas precalculadas. Pero dado que la función sigue siendo muy rápida, se pueden averiguar con éxito un buen número de contraseñas en poco tiempo. Por eso hubo que diseñar funciones capaces de hacer frente a estos ataques.

Password Reuse
¿Os sigue pareciendo buena idea reutilizar contraseñas? Imagen cortesía de xkcd, y bajo licencia CC.

Es necesario añadir que esta forma de protección data de los años 70, cuando era utilizada en las primeras versiones de Unix: se limitaba la longitud de las contraseñas a ocho caracteres y se añadían 12 bits aleatorios, con lo que se podían obtener 4.096 resúmenes distintos para cada una. Lo sorprendente es que si hace más de cuarenta años ya sabíamos cómo proteger las contraseñas, ¿por qué han llegado a existir las rainbow tables?

2. La función bcrypt: diseñada para proteger contraseñas

Desde la versión 5.5.0 de PHP disponemos de password_hash, que por defecto utiliza la función bcrypt, y que tiene dos diferencias muy importantes respecto a una función hash tradicional:

  • Aplica una cadena aleatoria de forma totalmente automática. Luego la agrega a la salida de la función, junto con todos los parámetros necesarios para validar contraseñas usando la función password_verify.
  • Está diseñada de forma que sea difícil de implementar en GPUs gracias al uso que hace de la RAM, y permite establecer el coste de calcular cada hash.

A día de hoy, el consenso general es que es la función que más protección nos ofrece si tenemos que almacenar contraseñas en una base de datos. Había muchas esperanzas puestas en scrypt, pero con las necesidades que tienen los sistemas de autenticación web, la seguridad que ofrece puede llegar a ser incluso menor que la ofrecida por bcrypt. Además utiliza más parámetros, lo que hace probable que se cometan errores durante su utilización.

Hace pocos años se llevó a cabo una competición para encontrar funciones más seguras, resultando ganadora Argon2. Con menos de dos años de vida quizá sea un poco pronto para confiar plenamente en su seguridad, y solo el tiempo dirá si es un paso adelante o no. De todas formas merece la pena estar atentos a cualquier novedad que pueda surgir.

2.1. Evaluar los costes de bcrypt

Antes de decidir qué parámetros utilizar en password_hash, es necesario evaluar el impacto que pueden tener en el proceso de autenticación. Para ello he creado un sencillo script que muestra una tabla con los tiempos que se tarda en crear un hash en función del coste. Para hacerlo más fiable lo he diseñado de tal forma que la medida se tome tras ejecutar la función múltiples veces. Y también es fácil de usar, ya que se controla con apenas cuatro variables:

$pass_lengths = [32]; // Longitudes de las contraseñas generadas.
$pass_number = 10;    // Número de contraseñas de cada longitud.

$min_cost = 10;       // Coste mínimo con el que ejecutar la función.
$max_cost = 14;       // Coste máximo con el que ejecutar la función.

La tabla será algo parecido a esto:

COST    #       LENGTH  HASH TIME
---------------------------------
10      10      32      0.068191
11      10      32      0.137203
12      10      32      0.274425
13      10      32      0.547829
14      10      32      1.087198
Los resultados variarán en función del hardware.

Como puede verse, los tiempos tienen un crecimiento exponencial según se aumenta el coste, por lo que tendremos que tener especial cuidado. Se trata de dificultar el trabajo de un atacante, y al mismo tiempo evitar que un usuario legítimo se vea obligado a esperar demasiado tiempo para acceder. Podéis descargaros el script para experimentar con él si queréis.

hashcosts.zip

hashcosts.zip

No considero que sea un código tan importante como para tener que ponerle una licencia, así que considerad que está bajo dominio público y usadlo como mejor os parezca. Solo hay que tener cuidado con no bloquear el servidor donde lo probéis, y también considerad todas las contraseñas como inseguras debido a la forma de generarlas.

2.2. Autenticación mediante contraseñas en PHP

Ahora que ya tenemos una forma de decidir «cuánto» proteger las contraseñas, podemos empezar a pensar en cómo puede ser nuestro sistema de autenticación. Las funciones password_verify y password_needs_rehash son todo lo que necesitamos para implementarlo, y a partir de ese punto construir el sistema de acceso. Además podremos estar preparados para el aumento en la capacidad de los procesadores, e ir aumentando el coste de calcular cada hash según sea necesario.

Actualización de hashes sobre la marcha
Si está previsto, se pueden ir actualizando los hashes según avance la capacidad de cálculo del hardware.

3. Consideraciones finales, ¿estamos protegidos?

Ni muchísimo menos. Si solo nos centramos en «ocultar» contraseñas, sería como poner una cerradura de seguridad en un shōji. Hay muchas otras medidas necesarias para mantener a salvo un sistema el máximo tiempo posible, pero es un tema demasiado amplio y resultaría imposible tratarlo en un solo artículo.

0 comentarios

Añadir comentario