Cómo aplicar correctamente el hashing de contraseñas en aplicaciones Java
24 de marzo de 2022
0 minutos de lecturaHay varias formas de almacenar contraseñas confidenciales. Y aunque contar con opciones puede ser excelente, en el contexto del almacenamiento de contraseñas, elegir mal puede convertirse en una pesadilla de seguridad. Con esto en mente, vamos a analizar algunas de tus opciones. En este artículo hablaremos sobre cómo aplicar hashing a las contraseñas en tus aplicaciones Java. Aunque puedes aplicar estos principios a cualquier ecosistema, mostraremos específicamente la mejor forma de manejar el hashing de contraseñas en Java.
Lo primero que debes considerar al decidir cómo almacenar contraseñas confidenciales es si realmente quieres encargarte de la autenticación y la autorización por tu cuenta. Hay muchos servicios disponibles que pueden hacerlo por ti. Usar un servicio de autenticación y autorización elimina la carga de almacenar contraseñas de forma segura en tu aplicación. Hay muchos servicios comerciales, como Auth0 y Okta, que lo hacen sencillo. Siempre que el proveedor que elijas admita estándares abiertos como OpenID Connect, podrás integrarlo fácilmente en tus aplicaciones Java (por ejemplo, Spring Boot). Además, es posible que tu proveedor también admita MFA (autenticación multifactor) para mejorar aún más la seguridad de tus usuarios finales.
Si sí necesitas almacenar las contraseñas por tu cuenta en tu aplicación Java, este artículo es para ti.
Aplicar hashing a las contraseñas para almacenarlas
Si almacenas nombres de usuario y contraseñas en tu aplicación, hagas lo que hagas, no almacenes contraseñas en texto sin formato (¡lo sé, lo sé, parece obvio!). Si vulneran tu sistema, un atacante podrá leer estas contraseñas en texto sin formato e suplantar la identidad de los usuarios.
Para evitar las temidas contraseñas en texto sin formato, debes almacenar una versión con hashing de las contraseñas de tus usuarios. Para eso, necesitarás un algoritmo de hashing de contraseñas.
¿Qué es un algoritmo de hashing de contraseñas?
Hay distintos tipos de algoritmos de hashing, pero los algoritmos de hashing de contraseñas son funciones unidireccionales diseñadas específicamente para trabajar con contraseñas. Estos algoritmos convierten una contraseña en texto sin formato en una cadena de datos que puedes almacenar de forma segura en una base de datos y que nunca se puede revertir (puedes transformar una contraseña en texto sin formato en un hash, pero es imposible transformar un hash en una contraseña; de ahí el nombre de función «unidireccional»).
¿Cómo funciona el hashing de contraseñas?
Los algoritmos de hashing de contraseñas se usan durante la autenticación de usuarios. Cada vez que un usuario te envía su contraseña (en texto sin formato), esta se convierte en un hash de inmediato. Luego, el hash se almacena (para el registro) o se compara con el hash almacenado del usuario para verificar que la contraseña sea correcta (autenticación).
Hay muchos algoritmos de hashing, pero muy pocos son buenas opciones para las contraseñas. Muchos tipos de funciones de hashing tienen usos específicos, como comprobar la integridad de un archivo. Estos algoritmos de hashing no son una buena opción para las contraseñas.
Lo que necesitas es un algoritmo sólido de hashing de contraseñas para las contraseñas de tus usuarios. Un buen algoritmo de hashing de contraseñas consume una cantidad considerable de potencia de cómputo y memoria. Cuando hablamos de estos algoritmos, ¡cuantos más recursos informáticos utilicen, mejor!
Los algoritmos sólidos de hashing de contraseñas son lentos, lo que reduce la eficacia de los ataques de fuerza bruta. Podríamos profundizar mucho más en este tema, pero el resumen es el siguiente: para las contraseñas, debes usar un algoritmo sólido de hashing que consuma muchos recursos, a fin de ofrecer la mayor seguridad posible a tus usuarios.
¿Cuál es el mejor algoritmo de hashing de contraseñas?
La primera regla de los algoritmos de hashing de contraseñas es: ¡No escribas tu propio algoritmo de hashing de contraseñas! La excepción es si eres un criptógrafo con experiencia. Si no lo eres, apégate a los estándares del sector y elige algoritmos y bibliotecas probados para tu aplicación.
Según la fundación OWASP, solo deberían considerarse unos pocos algoritmos para el hashing moderno de contraseñas. ¡Veamos los algoritmos recomendados para el hashing de contraseñas!
Argon2id
Actualmente (en 2022), el mejor algoritmo es Argon2id. Argon2 es una función de derivación de claves que ganó la Competencia de hashing de contraseñas. Tiene tres versiones diferentes:
Argon2d: maximiza la resistencia a los ataques de descifrado mediante GPU
Argon2i: está optimizado para resistir los ataques de canal lateral
Argon2id: es una versión híbrida
Se recomienda usar la versión Argon2id para las contraseñas por su enfoque equilibrado frente a los ataques de canal lateral y los basados en GPU. Puedes configurar el algoritmo Argon2id mediante tres parámetros: tamaño de memoria (m), iteraciones (t) y grado de paralelismo (p).
Actualmente, se recomienda usar Argon2id con al menos 15 MiB de memoria, 2 iteraciones y un grado de paralelismo de 1. El algoritmo ya incluye salting automático para cada usuario, lo que ayuda a prevenir ataques basados en valores precalculados, así que no tienes que hacer nada especial.
Uso de Spring Security Crypto con Bouncy Castle
La forma más sencilla de implementar Arong2id en tu aplicación Java es usar la biblioteca spring-security-crypto. Aunque forma parte del framework Spring, no tienes que usar el resto del framework.
La biblioteca spring-security-crypto incluye un Argon2PasswordEncoder que puedes usar. En mi opinión, el nombre no es del todo acertado, ya que técnicamente no es un codificador, sino un hasher. La biblioteca Spring usa bouncycastle como dependencia, que contiene una implementación completa del algoritmo Argon2 basada en Java. La biblioteca spring-security-crypto puede considerarse una interfaz intuitiva sobre bouncycastle, que facilita su uso. Además de bouncycastle, también necesitas una dependencia de common-logging para, como quizá ya imaginaste, el registro.
De forma predeterminada, Argon2PasswordEncoder usa la versión Argon2id con m=4MiB, t=3 y p=1. Aunque el número de iteraciones supera el mínimo, probablemente te convenga considerar un mayor consumo de memoria. En el ejemplo de Java que aparece abajo, uso los requisitos mínimos: m=15Mib, t=2 y p=1. También uso los valores predeterminados para la longitud de la sal y del hash.
El resultado de este código será similar a este:
Este hash contiene toda la información necesaria, incluidos los ajustes que proporcioné al codificador y los datos de la sal. Así, puedes comprobar fácilmente si la contraseña original coincide con encoder.matches(). Esto también funciona con otra instancia de Argon2PasswordEncoder que tenga ajustes diferentes.
Uso de Argon2 Binding para JVM
La biblioteca Argon2 Binding para JVM es una alternativa a la biblioteca Spring. La creó Moritz Halbritter y usa JNA para llamar a una biblioteca C nativa. La ventaja de este enfoque es que utiliza la implementación original de los autores de Argon2 y posiblemente ofrece un mejor rendimiento. Como resultado, necesitas tener acceso a la biblioteca C subyacente en tu sistema.
Puedes instalar esta biblioteca C con tu administrador de paquetes o depender de la biblioteca que incluye un conjunto de bibliotecas Argon2 precompiladas. Se recomienda la primera opción por una razón obvia: ocupa menos espacio.
Sin bibliotecas precompiladas:
Con bibliotecas precompiladas:
El uso de esta biblioteca es bastante sencillo. En el ejemplo de código de abajo, puedes ver un ejemplo equivalente al de Spring que utiliza los mismos parámetros mínimos.
scrypt
Si por alguna razón no puedes usar Argon2id, scrypt es una buena segunda opción. Al igual que Argon2id, scrypt es otro algoritmo sólido de hashing de contraseñas que te permite configurar distintos parámetros de costo para aumentar los recursos que consume.
Según la hoja de referencia de contraseñas de OWASP, debes usar un parámetro mínimo de costo de CPU/memoria (N), un tamaño mínimo de bloque (r) y un parámetro de paralelización (p). Las opciones de abajo se consideran los valores mínimos que debes usar.
N=2^16 (64 MiB), r=8 (1024 bytes), p=1
N=2^15 (32 MiB), r=8 (1024 bytes), p=2
N=2^14 (16 MiB), r=8 (1024 bytes), p=4
N=2^13 (8 MiB), r=8 (1024 bytes), p=8
N=2^12 (4 MiB), r=8 (1024 bytes), p=15
Ten en cuenta que la lista anterior de parámetros de scrypt produce hashes de contraseñas igualmente sólidos. Las opciones solo muestran que, al especificar diferentes valores para los parámetros N, r y p, puedes hacer que el algoritmo scrypt use más recursos de CPU o memoria, según tus preferencias. Mi recomendación de abajo muestra un uso práctico que puedes copiar y pegar para simplificar.
La biblioteca spring-security-crypto admite muchos algoritmos, incluido SCrypt. Puedes usar SCryptEncoder de la misma forma que usamos Argon2Encoder. En el ejemplo de abajo, usé la sugerencia intermedia para lograr un buen equilibrio entre la asignación de CPU y memoria.
Resultado:
Uso de BCrypt
Si Argon2id y scrypt no están disponibles, otra opción sólida es BCrypt. Si decides usar BCrypt, el factor de trabajo debe ser como mínimo 10.
Puedes usar la biblioteca spring-security-crypto de forma similar a la anterior. La carga de trabajo predeterminada está configurada en 10, pero en el siguiente ejemplo la establecemos en 14 (un valor razonable en 2022).
Cuanto mayor sea el factor de trabajo, más sólido será el hash, pero también se necesitarán más recursos de CPU (¡y más tiempo!) para completarlo. En mi computadora local, ejecutar BCrypt con una carga de trabajo de 10 tarda en promedio unos 89 ms. Con una carga de trabajo de 14, tarda en promedio 1033 ms. Es casi 12 veces más. De ninguna manera se trata de una prueba de rendimiento precisa, pero sí muestra el impacto de aumentar el factor de trabajo.
Prácticas recomendadas para el hashing de contraseñas
Siempre que necesites implementar hashing de contraseñas en tu aplicación, ten en cuenta algunas prácticas recomendadas.
Nunca implementes algoritmos de hashing de contraseñas por tu cuenta: ¡usa una biblioteca para desarrolladores bien probada! La criptografía es un campo complejo y muchas cosas pueden salir mal si intentas implementar por tu cuenta un algoritmo popular.
Como las computadoras se vuelven más potentes y rápidas cada año, es necesario ajustar los algoritmos de hashing de contraseñas (y sus parámetros). Los algoritmos modernos de hashing de contraseñas dependen de que tú, como desarrollador, especifiques cuántos recursos deben usar para calcular un hash. Con el tiempo, tendrás que actualizar estos parámetros. La única forma de mantenerte al día es seguir las últimas noticias y tendencias de seguridad. Por ejemplo, el blog de Snyk es un excelente recurso para conocer información y tendencias de seguridad a lo largo del tiempo.
Asegúrate de que las bibliotecas y técnicas que usas sean seguras con herramientas gratuitas como Snyk, que te ayudan a identificar problemas de seguridad en tu código de forma proactiva. Herramientas como Snyk pueden detectar problemas de seguridad en tu código en una etapa temprana, además de sugerir soluciones y proporcionarte información adicional. Son fundamentales para mantener una base de código segura a medida que avanza la tecnología.
Empieza con Capture the Flag
Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.



