Cuando desarrollamos un sistema de usuarios, siempre queremos tratar de implementar la mayor seguridad posible para que el mecanismo de autenticación sea impenetrable. Uno de esos puntos consiste en cifrar las contraseñas de los usuarios, de este modo, si un atacante se hace con una contraseña, no tendrá modo de descifrarla y saber cual es realmente la contraseña.

Antiguamente era muy común ver que estos sistemas se implementaban utilizando MD5 o SHA-1. Si bien estos algoritmos ya están deprecados, hoy se continúan utilizando versiones más nuevas como SHA-256 o SHA-512. El problema de todos estos algoritmos es que no se concibieron pensando en la encriptación de contraseñas, por lo que no deben ser utilizados para este propósito, y todo tiene que ver con la velocidad de cifrado.

Cifrado rápido y cifrado lento

Si bien es cierto que cifrados como SHA son irreversibles, el problema es que el tiempo cifrado es demasiado corto, lo que significa que un atacante con un hash en sus manos (y una buena GPU) puede realizar un ataque de fuerza bruta de cientos de millones de combinaciones por hora. Esto aumenta significativamente las posibilidades de que el atacante dé con la contraseña correcta (al menos en esta vida).

Es más, SHA-512 por ejemplo, fue diseñado específicamente para ser un algoritmo rápido, lo cual ya nos dice que NO deberíamos utilizarlo para cifrar contraseñas.

Si bien SHA-512 nos entrega un nivel de seguridad, la solución óptima para este caso es utilizar algoritmos de cifrado lento, es decir, que la generación de un hash tome un tiempo considerable:

  • Lo suficiente para que el ataque de fuerza bruta se vuelva ineficaz
  • Y no tan lento como para ralentizar un login de usuario

Los algoritmos de cifrado lento se concibieron con este propósito y son los que debiésemos utilizar para cifrar las contraseñas de nuestros usuarios. Estos se conocen como PBKDF (Password-based key derivation function). Un ejemplo de PBKDF es bcrypt.

Bcrypt

Bcrypt es una función de cifrado lento. El cifrado que aplica sobre una contraseña es tan pesado que por cada hash generado, SHA-512 puede generar miles de hashes en el mismo periodo de tiempo.

Bcrypt está disponible como librería para los lenguajes más populares, acá vamos a ver un ejemplo de como implementarla en Java y Node.

Antes de ver los ejemplos, mencionar que Bcrypt tiene una característica que permite “saltear” el hash mediante iteraciones en las que se le agrega aun más complejidad. En los ejemplos definimos 10 iteraciones, pero subir esta número puede afectar el rendimiento por lo que hay que manejar este parámetro con cuidado. Si quieres saber más detalles del algoritmo puedes leer este artículo que lo lleva más detallado.

Implementar BCrypt en Node

Vamos a iniciar un proyecto nuevo:

mkdir bcrypt-test && cd bcrypt-test
npm init
npm install --save bcrypt

Luego creamos el archivo “bcrypt-test.js”:

const bcrypt = require('bcrypt');

// encriptamos la cadena "chaomundo"
const plaintext = "chaomundo";
const salt = 10;
const hash = bcrypt.hashSync(plaintext, salt);

console.log(plaintext);
console.log(hash);

// validamos cual de las dos coincide con el cifrado anterior
const compA = bcrypt.compareSync("holamundo", hash); // true
const compB = bcrypt.compareSync("chaomundo", hash); // false

console.log(compA);
console.log(compB);

Y si ejecutamos, veremos el output:

$ node btest.js 
chaomundo
$2b$10$QN9YSSTlVbkBNRQL/OAvKuzgpKojsp4xMnOq1gyrMhClL0x7wyAum
false
true

Con la función hashSync() transformamos en input a hash. Y con la función compareSync() corroboramos que un input, hasheado, corresponda al hash que generamos con hashSync().

¡OJO! Por simpleza se usó hashSync() y compareSync() para este ejemplo, pero ambas tienen versiones asíncronas que son hash() y compare(), las cuales son más recomendables ya que al ser asíncronas no dejarán bloqueado el hilo principal de Node, recordemos que Bcrypt es un hasheo pesado.

La única diferencia es que al retornar una promesa, debemos adjuntar un callback o bien, invocarlas utilizando await:

await bcrypt.compare("chaomundo", hash);
Respecto a la implementación en Javascript solo mencionar que existen dos versiones: bcrypt y bcryptjs. La primera es una implementación de bcrypt en C++ con un wrapper Javascript. La segunda está implementada 100% en Javascript. Si bien los resultados son los mismos, bcrypt por ser C++ debiese ser más rápida, pero al mismo tiempo se limita la cantidad de plataformas en las que puede correr. A efectos prácticos no debiese ser un problema si usamos servidores x64 por ejemplo.

Implementar Bcrypt en Java

En Java utilizaremos jbcrypt, que es básicamente la implementación de bcrypt para Java. Si usas maven, solo agregar:

<dependency>
    <groupId>org.mindrot</groupId>
    <artifactId>jbcrypt</artifactId>
    <version>0.4</version>
</dependency>

Al igual que en la implementación de Node, tenemos dos funciones. Una para encriptar:

BCrypt.hashpw(password, BCrypt.gensalt(10));

Y otra para comprobar que una cadena dada hace match con el hash:

BCrypt.checkpw(password, hashedPassword)

Conclusiones

A diferencia de otros topics de desarrollo que vemos en nuestro día a día, en el mundo de la seguridad es mucho más importante estar al día e investigar por nuestra cuenta cuales son los estándares recomendables a la fecha. Guiarse por ellos y estar en constante revisión puede ayudarnos a evitar desastres y por supuesto le entregará más valor a nuestro trabajo 🙂