Funciones hash

Contraseñas seguras

Con el poder computacional actual, cada vez es más fácil burlar el control de acceso a los sistemas basados en credenciales compuestas por un usuario y clave. ¿Estás seguro de saber cómo manejar las claves de manera adecuada?

Un software multiusuario con frecuencia necesita un componente para la gestión de las cuentas que tienen acceso a la aplicación. Existen múltiples formas de otorgarles acceso a los usuarios y quizás la más popular es el uso de credenciales compuestas por un usuario y una clave.

Las bases de datos que contienen la información de las cuentas de los usuarios son un blanco frecuente de los atacantes, por ello, es importante proteger correctamente las claves de nuestros usuarios. La mejor manera de hacerlo es utilizando una técnica conocida como: “salted password hashing” 

En realidad el proceso para crear un hash para una clave es algo relativamente simple, dado que el problema de cifrar información ya está resuelto de forma nativa por distintos lenguajes de programación y si no lo hacen de manera nativa, existen bibliotecas que podemos usar para hacerlo. 

Sin embargo, crear un hash, también es algo que fácilmente podríamos hacerlo mal.

¿Qué es password hashing?

Son funciones de una vía que reciben un valor de cualquier longitud y lo transforman en un conjunto de caracteres de longitud fija, que no puede ser reversados para construir el valor original. 

También tienen la característica de que si cambian algo en el valor original, aunque sea un solo carácter, el resultado será completamente distinto.Esta característica resulta útil para proteger las claves, dado que nunca almacenaremos el conjunto de caracteres que conforman la clave de nuestros usuarios. 

Pero entonces, cuando un usuario proporciona su clave para ingresar en el sistema ¿Cómo podemos verificar si la clave es correcta?

En términos generales el proceso es el siguiente:

  • El usuario crea una cuenta en el sistema
  • Se crea el hash de la clave ingresada y dicho hash es almacenado en la base de datos. En ningún momento se almacena la clave sin cifrar.
  • Cuando el usuario intenta ingresar, se crear el hash para la clave proporcionada y es comparada con el hash almacenado en la base de datos.
  • Si son iguales, el usuario puede acceder. Caso contrario, se retorna un mensaje de credenciales inválidas.

En el punto 4, es importante que el mensaje sea genérico y no precisar si el valor incorrecto es el usuario o la clave.

Existen múltiples funciones para crear el hash. Es importante utilizar únicamente  funciones criptográficas para crear el hash, como por ejemplo SHA256, SHA512, RipeMD, etc.

Aún cuando utilicemos una función criptográfica para crear el hash no deberíamos pensar que las claves de nuestros usuarios están almacenadas de forma segura dado que existen varias maneras de recuperar las claves desde los hash de maneras relativamente rápidas. 

Para protegernos podemos usar alguna de las múltiples técnicas que permiten hacer más difícil descubrir las claves originales.

¿Cómo se rompe un hash?

Diccionarios y fuerza bruta

La manera más simple de romper un hash es tratar de adivinar la clave. Para ello planteamos una conjunto de opciones y calculamos su hash, entonces verificamos si alguno de los hash generados es igual al almacenado en la base de datos y de ser así, ¡el hash ha sido roto! 

Las dos maneras más comunes de adivinar claves son: los ataques de diccionario y los ataques de fuerza bruta.

Un ataque de diccionario utiliza un archivo que contiene palabras, frases, claves comunes y otras opciones que son frecuentemente utilizadas como claves. A cada palabra en el archivo se le calcula el hash y es comparado con el hash de la clave almacenada, si coinciden, la palabra del archivo a la cual le corresponde el hash comparado será la clave.

Estos diccionarios son construidos extrayendo palabras de textos y también de bases de datos reales de claves. Adicionalmente se suele aplicar un procesamiento adicional, que permite reemplazar caracteres por otros equivalentes, por ejemplo “hola” se transforma en “h01@”, lo que produce diccionarios más efectivos.

