00:00 / 00:00

Logo curso MongoDB: almacenando claves de forma segura

MongoDB: almacenando claves de forma segura

Fecha publicación: 26 jul 2021

Guardando credenciales

Es muy común que en una aplicación necesitemos que el usuario se identifique introduciendo su id de usuario y clave, en la parte de Front End nos basta con añadir un diálogo de login y enviar dichas credenciales de forma segura al servidor.

¿Y en la parte servidora?, ¿dónde almacenamos esa clave?, ¿cómo comprobamos que es correcta? Lo primero que se nos puede venir a la cabeza es almacenar la clave tal cual en un campo de la base de datos, ¿qué problemas nos podemos encontrar siguiendo esta aproximación?

Si guardo la claves cómo texto en claro, en caso de que me roben la base de datos, estoy exponiendo usuarios y claves a un tercero. Por otro lado, aunque sea mala práctica, hay usuarios que utilizan esa clave para otros sitios, no es buena idea tenerla expuesta en la base de datos. Y para finalizar si quieremos cumplir con normativas de seguridad, no podemos almacenar esta clave como texto en claro, podemos enfrentarnos a una cuantiosa multa.

¿Qué podemos hacer? Almacenar un hash de la clave en base de datos.

En este vídeo vamos a ver cómo podemos guardar las contraseñas de los usuarios de manera que el atacante lo tenga complicado para recuperar el valor.

Manos a la obra

Vamos a partir de este ejemplo donde insertamos un usuario en MongoDB utilizando NodeJS. Para instalar MongoDB hemos utilizado Docker, si tienes dudas de como hacerlo, puedes echar un vistazo a este curso Docker + MongoDB.

El cuál tiene un fichero ./docker-compose.yml donde tenemos la configuración para poder arrancar un contenedor de Docker utilizando la imagen oficial de MongoDB:

./docker-compose.yml

version: "3.8"
services:
  my-mongo-db:
    container_name: my-mongo-db
    image: mongo:4.4.6
    ports:
      - "27017:27017"

Ejecutamos el fichero con el comando docker-compose para levantar el contenedor de Docker con MongoDB:

docker-compose up

Por otro lado, está el fichero principal ./src/index.js donde utilizamos la libreria mongodb para conectarnos a una base de datos en local e insertar dos usuarios:

./src/index.js

import { MongoClient } from 'mongodb';

// TODO: Move to env variable
const connectionURI = 'mongodb://localhost:27017/my-database';

(async function () {
  const client = new MongoClient(connectionURI);
  await client.connect();
  const db = client.db();
  console.log('Conectado a la base de datos');
  
  const users = [
    {
      name: 'John Doe',
      password: 'my-password',
      email: 'john.doe@email.com',
    },
    {
      name: 'Jane Doe',
      password: 'my-password',
      email: 'jane.doe@email.com',
    },
  ];

  await db.collection('users').insertMany(users);
  console.log(`Se han insertado ${users.length} usuarios`);
})();

En el fichero package.json, a parte de apuntar la libreria mongodb como dependencia, hemos creado un comando de npm, el comando start para que al ejecutarlo, arranque NodeJS utilizando nuestro fichero principal src/index.js:

{
  "name": "password-hash-playground",
  "version": "1.0.0",
  "description": "Demo guardando passwords en MongoDB y NodeJS",
  "type": "module",
  "scripts": {
    "start": "node src/index.js"
  },
  "author": "Lemoncode",
  "license": "MIT",
  "dependencies": {
    "mongodb": "^4.0.1"
  }
}

Vamos a ejecutarlo, para ello, en otro terminal instalamos todas las dependencias necesarias:

npm install

Y ejecutamos el comando start para arrancar nuestra aplicación:

npm start

Podemos ver los datos por ejemplo utilizando Mongo Compass, la herramienta oficial de MongoDB.

Viendo usuarios insertados en Mongo Compass

Como vemos, estamos guardando las contraseña en texto plano

Una pregunta que nos hacemos cuando trabajamos con passwords de usuarios: ¿y es seguro guardarlas en base de datos tal cuál?. Si lo hacemos así, veremos que podemos tener varios problemas:

  • Cualquier persona con acceso a la base de datos, podría verlas.

  • Confío en mi equipo o solamente yo tengo acceso, vale pero ¿y si la base de datos está comprometida por algún ataque? Podrían tener acceso a todos los datos de nuestros clientes.

  • Otro punto es que un usuario igual usa la misma para clave no sólo para mi sitio web.

Cuando hablamos de seguridad, nunca podemos garantizar que algo esté cubierto al 100%, pero si se lo podemos poner complicado a un hacker.

En el caso de las passwords, la estrategia a seguir es el hashing, que es un mecanismo para ocultar el verdadero valor de ésta:

"my-password" -> hash("my-password") -> 2cf241ab6

De esta forma, aunque el atacante tenga acceso al valor del password hasheado (2cf241ab6) no hay algoritmo inverso para recuperar el valor original ("my-password").

