00:00 / 00:00
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.
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ónpromisify
para poder utilizarla con async/awaitComo 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
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.