Un ataque de fuerza bruta, busca probar cada posible combinación de caracteres dado una longitud de caracteres. El ataque es muy costoso desde el punto de vista computacional y usualmente es menos eficiente en términos de cuántos hash se logran romper por tiempo de proceso, pero este tipo de ataque, siempre, en algún instante logrará encontrar la clave. La clave debería ser lo suficientemente larga para que el tiempo que tarde en descubrirla sea extenso.

Tablas de búsqueda

Las tablas de búsqueda es un método extremadamente efectivo para descifrar muchos hashes del mismo tipo de manera rápida. Se basa en calcular previamente los hash de claves obtenidas de un diccionario y almacenar la clave junto con su hash en una base de datos.

Tablas de búsqueda inversa

Este tipo de ataque permite a un atacante aplicar un diccionario o un ataque de fuerza bruta a muchos hashes al mismo tiempo.

En primer lugar, el atacante crea una tabla de búsqueda en la cual asigna cada hash de la base de datos a una lista de usuarios que comparten el mismo hash. Luego, el atacante compara los hash con los existentes en la tabla de búsqueda. Este ataque es especialmente efectivo porque es común que muchos usuarios tengan la misma clave.

Tablas arcoíris

Son similares a las tablas de búsqueda, expecto que sacrifican la velocidad con el fin de que las tablas de búsqueda sean más pequeñas. Debido a que son más pequeñas, las soluciones a más hashes se pueden almacenar en la misma cantidad de espacio, haciéndolos más efectivos. Existen tablas arcoíris que pueden descifrar cualquier hash md5 de una clave de hasta 8 caracteres de longitud.

Agregando salt

Las tablas de búsqueda y las tablas arcoíris funcionan debido a que cada clave se codifica exactamente de la misma manera. Si dos usuarios tienen la misma clave, ellos tendrán el mismo hash. Se puede evitar esos ataques agregando aleatoriedad a cada hash, de modo que cuando la misma clave se haya cifrado dos veces, los hash no sean los mismos.

Se puede generar esta aleatoriedad en los hash agregando una cadena aleatoria, conocida como salt, a la clave antes de calcular su hash. Para verificar que la clave es correcta, se necesita conocer el salt, por lo que generalmente, se almacena en la base de datos junto con el hash, o como parte de la propia cadena hash.

El valor del salt no necesita ser secreto. Su función simplemente es dar aleatoriedad a los hash de tal forma que las tablas de búsqueda, tablas de búsqueda inversa y las tablas arcoíris se vuelvan ineficaces.

Un atacante no sabrá de antemano cuál será el valor del salt, por lo que no puede calcular previamente una tabla de búsqueda o una tabla de arcoíris. Si la clave de cada usuario está codificada con un salt diferente, el ataque de la tabla de búsqueda inversa tampoco funcionará.

La manera Incorrecta

Los errores de implementación más comunes son: reutilizar el mismo valor de salt en múltiples hashes o usar una valor salt que es demasiado corto.

Reutilización de salt

Un error es utilizar el mismo salt para cada hash, entonces el valor del salt estará codificado en el programa o se generará aleatoriamente una vez. Esto no es efectivo por que si dos usuarios tienen la misma clave, seguirán teniendo el mismo hash.

Un atacante aún puede usar el ataque de tabla de búsqueda inversa para ejecutar un ataque de diccionario en cada hash al mismo tiempo. Solo debería aplicar el valor del salt a cada clave antes de usar el hash. 

Si el valor del salt está codificado en un software popular, se pueden construir tablas de búsqueda y tablas arcoíris para ese valor de salt, para que sea más fácil descifrar los hashes generados por el software.

Se debe generar un nuevo valor de salt aleatorio cada vez que un usuario crea una nueva cuenta o cambia su clave.

Salt corto

Si el valor del salt es demasiado corto, un atacante puede construir una tabla de búsqueda para cada valor de salt posible. Por ejemplo, si el salt tiene solo tres caracteres ASCII, solo hay 95x95x95 (857.375) valores posibles. Puede parecer mucho, pero si cada tabla de búsqueda contiene solo 1 MB de las claves más comunes, colectivamente serán solo 837 GB

