Guardar contraseñas usando «Salted Password Hashing» y otras formas correctas

Publicado por pico.dev el , actualizado el .
java programacion seguridad tapestry blog-stack planeta-linux planeta-codigo
Comentarios

Apache Shiro

Para cada servicio deberíamos emplear una contraseña de una longitud de al menos 8 caracteres que incluya letras en minúscula, mayúscula, números y símbolos, una herramienta que podemos utilizar para generar contraseñas más seguras con los criterios que indiquemos es Strong Password Generator. Sin embargo, recordar cada una de estas contraseñas es muy difícil de modo que es habitual que utilicemos la misma contraseña para varios o todos los servicios y no empleando todos los criterios anteriores. Por otro lado, los desarrolladores no deberíamos guardar en la base de datos las contraseñas que nos entregan los usuarios en texto plano, para evitar guardalas en texto plano hace un tiempo se utilizaba únicamente una función de hashing unidireccional como MD5 o SHA, de este modo si la base de datos fuese comprometida en teoría no podrían conocer la contraseña original. En este artículo comentaré que aún guardando las contraseñas con una función de hashing no es suficiente para hacerlas seguras y comentaré una implementación con Apache Shiro de una de las ideas propuestas.

Algo de teoría y algunas explicaciones

Java

Aunque guardemos las contraseñas con MD5 o alguna variante de SHA hoy en día no es suficiente para que en caso de que alguien obtenga los hashes de las contraseñas de la base de datos pueda averiguarlas o dar con una que genere el mismo hash, usando estas funciones se pueden encontrar colisiones en un tiempo razonable y por tanto ya no se consideran seguras. Dada la computación actual de los procesadores y las tarjetas gráficas una contraseña débil puede romperse usando un ataque de fuerza bruta y quizá antes con un ataque de diccionario que pruebe las más comunes. Muchos usuarios no tienen contraseñas largas ni utilizan letras en minúscula, mayúscula, números y símbolos, muchos usuarios utilizan contraseñas sencillas para ser recordadas más fácilmente, y aún hasheando las contraseñas pueden ser averiguadas. También se pueden usar tablas arcoiris o rainbow tables con los hashes precalculados de las contraseñas de un diccionario con lo que el tiempo empleado para romper una puede requerir poco tiempo de computación.

También hay que tener en cuenta que muchos usuarios usan la misma contraseña para múltiples servicios por lo que basta que alguien obtenga la contraseña original de un servicio y podrá acceder a otros más interesantes para alguien con malas intenciones por mucha seguridad que tenga esos otros servicios, este es uno de los motivos de la autenticación en dos pasos (que emplea algo que sé, la contraseña, y algo que tengo, como el móvil) y la recomendación de usar una contraseña diferente para cada servicio. Las contraseñas por si solas tiene la seguridad más baja de los diferentes servicios donde se usen.

Con Salted Password Hashing el uso de rainbow tables que aceleren el ataque no serían posibles por la entropía añadida por los salt. Aún así conociendo el salt y la función de hash empleada seguiría siendo posible un ataque de fuerza bruta y de diccionario. Con Salted Password Hashing se usa en la función de hash y un dato variable denominado salt que añade suficiente entropía y es diferente para cada contraseña, en la base de datos se guarda el resultado de la función de hash junto con el salt, esto es, el resultado de SHA-512(contraseña+salt) y también el salt.

Ejemplo de Salted Password Hashing usando Apache Shiro

Antes de comentar alguna opción más que dificulte los ataques de fuerza bruta o de diccionario veamos como implementar Salted Password Hashing empleando Apache Shiro como librería de autenticación y autorización para los usuarios. El ejemplo será simple sin guardar los datos en una base de datos pero suficiente para mostrar que se debe añadir al proyecto para que Shiro compruebe las contraseñas usando una función de hash y un salt. Partiré de un ejemplo que hice para el libro PlugIn Tapestry sobre el desarrollo de aplicaciones web con el framework Apache Tapestry. Básicamente deberemos crear un nuevo Realm que devuelva los datos del usuario, el hash y el salt. Una implementación suficiente para el ejemplo sería la siguiente, la parte importante está en el método doGetAuthenticationInfo y en la inicialización static de la clase:

Las contraseñas hasheadas tendrán la siguiente forma, podemos guardarlas codificadas en formato hexadecimal o en formato Base64:

En el ejemplo tratándose de una aplicación web usando Apache Tapestry se debe modificar la configuración para que se utilice el nuevo Realm el antiguo guardaba las contraseñas en texto plano (shiro-users.properties).

El cambio de Realm para el usuario no supone ninguna modificación y podrá seguir autenticandose con su misma contraseña. En el ejemplo con root como usuario y password como contraseña.

Este es todo el código que necesitamos para la implementación de contraseñas codificadas con una función de hashing, en este caso SHA-512, y un salt, no es mucho y además es bastante simple la implementación con Shiro y en este caso en una aplicación usando el framework Apache Tapestry. Estas pocas líneas de código pueden aumentar notablemente la seguridad de las contraseñas que guardamos en la base de datos. En el caso de que la base de datos se vea comprometida será más difícil para alguien con malas intenciones obtener las contraseñas originales.

El siguiente ejemplo de federatedaccounts puede verse como usar está técnica de hash con salt usando una base de datos. Básicamente es lo mismo pero accediendo a base de datos para obtener el hash de la contraseña y el salt con una entidad JPA.

Otras opciones que añaden más seguridad

Aún así como comento este ejemplo de Salted Password Hashing aunque dificulta un ataque aún es viable usar fuerza bruta o un diccionario. En el artículo Password Security Right Way comentan tres ideas más. Una es usar como función de hash Bcrypt no porque sea más segura que SHA-512 sino porque es más lenta y esto puede hacer inviable la fuerza bruta o de diccionario, hay planes de proporcionar Bcrypt en Apache Shiro en futuras versiones. En el ejemplo como alternativa a bcrypt se usan varios millones de iteraciones de aplicación de la función para añadir tiempo de cálculo al hash, este tiempo adicional no es significativo en el cálculo de un hash pero en un ataque de fuerza bruta puede aumentarlo de forma tan significativa que sea inviable. La segunda idea interesante es además de hashear la clave es cifrarla de modo que aún habiendo sido comprometida la base de datos se necesite la clave privada de cifrado que también debería ser comprometida para producir el ataque. La tercera es partir el hash y distribuirlo entre varios sistemas de modo que sea necesario romperlos todos para obtener en hash original, lo que dificulta aún más un ataque.

Para implementar la segunda opción deberemos proporcionar implementaciones propias de CredentialsMatcher y de SimpleHash, quizá esto sea tema para otro artículo.

Código fuente del ejemplo

El código fuente completo del ejemplo está alojado en un repositorio de GitHub, es completamente funcional y puedes probarlo en tu equipo. Una vez descargado el siguiente comando e introduciendo en el navegador http://localhost:8080/PlugInTapestry, en la página que se muestra hay un botón para iniciar sesión:

Nota final

En este artículo recomiendo leer los interesantes enlaces del apartado de referencia del final, de ellos los siguientes dos son bastante completos Password Security the Right Way y The RIGHT Way: How to Hash Properly aunque todos merecen el tiempo dedicado a una lectura detenida. Para terminar mucho de esto es fútil si se permiten contraseñas sencillas por lo que exigir contraseñas con cierta fortaleza de la forma comentada al principio también es necesario si la seguridad de la aplicación es un requisito importante.