Entender las funciones de hash y mantener las contraseñas seguras

De vez en cuando, los servidores y las bases de datos son robados o comprometidos. Teniendo esto en cuenta, es importante asegurarse de que algunos datos cruciales del usuario, como las contraseñas, no puedan recuperarse. Hoy, aprenderemos los conceptos básicos del hash y lo que se necesita para proteger las contraseñas en sus aplicaciones web..

Tutorial republicado

Cada pocas semanas, revisamos algunas de las publicaciones favoritas de nuestros lectores de toda la historia del sitio. Este tutorial fue publicado por primera vez en enero de 2011..


1. Descargo de responsabilidad

La criptología es un tema suficientemente complicado, y de ninguna manera soy un experto. Se están realizando investigaciones constantes en esta área, en muchas universidades y agencias de seguridad..

En este artículo, intentaré mantener las cosas lo más simples posible, al tiempo que le presentaré un método razonablemente seguro para almacenar contraseñas en una aplicación web.


2. ¿Qué hace el "hash"??

El hash convierte un dato (ya sea pequeño o grande) en un dato relativamente corto, como una cadena o un entero..

Esto se logra mediante el uso de una función hash de una sola vía. "Unidireccional" significa que es muy difícil (o prácticamente imposible) revertirlo.

Un ejemplo común de una función hash es md5 (), que es bastante popular en muchos idiomas y sistemas diferentes..

$ data = "Hello World"; $ hash = md5 ($ datos); echo $ hash; // b10a8db164e0754105b7a99be72e3fe5

Con md5 (), El resultado siempre será una cadena de 32 caracteres. Pero, contiene solo caracteres hexadecimales; técnicamente también se puede representar como un entero de 128 bits (16 bytes). Puedes md5 () cadenas y datos mucho más largos, y aún así terminará con un hash de esta longitud. Este solo hecho podría darte una idea de por qué esto se considera una función "unidireccional"..


3. Usando una función de hash para almacenar contraseñas

El proceso habitual durante un registro de usuario:

  • El usuario completa el formulario de registro, incluido el campo de contraseña.
  • El script web almacena toda la información en una base de datos.
  • Sin embargo, la contraseña se ejecuta a través de una función hash, antes de ser almacenada.
  • La versión original de la contraseña no se ha almacenado en ningún lugar, por lo que se descarta técnicamente.

Y el proceso de inicio de sesión:

  • El usuario ingresa el nombre de usuario (o correo electrónico) y la contraseña.
  • El script ejecuta la contraseña a través de la misma función hash.
  • El script encuentra el registro de usuario de la base de datos y lee la contraseña de hash almacenada.
  • Ambos valores se comparan, y el acceso se concede si coinciden.

Una vez que decidamos un método decente para hashear la contraseña, implementaremos este proceso más adelante en este artículo..

Tenga en cuenta que la contraseña original nunca se ha almacenado en ningún lugar. Si la base de datos es robada, los inicios de sesión del usuario no pueden verse comprometidos, ¿verdad? Bueno, la respuesta es "depende". Veamos algunos problemas potenciales.


4. Problema # 1: Hash Collision

Una "colisión" de hash se produce cuando dos entradas de datos diferentes generan el mismo hash resultante. La probabilidad de que esto suceda depende de la función que utilice.

¿Cómo puede ser explotado??

Como ejemplo, he visto algunos scripts más antiguos que usaban crc32 () para hash de contraseñas. Esta función genera un entero de 32 bits como resultado. Esto significa que solo hay 2 ^ 32 (es decir, 4,294,967,296) resultados posibles.

Hashemos una contraseña:

echo crc32 ('supersecretpassword'); // salidas: 323322056

Ahora, asumamos el rol de una persona que ha robado una base de datos y tiene el valor hash. Es posible que no podamos convertir 323322056 en 'supersecretpassword', sin embargo, podemos averiguar otra contraseña que se convertirá al mismo valor hash, con un simple script:

