Cifrado de contraseñas con Argon21. ¿Qué es el Hasing?2. MD5. No recomendado.3. SHA-512. No recomendado.3.1. ¿Por qué SHA-512?3.2. Implementación en Java3.3. Generando Sal3.4. ¿Por qué no es recomendado?4. Hash de contraseñas Java con Argon24.1. ¿Por qué necesitamos un hash de contraseña lento?5. Hash de contraseña Java Argon2 – argon2-jvm
Fecha | Versión | Descripción |
---|---|---|
16/05/2022 | 1.0.0 | Cifrado de contraseñas seguro con Argon2 |
28/03/2025 | 2.0.0 | Actualización de dependencias |
En este pequeño documento, vamos a ver cuales son los motivos por los que es necesario llevar a cabo un cifrado seguro de las contraseñas que almacenamos en la base de datos.
Hashing es el proceso de generar una cadena, o hash, a partir de un mensaje determinado mediante una función matemática conocida como función hash criptográfica.
Si bien existen varias funciones de hash, las que se adaptan a las contraseñas de hash deben tener cuatro propiedades principales para ser seguras:
Debe ser determinista: el mismo mensaje procesado por la misma función hash siempre debe producir el mismo hash.
No es reversible: no es posible generar un mensaje a partir de su hash.
Tiene una entropía alta: un pequeño cambio en un mensaje debería producir un hash muy diferente.
Y resiste colisiones: dos mensajes diferentes no deberían producir el mismo hash.
Una función hash que tiene las cuatro propiedades es una fuerte candidata para el hash de contraseñas, ya que juntas aumentan drásticamente la dificultad de realizar ingeniería inversa para tratar de obtener la contraseña a partir del hash.
Además, sin embargo, las funciones de hashing de contraseñas deberían ser lentas. Un algoritmo rápido ayudaría a los ataques de fuerza bruta en los que un hacker intentará adivinar una contraseña mediante el hash y la comparación de miles de millones (o trillones) de contraseñas potenciales por segundo.
Algunas excelentes funciones hash que cumplen con todos estos criterios son PBKDF2, BCrypt , SCrypt. y Argon2. Pero primero, echemos un vistazo a algunos algoritmos más antiguos y por qué ya no se recomiendan.
Nuestra primera función hash es el algoritmo MD5 MessageDigest , desarrollado en 1992.
MessageDigest de Java hace que esto sea fácil de calcular y aún puede ser útil en otras circunstancias.
Sin embargo, en los últimos años, se descubrió que MD5 fallaba en la cuarta propiedad de hashing de contraseñas, ya que se volvió computacionalmente fácil generar colisiones. Para colmo, MD5 es un algoritmo rápido y, por lo tanto, inútil contra ataques de fuerza bruta.
Debido a esto, no se recomienda MD5.
A continuación, veremos SHA-512, que forma parte de la familia Secure Hash Algorithm, una familia que comenzó con SHA-0 en 1993.
A medida que aumenta la potencia de las computadoras y encontramos nuevas vulnerabilidades, los investigadores derivan nuevas versiones de SHA. Las versiones más nuevas tienen una longitud progresivamente más larga o, a veces, los investigadores publican una nueva versión del algoritmo subyacente.
SHA-512 representa la clave más larga en la tercera generación del algoritmo.
Si bien ahora hay versiones más seguras de SHA, SHA-512 es la más fuerte que se implementa en Java.
Ahora, echemos un vistazo a la implementación del algoritmo hash SHA-512 en Java.
Primero, tenemos que entender el concepto de sal. En pocas palabras, esta es una secuencia aleatoria que se genera para cada nuevo hash.
Al introducir esta aleatoriedad, aumentamos la entropía del hash y protegemos nuestra base de datos contra listas precompiladas de hash conocidas como tablas arcoíris.
Nuestra nueva función hash se vuelve aproximadamente:
salt <- generate-salt;
hash <- salt + ':' + sha512(salt + password)
Para introducir sal, usaremos la clase SecureRandom de java.security:
xxxxxxxxxx
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
Luego, usaremos la clase MessageDigest para configurar la función hash SHA-512 con nuestra sal:
xxxxxxxxxx
MessageDigest md = MessageDigest.getInstance("SHA-512");
md.update(salt);
Y con eso añadido, ahora podemos usar el método de resumen para generar nuestra contraseña cifrada:
xxxxxxxxxx
byte[] hashedPassword = md.digest(passwordToHash.getBytes(StandardCharsets.UTF_8));
Cuando se emplea con sal, SHA-512 sigue siendo una buena opción, pero existen opciones más potentes y más lentas.
Argon2 fue el ganador de Password Hashing Competition en julio de 2015, una función de hash unidireccional que intencionalmente requiere muchos recursos (CPU, memoria, etc.). En Argon2, podemos configurar la longitud de la sal, la longitud del hash generado, las iteraciones, el costo de la memoria y el costo de la CPU para controlar los recursos que se necesitan para codificar una contraseña.
El algoritmo Argon2 tiene tres variantes:
Argon2d, maximiza la resistencia a los ataques de craqueo de GPU, adecuado para criptomonedas.
Argon2i, optimizado para resistir ataques de canal lateral, adecuado para el hashing de contraseñas.
Argon2id, versión híbrida, si no estás seguro, elige esta.
El algoritmo Argon2 acepta cinco parámetros configurables:
Longitud de sal: la longitud de la sal aleatoria, se recomiendan 16 bytes.
Longitud de la clave: la longitud del hash generado, se recomiendan 16 bytes, pero la mayoría prefiere 32 bytes.
Iteraciones: la cantidad de iteraciones afecta el costo del tiempo.
Memoria: la cantidad de memoria utilizada por el algoritmo (en kibibytes, 1k = 1024 bytes) afecta el costo de la memoria.
Paralelismo: la cantidad de subprocesos (o carriles) utilizados por el algoritmo afecta el grado de paralelismo.
El hardware moderno (CPU y GPU) es cada vez mejor y más barato. La CPU de usuario, como AMD Ryzen Threadripper, aumenta los núcleos cada año, por ejemplo, AMD Ryzen™ Threadripper™ PRO 7995WX tiene 96 núcleos, 192 subprocesos.
Además, gracias al auge de la minería de criptomonedas, la GPU sigue evolucionando, la FPGA o el ASIC dedicado pueden realizar miles de millones de cálculos de hash por segundo.
La computación cuántica aún es demasiado pronto para mencionarla, pero tarde o temprano entraremos en la era de la computación cuántica, y nadie sabe cuánto más rápido pueden funcionar las computadoras cuánticas a la velocidad hash.
Ley de Moore, pronostica un avance rápido en diez años, la velocidad del hash crecerá exponencialmente, es muy posible que podamos decodificar una contraseña MD5, SHA1, SHA-256, SHA-512 o BLAKE2 en un segundo o incluso más rápido (incluso sin necesidad de salar).
En pocas palabras, todos los algoritmos hash rápidos no son adecuados para el hash de contraseñas, y necesitamos algunos algoritmos hash lentos y de uso intensivo de recursos como Bcrypt, Scrypt o Argon2.
Para poder hacer uso de este vamos a necesitar configurar la librería de Maven, por lo tanto tendremos que añadir al pom.xml la siguiente dependencia:
x
<dependency>
<groupId>de.mkammerer</groupId>
<artifactId>argon2-jvm</artifactId>
<version>2.11</version>
</dependency>
El Argon2Factory.create() predeterminado devuelve una variante de argon2i, con 16 bytes de sal y 32 bytes de longitud de hash.
xxxxxxxxxx
Argon2 argon2 = Argon2Factory.create(
Argon2Factory.Argon2Types.ARGON2id,
32,
64);
En este ejemplo se dan las siguientes características:
Variante= argon2i
(default)
Sal= 16 bytes, 128-bit (default)
Longitud del Hash = 32 bytes, 256-bit (default)
Iteraciones= 10
Memoria= 65536k, 64M
Paralelismo= 1
¿Cómo podemos cifrar una contraseña con Argon2?
Lo primero que haremos en la clase donde vayamos a utilizar Argon2 es añadir los siguientes import (antes habremos añadido la dependencia de Maven):
x
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
Una vez tenemos los import realizamos lo siguiente:
x
Argon2 argon2 = null;
char[] password = new char[0];
argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id);
password = "12345".toCharArray();
String hash = argon2.hash(10, 65536, 1, password);
System.out.println(hash);
Esto nos mostrará el hash generado, algo similar a lo siguiente:
x
$argon2id$v=19$m=65536,t=10,p=1$huHPUo6vTZiGGdIk742phg$g+FJ4RBJj84IuNAVeCvZHfF2pK3UmLeS2WxM+XEf43c
Entendemos que el método hash nos genera el hash del password.
A partir de p= podemos ver el hash del password. Si lo ejecutamos en varias ocasiones nos dará un hash diferente.
Este es el que almacenaríamos en la BD:
xxxxxxxxxx
1$huHPUo6vTZiGGdIk742phg$g+FJ4RBJj84IuNAVeCvZHfF2pK3UmLeS2WxM+XEf43c
¿Cómo comparamos si este es correcto?
Utilizaremos el siguiente método que nos ofrece:
x
argon2.verify(hash, passwordAlmacenado)
Esta función es booleana, devuelve true si los hash hacen match y devuelve false si no hacen match.
Dejo un fragmento de código para que se pueda ver:
ximport de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
public class Argon2Prueba {
public static void main(String[] args) {
System.out.println("Ejecutando test");
test();
System.out.println("¡Test finalizado!");
}
private static void test() {
char[] password = new char[0];
password = "12345".toCharArray();
Argon2 argon2 = null;
String hash = null;
try {
argon2 = Argon2Factory.create();
// Hash password
hash = argon2.hash(10, 65536, 1, password);
System.out.println("El hash generado es:");
System.out.println(hash);
if (argon2.verify(hash, password)) {
System.out.println("¡Verificación correcta!");
}
// Verificación errónea, devue
if (!argon2.verify(hash, "1234567".toCharArray())) {
System.out.println("¡Verification erronea!");
}
} finally {
argon2.wipeArray(password);
}
}
}