Cuando empezamos cualquier proyecto de una plataforma con usuarios, unos de los primeros problemas que nos surgen es la seguridad a la hora de iniciar sesión. Hay que tener cuidado con el formulario de inicio de sesión o login, pero también hay que tener en cuenta el guardar las contraseñas cifradas en la base de datos. Así, si alguien logra acceder a la base de datos, no podrá ser capaz de obtener las contraseñas tal cual las escribe el usuario al entrar en la web.
Para esta clase de tareas, se suelen usar algoritmos de Hash. Son algoritmos que, a partir de unos datos de entrada (una cadena, un texto, un documento o cualquier tipo de dato), calcula una cadena de longitud fija. Es como un resumen. De manera que es relativamente sencillo calcular el valor Hash, pero es casi imposible volver a obtener el dato original a partir de un Hash.
Para calcular la función Hash, se puede utilizar PBKDF2, que es un algoritmo genérico de alto nivel que llama internamente a una función pseudoaleatoria (PRF) para procesar su entrada. La especificación PBKDF2 no exige ningún PRF en particular, por lo que los implementadores son libres de elegir cualquier PRF que deseen (siempre que cumpla con la definición de un PRF seguro y pueda aceptar la entrada que PBKDF2 le da).
Da la casualidad de que, con mucho, la elección más común de PRF para PBKDF2 es HMAC, que es otra construcción de alto nivel que utiliza internamente una función hash criptográfica. Nuevamente, la especificación HMAC no exige ninguna función hash en particular, por lo que los implementadores son libres de elegir cualquier hash que deseen. Probablemente, la opción más común en la actualidad es uno de la familia de hashes SHA-2, que incluye SHA-256 y SHA-512.
Generación del Hash PBKDF2 en Java
Para generar la versión encriptada en PBKDF2 de una contraseña, se puede utilizar el siguiente código.
Como se ve, primero se calculan los dos parámetros que se necesitan para generar el hash:
- iterations: Número de iteraciones hasta lograr el hash. Por defecto, se ha puesto 1000 iteraciones. Sigue siendo rápido y suele ser lo suficientemente seguro.
- Salt: palabra clave para generar el hash. En este código, se genera un código aleatorio para cada contraseña.
Como si cambiamos las iteraciones o el salt, nos devolverá una cadena Hash distinta, lo guardaremos concatenados en el hash. Así sabremos con qué valores se ha codificado el password guardado en base de datos.
public static String generateStrongPasswordHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException
{
int iterations = 1000;
char[] chars = password.toCharArray();
byte[] salt = getSalt();
PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 64 * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] hash = skf.generateSecret(spec).getEncoded();
return iterations + ":" + toHex(salt) + ":" + toHex(hash);
}
private static byte[] getSalt() throws NoSuchAlgorithmException
{
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
byte[] salt = new byte[16];
sr.nextBytes(salt);
return salt;
}
private static String toHex(byte[] array)
{
BigInteger bi = new BigInteger(1, array);
String hex = bi.toString(16);
int paddingLength = (array.length * 2) - hex.length();
if(paddingLength > 0)
{
return String.format("%0" +paddingLength + "d", 0) + hex;
}else{
return hex;
}
}
Comprobación de contraseñas con PBKDF2
Para comprobar si una contraseña es correcta, podemos usar la siguiente función. Simplemente hay que pasarle la contraseña introducida y la contraseña guardada en base de datos.
Simplemente, volverá a codificar la contraseña introducida con las iteraciones y el salt de la contraseña guardada en la base de datos y comparará el resultado.
public static boolean validatePassword(String originalPassword, String storedPassword) throws NoSuchAlgorithmException, InvalidKeySpecException
{
try {
String[] parts = storedPassword.split(":");
int iterations = Integer.parseInt(parts[0]);
byte[] salt = fromHex(parts[1]);
byte[] hash = fromHex(parts[2]);
PBEKeySpec spec = new PBEKeySpec(originalPassword.toCharArray(), salt, iterations, hash.length * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] testHash = skf.generateSecret(spec).getEncoded();
int diff = hash.length ^ testHash.length;
for(int i = 0; i < hash.length && i < testHash.length; i++) {
diff |= hash[i] ^ testHash[i];
}
return diff == 0;
} catch (NumberFormatException e) {
log.error("NumberFormatException validando contraseña");
return false;
}
}
private static byte[] fromHex(String hex) {
byte[] bytes = new byte[hex.length() / 2];
for(int i = 0; i<bytes.length ;i++) {
bytes[i] = (byte)Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
}
return bytes;
}