set_time_limit (0); $ i = 0; while (verdadero) if (crc32 (base64_encode ($ i)) == 323322056) echo base64_encode ($ i); salida;  $ i ++; 

Esto puede funcionar por un tiempo, aunque, eventualmente, debería devolver una cadena. Podemos usar esta cadena devuelta, en lugar de 'supersecretpassword', y nos permitirá iniciar sesión con éxito en la cuenta de esa persona.

Por ejemplo, después de ejecutar este script exacto por unos momentos en mi computadora, me dieron 'MTIxMjY5MTAwNg =='. Vamos a probarlo:

echo crc32 ('supersecretpassword'); // salidas: 323322056 echo crc32 ('MTIxMjY5MTAwNg =='); // salidas: 323322056

¿Cómo se puede prevenir esto??

Hoy en día, una poderosa PC doméstica puede usarse para ejecutar una función hash casi mil millones de veces por segundo. Así que necesitamos una función hash que tenga una muy gran alcance.

Por ejemplo, md5 () Podría ser adecuado, ya que genera hashes de 128 bits. Esto se traduce en 340,282,366,920,938,463,463,374,607,431,768,211,456 posibles resultados. Es imposible pasar por tantas iteraciones para encontrar colisiones. Sin embargo, algunas personas todavía han encontrado maneras de hacer esto (vea aquí).

Sha1

Sha1 () es una mejor alternativa y genera un valor hash de 160 bits aún más largo.


5. Problema # 2: tablas del arco iris

Incluso si solucionamos el problema de colisión, todavía no estamos seguros.

Una tabla de arco iris se construye calculando los valores de hash de las palabras de uso común y sus combinaciones.

Estas tablas pueden tener hasta millones o incluso miles de millones de filas.

Por ejemplo, puede revisar un diccionario y generar valores hash para cada palabra. También puedes comenzar a combinar palabras y generar hashes para esos también. Eso no es todo; incluso puede comenzar a agregar dígitos antes / después / entre palabras, y almacenarlos en la tabla también.

Teniendo en cuenta lo barato que es el almacenamiento en la actualidad, se pueden producir y utilizar gigantescas tablas Rainbow..

¿Cómo puede ser explotado??

Imaginemos que se roba una gran base de datos, junto con 10 millones de hashes de contraseña. Es bastante fácil buscar en la tabla del arco iris para cada uno de ellos. No todos se encontrarán, ciertamente, pero, sin embargo ... algunos de ellos!

¿Cómo se puede prevenir esto??

Podemos intentar agregar una "sal". Aquí hay un ejemplo:

$ password = "easypassword"; // esto se puede encontrar en una tabla de arco iris // porque la contraseña contiene 2 palabras comunes echo sha1 ($ contraseña); // 6c94d3b42518febd4ad747801d50a8972022f956 // usa un montón de caracteres aleatorios, y puede ser más largo que este $ salt = "f # @ V) Hu ^% Hgfds"; // esto NO se encontrará en ninguna tabla arco iris pre-construida echo sha1 ($ sal. $ contraseña); // cd56a16759623378628c0d9336af69b74d9d71a5

Lo que básicamente hacemos es concatenar la cadena "salt" con las contraseñas antes de marcarlas. La cadena resultante obviamente no estará en ninguna tabla de arco iris pre-construida. Pero, todavía no estamos seguros todavía!


6. Problema # 3: tablas del arco iris (otra vez)

Recuerde que una tabla Rainbow se puede crear desde cero, después de que la base de datos haya sido robada.

¿Cómo puede ser explotado??

Incluso si se usó una sal, esto puede haber sido robado junto con la base de datos. Todo lo que tienen que hacer es generar una nueva tabla del arco iris desde cero, pero esta vez concatenan la sal a cada palabra que ponen en la tabla..

Por ejemplo, en una tabla genérica del arco iris, "contraseña fácil"Puede existir. Pero en esta nueva tabla del arco iris, tienen"f # @ V) Hu ^% Hgfdseasypassword"también. Cuando ejecuten todos los 10 millones de hash salados robados en contra de esta mesa, podrán nuevamente encontrar algunas coincidencias.

