Encriptación de password en NodeJS y MongoDB: bcrypt

Cada vez que planteamos un nuevo proyecto, hay requisitos que se hacen recurrentes, como la modularidad, la seguridad, la internacionalización … Asociado al requisito de seguridad, así como al de confidencialidad de los datos, va unida la ocultación de información en la base de datos (para que no puedan ser leídas por cualquiera que tenga acceso a la misma).

A veces basta con encriptar / hashear las password de los usuarios, otras hay que encriptar mucha más información. Aunque, por desgracia, en muchos proyectos esto todavía se sigue dejando de lado y se mantienen las password en la base de datos en claro. En las implementaciones de API, guardar un hash de la password debe complementarse con sistemas de autenticación que mantengan la seguridad durante el acceso a todos los recursos, por ejemplo, autenticando los métodos del API con JWT más refresh token.

NodeJS y MongoDB

Como parte de nuestro stack tecnológico, gran parte de nuestros proyectos usan el stack MEAN (MongoDB, Express, AngularJS y NodeJS). Cuando nos planteamos como afrontar el encriptado de la password y guardado en MongoDB, dado que en ese proyecto usábamos Mongoose, uno de las posibilidades que barajamos fue usar el plugin mongoose-encryption.
mongoose-encryption es muy potente, permite opciones como encriptar, desencriptar, firmar y autenticar. Puedes elegir si encriptar toda la información de los documentos de una colección (excepto el _id), o encriptar sólo algunos campos, etc. Al ser un plugin sobre Mongoose, nos permitía abstraernos de esa problemática y la delegábamos completamente en el plugin. Pero decidimos no usarla, principalmente porque no queríamos algo que nos hiciera depender de Mongoose, ya que en otros proyectos no lo usaremos y la solución no nos serviría. Además, no necesitábamos desencriptar las password, nos bastaba con que el software fuera capaz de decirnos si la password introducida es equivalente a la registrada, por lo que el plugin nos estaba aportando mucha más funcionalidad de la que necesitábamos. Y el hecho de que una password pueda desencriptarse hace el sistema un poco más vulnerable. Lo ideal es guardar un hash, para que a partir del campo valor guardado en la base de datos no pueda volver a obtenerse la password en claro.

bcrypt

Bcrypt es una función de hashing de passwords diseñado por Niels Provos y David Maxieres, basado en el cifrado de Blowfish. Se usa por defecto en sistemas OpenBSD y algunas distribuciones Linux y SUSE. Lleva incorporado un valor llamado salt, que es un fragmento aleatorio que se usará para generar el hash asociado a la password, y se guardará junto con ella en la base de datos. Así se evita que dos passwords iguales generen el mismo hash y los problemas que ello conlleva, por ejemplo, ataque por fuerza bruta a todas las passwords del sistema a la vez. Otro ataque relacionado es el de Rainbow table (tabla arcoíris), que son tablas de asociaciones entre textos y su hash asociado, para evitar su cálculo y acelerar la búsqueda de la password. Con el salt, se añade un grado de complejidad que evita que el hash asociado a una password sea único.

NodeJS, MongoDB y bcrypt

Finalmente decidimos utilizar la librería de encriptación: bcrypt. Con esta librería podemos generar el hash de cualquier campo. Nos permite elegir el valor de saltRounds, que nos da el control sobre el coste de procesado de los datos. Cuanto más alto es este número, más tiempo requiere la máquina para calcular el hash asociado a la password. Es importante a la hora de elegir este valor, seleccionar un número lo suficientemente alto como para que alguien que intente encontrar la password para un usuario por fuerza bruta, requiera tanto tiempo para generar todas los hash de las contraseñas posibles que no le compense. Y por otra parte, debe ser lo suficientemente pequeño para no acabar con la paciencia del usuario a la hora de registrarse y de acceder (dicha paciencia no suele ser muy alta). Por defecto, el valor saltRounds es 10.

Veamos un ejemplo práctico como funciona la librería antes de saltar al código. Imaginemos que nuestra contraseña al registrarnos es abc123. Al crear el hash, le hemos pasado a bcrypt un saltRounds numérico y con él ha generado un segmento aleatorio, por ejemplo, FvT4pO8HMbZX3ravxa8pEOVAenB. La librería además le añadirá delante unos parámetros de control, para saber con qué algoritmo está implementado, y para conocer la complejidad salt usado, por ejemplo $2a$10$. Usando el salt encriptará la password (abc123) y obtendrá como resultado otra cadena, por ejemplo oXAUXEckEmHaHSuB8oNlvsLzR. Con un carácter separador unirá la primera parte generada con la password encriptada, quedando algo así: $2a$10$FvT4pO8HMbZX3ravxa8pEOVAenB/oXAUXEckEmHaHSuB8oNlvsLzR

Registro

Vamos a ver un ejemplo de implementación.
En el proceso de registro del usuario, tenemos que aplicar el hasheado de la contraseña antes de almacenarla en la base de datos. Para ello, con la librería bcrypt calculamos el hash asociado a la password. La librería usará el valor que le pasemos como saltRounds para aplicar el coste de procesado para generar el hash.

Al recibir la petición de registro, pasamos la password y el valor satlRounds a la librería para que la cifre, y con lo que nos devuelve, creamos el usuario en la base de datos.

Autenticación

Cuando un usuario hace log-in en nuestro sistema, necesitamos comprobar que la password introducida es correcta. Al contrario que en otros sistemas en los que se desencriptaría la password en la base de datos (en caso de estar encriptada), y compararla con la introducida por el usuario, lo que hacemos con bcrypt es cifrar la introducida por el usuario. Para ello le pasaremos a bcrypt la contraseña a calcular el hash, pero también la almacenada en base de datos asociada al usuario (hasheada). Esto se debe, a que como hemos comentado antes, el algoritmo bcrypt utilizó un segmento aleatorio (salt) para generar el hash asociado a la pasword. Este quedó almacenado junto con la password, y lo necesita para calcular de nuevo el hash de la contraseña introducida por el usuario y finalmente comparar con la introducida al registrarse y ver si coinciden.

La respuesta de la llamada a la librería será un boolean que nos indica si la comparación es correcta o no, y según este valor daremos al usuario por autenticado o no.

 

Cuando recibimos una petición de login, primero buscamos el usuario en la base de datos por el username, y delegamos la comparación de password a la librería bcrypt, la cual nos dirá si son la misma o no, y según el resultado que nos devuelva procederemos a devolver un error de autenticación o a dejarle pasar.

La función bcrypt, como algoritmo de hasheado, tiene ventajas importantes sobre otros algoritmos de cifrado, como SHA-256 con salt. El coste en tiempo de cálculo del algoritmo bcrypt es mayor, por lo que las password generadas son mucho más seguras frente a ataques de fuerza bruta, ya que los atacantes necesitarían mucho más tiempo para probar cada una de las claves posibles.