Por la misma razón, el nombre de usuario no debe utilizarse como salt. El nombre de usuario pueden ser únicos para un solo servicio, pero son predecibles y comúnmente se reutilizan entre distintos servicios. Un atacante puede crear tablas de búsqueda para nombres de usuarios comunes y usarlos para descifrar hashes con el nombre de usuario como salt.

Para que un atacante no pueda crear una tabla de búsqueda para cada salt posible, el salt debe ser grande. Una buena regla general es usar un salt que tenga el mismo tamaño que la salida de la función hash. Por ejemplo, la salida de SHA256 es de 256 bits (32 bytes), por lo que el salt debe ser de al menos 32 bytes aleatorios.

Doble hash y funciones hash ilógicas

Otro error común es el uso de combinaciones extrañas de algoritmos para crear hash. Es fácil dejarse llevar e intentar combinar diferentes funciones hash, con la esperanza de que el resultado sea más seguro. 

En realidad no es beneficioso y puede crear problemas de interoperabilidad y en ocasiones obtener hash menos seguros. La recomendación es utilizar un estándar criptográfico y no tratar de inventar uno.

Un argumento común es que utilizar varias funciones hash hace que el proceso de cálculo sea más lento, por lo que el descubrimiento de la clave también será más lento, sin embargo hay una mejor manera de hacerlo.

Si bien es cierto que un atacante no puede atacar un hash cuando no conoce el algoritmo, no se puede tratar de ocultarlo combinando varias funciones hash dado que el atacante eventualmente tendrá acceso al código. Usarlas solo agrega un poco más de tiempo para descubrirlas. Es mejor utilizar un algoritmo diseñado para ser extremadamente difícil de paralelizar. 

Colisiones de hash

Dado que las funciones que crean hash generan una cadena de longitud fija en función de una cantidad variable de datos, con seguridad habrá varias entradas que generen la misma cadena de longitud fija.

Las funciones criptográficas están diseñadas para que las colisiones sean difíciles de encontrar. De vez en cuando, los criptógrafos encuentran “ataques” en las funciones hash que facilitan la búsqueda de colisiones. 

Los ataques de colisión son una señal de que es más probable que una cadena que no sea la clave del usuario tenga el mismo hash. Sin embargo, encontrar colisiones incluso en una función hash débil como md5, requiere mucha potencia de cálculo, por lo que es muy poco probable que estas colisiones suceden “por accidente” en la práctica.

Un hash para una clave utilizando md5 y salt es, a efectos prácticos, tan seguro como si estuviera con sha256 y salt. Sin embargo, es una buena idea usar una función hash más segura como sha256, sha512, RipeMD o WHIRPOOL si es posible.

La manera correcta: Cómo crear un hash correctamente

Lo fundamental: Hash con salt

Se ha descrito anteriormente como los hackers pueden descifrar hash simples de manera rápida usando tablas de búsqueda y tablas arcoíris. También se a dicho que aleatorizar el hash usando salt és una solución a este problema. Pero, ¿cómo generamos el salt y cómo la aplicamos a la clave?

El salt debe ser generado utilizando un “Generador de números criptográficos pseudoaleatorios“ (CSPRNG) que son muy distintos que un generador de números aleatorio común, como la función rand() de “C”

Como su nombre lo indica, están diseñados para ser criptográficamente seguros, lo que significa que proporcionan un alto nivel de aleatoriedad y son completamente impredecibles.

El salt debe ser único para cada clave de usuario. Cada vez que un usuario se registra o cambia su clave, la clave debe ser cambiada utilizando un nuevo salt aleatorio. Nunca se debe reutilizar un salt. El salt debe ser largo, para que haya muchos salt posibles. Como regla general debe ser tan largo como la salida de la función hash que seleccionemos. 

Para almacenar la clave

  • Generar un salt aleatorio grande utilizando CSPRNG
  • Anteponer el salt a la clave y ejecutar la función hash estándar como Argon2, bcrypt, scrypt o PBKDF2
  • Almacenar tanto el salt como el hash en la tabla de la base de datos.