¿Cómo se puede prevenir esto??

Podemos usar una "sal única" en su lugar, que cambia para cada usuario.

Un candidato para este tipo de sal es el valor de identificación del usuario de la base de datos:

$ hash = sha1 ($ user_id. $ password);

Esto se supone que el número de identificación de un usuario nunca cambia, lo que suele ser el caso.

También podemos generar una cadena aleatoria para cada usuario y usarla como sal única. Pero tendríamos que asegurarnos de que almacenamos eso en el registro del usuario en algún lugar.

// genera una función de cadena aleatoria de 22 caracteres de longitud unique_salt () return substr (sha1 (mt_rand ()), 0,22);  $ unique_salt = unique_salt (); $ hash = sha1 ($ unique_salt. $ password); // y guarde el $ unique_salt con el registro de usuario // ... 

Este método nos protege contra Rainbow Tables, porque ahora todas las contraseñas han sido incluidas con un valor diferente. El atacante tendría que generar 10 millones de tablas arco iris separadas, lo que sería completamente impráctico..


7. Problema # 4: Hash Speed

La mayoría de las funciones de hash se han diseñado teniendo en cuenta la velocidad, ya que a menudo se utilizan para calcular valores de suma de comprobación para grandes conjuntos de datos y archivos, para verificar la integridad de los datos.

¿Cómo puede ser explotado??

Como mencioné anteriormente, una PC moderna con potentes GPU (sí, tarjetas de video) puede programarse para calcular aproximadamente mil millones de hashes por segundo. De esta manera, pueden usar un ataque de fuerza bruta para probar cada contraseña posible.

Puede pensar que requerir una contraseña con un mínimo de 8 caracteres puede protegerlo de un ataque de fuerza bruta, pero determinemos si ese es el caso:

  • Si la contraseña puede contener letras en minúsculas, mayúsculas y un número, es decir 62 (26 + 26 + 10) caracteres posibles.
  • Una cadena de 8 caracteres tiene 62 ^ 8 versiones posibles. Eso es un poco más de 218 trillones..
  • A una velocidad de 1 billón de hashes por segundo, esto se puede resolver en aproximadamente 60 horas..

Y para contraseñas de 6 caracteres, lo que también es bastante común, tomaría menos de 1 minuto.

Siéntase libre de requerir contraseñas de 9 o 10 caracteres, sin embargo, puede comenzar a molestar a algunos de sus usuarios..

¿Cómo se puede prevenir esto??

Usa una función hash más lenta.

Imagina que utilizas una función hash que solo puede ejecutarse 1 millón de veces por segundo en el mismo hardware, en lugar de 1 billón de veces por segundo. Luego, el atacante tardaría 1000 veces más en forzar un hash. 60 horas se convertirían en casi 7 años.!

Una forma de hacerlo sería implementarlo usted mismo:

function myhash ($ password, $ unique_salt) $ salt = "f # @ V) Hu ^% Hgfds"; $ hash = sha1 ($ unique_salt. $ password); // hacer que se tarde 1000 veces más para ($ i = 0; $ i < 1000; $i++)  $hash = sha1($hash);  return $hash; 

O puede usar un algoritmo que admita un "parámetro de costo", como BLOWFISH. En PHP, esto se puede hacer usando el cripta() función.

function myhash ($ password, $ unique_salt) // la sal para blowfish debería tener una cripta de retorno de 22 caracteres ($ password, '$ 2a $ 10 $'. $ unique_salt); 

El segundo parámetro para el cripta() La función contiene algunos valores separados por el signo de dólar ($)..

El primer valor es '$ 2a', lo que indica que usaremos el algoritmo BLOWFISH.