¿Con ésto ya lo tengo todo para guardarlo de manera segura? Por desgracia no, si solamente aplicamos la función hash, un atacante puede ir probando valores aleatorios hasta dar con la contraseña que coincida con el hash, lo que se conoce como ataque de fuerza bruta. Estos ataques, pueden ser muy costosos, incluso podrían llegar a magnitudes de años, pero si el atacante utiliza Rainbow Tables (donde hay guardadas información de claves más comunes y valores de hash asociados) pueden ayudarle a reducir muchísimo dicho tiempo, incluso a minutos, dependiendo, de la potencia de la máquina utilizada.

Para evitar ésto, se utiliza la salt, un código único y aleatorio por cada usuario que vamos utilizar para hashear nuestra contraseña:

"my-password" -> hash("my-password" + salt) -> 2189a685d5

Para contraseñas iguales, vamos a generar diferentes valores de hash:

"my-password" -> hash("my-password" + "o198d81") -> 2fj1a685d5

"my-password" -> hash("my-password" + "93jfd87") -> 82hjajd8d3

A todo esto, si aplicamos esta función hash una y otra vez (X iteraciones) sobre el resultado hasheado, podemos ponerlo más complicado a un hacker que tirara de Rainbow Tables:

resultado = hash(hash(hash(...hash(password + salt)... + salt) + salt) salt)

Vamos a actualizar nuestro código usando la libreria crypto de NodeJS para generar una salt:

./src/index.js

import { MongoClient } from 'mongodb';
+ import crypto from 'crypto';
+ import { promisify } from 'util';
+ const randomBytes = promisify(crypto.randomBytes);

// TODO: Move to env variable
const connectionURI = 'mongodb://localhost:27017/my-database';

+ const saltLength = 16; // 16 bytes -> 128 bits
+ const generateSalt = async () => {
+   const salt = await randomBytes(saltLength);
+   return salt.toString('hex');
+ };

(async function () {
  const client = new MongoClient(connectionURI);
  await client.connect();
  const db = client.db();
  console.log('Conectado a la base de datos');

+ const salt1 = await generateSalt();
+ const salt2 = await generateSalt();

  const users = [
    {
      name: 'John Doe',
      password: 'my-password',
      email: 'john.doe@email.com',
    },
    {
      name: 'Jane Doe',
      password: 'my-password',
      email: 'jane.doe@email.com',
    },
  ];

  await db.collection('users').insertMany(users);
  console.log(`Se han insertado ${users.length} usuarios`);
})();

Utilizamos la función randomBytes para generar una salt única y aleatoria y la función promisify para poder utilizarla con async/await

Como mínimo se recomienda una longitud de 64 bits (8 bytes). Nosotros vamos a usar el doble.

Ahora, vamos a crear nuestra función hash. En este caso vamos a usar una de las funciones que nos provee NodeJS PBKDF2 (Password-Based Key Derivation Function 2) y un algoritmo de hash SHA512. Además vamos a aplicar la función durante 100.000 iteraciones, aqui tenemos que encontrar un equilibrio, por un lado cuantas más iteraciones realizamos más tardaría un atacante en reventarnos el sistema, pero por otro lado más CPU consumimos y más tardamos en generar nuestra password hasheada.

./src/index.js

import { MongoClient } from 'mongodb';
import crypto from 'crypto';
import { promisify } from 'util';
const randomBytes = promisify(crypto.randomBytes);
+ const pbkdf2 = promisify(crypto.pbkdf2);

...

+ const iterations = 100000;
+ const hashedPasswordLength = 64; // 64 bytes -> 512 bits like digestAlgorithm
+ const digestAlgorithm = 'sha512';
+ const hash = async (password, salt) => {
+   const hashedPassword = await pbkdf2(
+     password,
+     salt,
+     iterations,
+     hashedPasswordLength,
+     digestAlgorithm
+   );
+   return hashedPassword.toString('hex');
+ };

(async function () {
  const client = new MongoClient(connectionURI);
  await client.connect();
  const db = client.db();
  console.log('Conectado a la base de datos');

  const salt1 = await generateSalt();
+ const password1 = await hash('my-password', salt1);

  const salt2 = await generateSalt();
+ const password2 = await hash('my-password', salt2);

  const users = [
    {
      name: 'John Doe',
-     password: 'my-password',
+     password: password1,
      email: 'john.doe@email.com',
    },
    {
      name: 'Jane Doe',
-     password: 'my-password',
+     password: password2,
      email: 'jane.doe@email.com',
    },
  ];

  await db.collection('users').insertMany(users);
  console.log(`Se han insertado ${users.length} usuarios`);
})();

Vamos a borrar la base de datos actual, para probar el nuevo código:

docker-compose down

Con este comando paramos el contenedor y se borrarían todos los datos.

Y volvemos a arrancarlo todo:

docker-compose up

Y en otro terminal:

npm start

Guardando el hash de las passwords

Ahora vemos que hemos guardado en base de datos, 2 valores totalmente diferentes en el campo clave (hash) para cada usuario ¡aún cuándo la clave de ambos usuarios era la misma!

¿Con ganas de aprender Backend?

En Lemoncode impartimos un Bootcamp Backend Online, centrado en stack node y stack .net, en él encontrarás todos los recursos necesarios: clases de los mejores profesionales del sector, tutorías en cuanto las necesites y ejercicios para desarrollar lo aprendido en los distintos módulos. Si quieres saber más puedes pinchar aquí para más información sobre este Bootcamp Backend.