Para validar la clave

  • Recuperar el salt y el hash desde la base de datos
  • Anteponer el salt a la clave proporcionada por el usuario con ejecutar la misma función hash.
  • Compara el hash de la clave proporcionada con el hash de la base de datos. Si coinciden, la clave es correcta, caso contrario, la clave es incorrecta.

En una aplicación web, siempre calcular el hash en el servidor

Es una pregunta frecuente ¿donde hacer el hash?, acaso ¿debería generar el hash en el navegador utilizando javascript? o ¿debería ser enviada al servidor “en claro” y realizar el hash allí?

Incluso si se genera el hash de la clave con JavaScript, aún se debe crear el hash en el servidor. Considera un sitio web que crea el hash de la clave en el navegador sin crear un hash en el servidor. 

Para autenticar un usuario, este sitio web generará un hash en el navegador y lo comprobará con el hash de la base de datos. Esto parece más seguro que simplemente hacer el hash en el servidor, ya que la clave nunca se envían al servidor, pero no es así.

El problema es que el hash del lado del cliente se convierte lógicamente en la clave del usuario. Todo lo que el usuario necesita hacer para autenticarse es decirle al servidor el hash de su clave. Si el atacante tiene el hash de un usuario podría usarlo para autenticarse en el servidor, sin conocer la clave del usuario. 

Entonces, el atacante que de alguna manera robe la base de datos de hashes del sitio web hipotético, tendrá acceso inmediato a las cuentas de todos sin tener que adivinar ninguna clave.

Esto no quiere decir que no se deba hacer hash en el navegador, pero si lo haces, también debes hacerlo en el servidor. Hacer el hash en el navegador es una buena idea, pero considera los siguientes puntos para su implementación;

  • Crear el hash en el lado del cliente no es un substituto de HTTPS (SSL/TLS). Si la conexión entre el navegador y el servidor es insegura, un intermediario puede modificar el código JavaScript a medida que se descarga para eliminar la funcionalidad de hash y obtener la clave del usuario
  • Algunos navegadores web pueden no admitir JavaScript y algunos usuarios pueden deshabilitar JavaScript en su navegador. Entonces, para una máxima compatibilidad, tu aplicación debe detectar si el navegador admite o no JavaScript y emular el hash del lado del cliente si no lo hace.
  • También debes aplicar el salt a los hash en el lado del cliente. La solución obvia es hacer que el script del lado del cliente solicite al servidor el salt del usuario. No hagas eso, por que permite al atacante comprobar si un nombre usuario es válido sin conocer la clave.  Dado que también estás generando el hash con salt en el lado del servidor, está bien usar el nombre de usuario (o correo electrónico) concatenado con una cadena específica del sitio (por ejemplo, nombre de dominio) como salt del lado del cliente.

Haciendo que el descifrado de las claves sea más difícil: funciones de hash lentas

Salt previene que el atacante use tablas de búsqueda y tablas arcoíris para descifrar grandes cantidades de hash rápidamente, pero no les impide ejecutar ataques de diccionario o de fuerza bruta a cada hash.

Las tarjetas gráficas de alta gama con su hardware de procesamiento pueden calcular miles de millones de hash por segundo, por lo que estos ataques siguen siendo muy efectivos. Para lograr que estos ataques sean menos efectivos, podemos usar una técnica conocida como estiramiento de clave

La idea es hacer que la función hash sea muy lenta de modo que incluso con una GPU rápida o hardware especializado, los ataques de diccionario y fuerza bruta sean demasiado lentos para que valga la pena. Sin embargo debe ser lo suficientemente rápida para no causar una mala experiencia al usuario.

El estiramiento de clave se implementa usando un tipo especial de función hash que hace uso intensivo de CPU. Se recomienda usar algoritmos estándar como PBKDF2 o bcrypt