El segundo valor, '$ 10' en este caso, es el "parámetro de costo". Este es el logaritmo base-2 de cuántas iteraciones ejecutará (10 => 2 ^ 10 = 1024 iteraciones). Este número puede oscilar entre 04 y 31.

Vamos a poner un ejemplo:

function myhash ($ password, $ unique_salt) return crypt ($ password, '$ 2a $ 10 $'. $ unique_salt);  function unique_salt () return substr (sha1 (mt_rand ()), 0,22);  $ password = "verysecret"; echo myhash ($ password, unique_salt ()); // resultado: $ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC

El hash resultante contiene el algoritmo ($ 2a), el parámetro de costo ($ 10) y la sal de 22 caracteres que se usó. El resto es el hash calculado. Vamos a hacer una prueba:

// asuma que se extrajo de la base de datos $ hash = '$ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; // asume que esta es la contraseña que el usuario ingresó para volver a iniciar sesión en $ password = "verysecret"; if (check_password ($ hash, $ password)) echo "Access Granted!";  else echo "¡Acceso denegado!";  function check_password ($ hash, $ password) // los primeros 29 caracteres incluyen algoritmo, costo y sal // llamémoslo $ full_salt $ full_salt = substr ($ hash, 0, 29); // ejecuta la función hash en $ password $ new_hash = crypt ($ password, $ full_salt); // devuelve retorno verdadero o falso ($ hash == $ new_hash); 

Cuando ejecutamos esto, vemos "Acceso concedido!"


8. Juntándolo

Con todo lo anterior en mente, escribamos una clase de utilidad basada en lo que hemos aprendido hasta ahora:

class PassHash // blowfish private static $ algo = '$ 2a'; // parámetro de costo privado estático $ costo = '$ 10'; // principalmente para uso interno público static function unique_salt () return substr (sha1 (mt_rand ()), 0,22);  // esto se usará para generar un hash función hash pública ($ contraseña) return crypt ($ contraseña, self :: $ algo. self :: $ cost. '$'. self :: unique_salt ());  // esto se usará para comparar una contraseña con una función estática pública hash check_password ($ hash, $ password) $ full_salt = substr ($ hash, 0, 29); $ new_hash = crypt ($ password, $ full_salt); return ($ hash == $ new_hash); 

Aquí está el uso durante el registro de usuario:

// incluir la clase require ("PassHash.php"); // lea todos los datos ingresados ​​desde $ _POST //… // haga su validación de formulario normal // // // hash la contraseña $ pass_hash = PassHash :: hash ($ _ POST ['password']); // almacenar toda la información del usuario en la base de datos, excluyendo $ _POST ['contraseña'] // almacenar $ pass_hash en su lugar // ... 

Y aquí está el uso durante un proceso de inicio de sesión de usuario:

// incluir la clase require ("PassHash.php"); // lea todos los datos ingresados ​​desde $ _POST // ... // busque el registro de usuario basado en $ _POST ['nombre de usuario'] o similar // ... // verifique la contraseña con la que el usuario intentó iniciar sesión si (PassHash :: check_password ( $ usuario ['pass_hash'], $ _POST ['contraseña']) // conceder acceso // ... else else // // denegar acceso // ...

9. Una nota sobre la disponibilidad de Blowfish

Es posible que el algoritmo Blowfish no esté implementado en todos los sistemas, a pesar de que ya es bastante popular. Puedes verificar tu sistema con este código:

if (CRYPT_BLOWFISH == 1) echo "Sí";  else echo "No"; 

Sin embargo, a partir de PHP 5.3, no necesita preocuparse; PHP se envía con esta implementación incorporada.


Conclusión

Este método de hashing de contraseñas debería ser lo suficientemente sólido para la mayoría de las aplicaciones web. Dicho esto, no lo olvide: también puede exigir que sus miembros usen contraseñas más seguras, al imponer longitudes mínimas, caracteres mixtos, dígitos y caracteres especiales..

Una pregunta para ti, lector: ¿cómo hash tus contraseñas? ¿Puedes recomendar alguna mejora sobre esta implementación??