Estos algoritmos toman un factor de seguridad o recuento de iteraciones como argumento. Este valor determina qué tan lenta será la función hash. Para aplicaciones de escritorio o las aplicaciones de teléfonos inteligentes nativas, la mejor manera de elegir este parámetro es ejecutar el código para tener una referencia en el dispositivo y encontrar un valor que haga que el hash tome aproximadamente medio segundo. De esta manera la aplicación puede ser lo más segura posible sin afectar la experiencia del usuario.

Si usas un hash con estiramiento de clave para una aplicación web, ten en cuenta que necesitarás recursos adicionales para procesar grandes volúmenes de solicitudes de autenticación, y que el estiramiento de clave puede facilitar la ejecución de un ataque de denegación de servicio (DoS) en tu sitio web.

Aún así es recomendable utilizar el estiramiento de clave pero con un número de iteraciones más bajo. El cálculo de iteraciones debe realizarse en función de los recursos computacionales y la tasa de solicitud de autenticación máxima esperada para tu sitio web.

La amenaza de denegación de servicio puede eliminarse haciendo que el usuario resuelva un CAPTCHA cada vez que inicie sesión. Diseña siempre tu aplicación para que el número de iteraciones se pueda aumentar o disminuir en el futuro.

Si te preocupa la carga computacional, pero aún quieres utilizar el estiramiento de claves en tu aplicación web, considera ejecutar el algoritmo de estiramiento de claves en el navegador del usuario con JavaScript.

La cantidad de iteraciones debe establecerse lo suficientemente baja como para que la aplicación pueda utilizarse con clientes más lentos, como dispositivos móviles. La aplicación debería recurrir al cálculo del lado del servidor si el navegador del usuario no admite JavaScript. 

El estiramiento de clave del lado del cliente no elimina la necesidad de calcular el hash en el lado del servidor. Debes generar el hash del hash generado por el cliente de la misma manera que lo harías con una clave normal.

Hash imposible de romper: hash con clave y hardware para crear hash

El atacante puede usar un hash para verificar si una clave es correcta o incorrecta, utilizando un diccionario o un ataque de fuerza bruta. El siguiente paso para proteger la clave es agregar una clave secreta al hash para que solo alguien que conozca la clave pueda usar el hash para validar una clave.

Esto se puede lograr de dos maneras. La primera opción es generar el hash utilizando un cifrado como AES y la segunda es que la clave secreta se puede incluir en el hash usando un algoritmo de hash con clave como HMAC.

Esto no es tan fácil como suena. La clave debe mantenerse en secreto del atacante. Si un atacante obtiene acceso completo al sistema, podrá robar la clave sin importar dónde esté almacenada.

La clave debe almacenarse en un sistema externo, como un servidor físicamente separado, dedicado a la validación de claves, o un dispositivo de hardware especial conectado al servidor como YubiHSM

Este enfoque es recomendado para servicios a gran escala (con más de 100.000 usuarios). Es necesario para cualquier servicio que aloje más de 1.000.000 de cuentas de usuario.

Si no te es posible utilizar múltiples servidores dedicados o dispositivos de hardware especiales, aún se puede obtener algunos de los beneficios utilizando un servidor web estándar.

La mayoría de las bases de datos se violan mediante ataques de inyección SQL, que en la mayoría de los casos, no dan acceso a los atacantes al sistema de archivos local. Si generas una clave aleatoria y la almacena en un archivo que no es accesible desde la web y la incluyes en los hashes con salt, entonces los hash no serán vulnerables si la base de datos es violada con un ataque de inyección SQL.

No codifiques una clave en el código fuente, generala al azar cuando se instala la aplicación. Esto no es tan seguro como usar un sistema separado para crear la clave del hash, por que si hay vulnerabilidades de inyección SQL en una aplicación web, probablemente hay otros tipos de vulnerabilidades, como la inclusión de archivos locales, que un atacante podría usar para leer el archivo de clave secreta. Pero, es mejor que nada.

Ten en cuenta que los hash con clave no eliminan la necesidad de salt, los atacantes hábiles, eventualmente encontrarán formas de comprometer las claves, por lo que es importante que los hashes sigan protegidos con salt y estiramiento de clave.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Blue Captcha Image
Refrescar

*