Lección 4 de 613 min
Contraseñas y secretos
Hashing de contraseñas con Argon2, bcrypt y scrypt, y gestión de secretos con AWS Secrets Manager.
Tarde o temprano, alguna base de datos se filtra: una copia de seguridad mal guardada, una inyección SQL, un empleado descontento. Lo que decide el daño es cómo guardaste las contraseñas. Si están en claro o con un hash rápido, el atacante tendrá en horas las de la mayoría de tus usuarios, que probablemente usan las mismas en otros servicios.
La segunda mitad del problema son tus propios secretos: la contraseña de la base de datos, la clave de la pasarela de pago o la clave con la que firmas los tokens. En esta lección verás cómo guardar contraseñas con Argon2, bcrypt y scrypt, y cómo gestionar secretos sin dejarlos en el código, con AWS Secrets Manager como ejemplo.
Guardar contraseñas
Tu servidor nunca necesita saber la contraseña de un usuario: solo comprobar que la que escribe ahora es la misma que eligió. Eso permite guardar algo de lo que no se puede volver atrás.
Hashing de contraseñas
Guardar una huella irreversible, con sal y deliberadamente lenta de calcular.
- Sentido:
- Solo de ida
- Nunca:
- MD5, SHA-1, SHA-256 a secas
Un hash no es cifrado. Lo cifrado se puede descifrar con la clave; un hash es de un solo sentido. Al registrarse el usuario guardas el hash de su contraseña y, al iniciar sesión, calculas el hash de lo que escribe y comparas. Si roban la base de datos, el atacante no puede «deshacer» los hashes: tiene que probar contraseñas una a una.
La sal es un valor aleatorio, distinto para cada usuario, que se mezcla con la contraseña antes de calcular el hash y se guarda junto a él. Así, dos usuarios con la misma contraseña tienen hashes distintos, y las tablas de hashes precalculados no sirven de nada.
Y la función tiene que ser lenta. MD5, SHA-1 o SHA-256 están pensadas para ser rápidas, y una tarjeta gráfica calcula miles de millones por segundo. Las funciones para contraseñas, como Argon2, bcrypt o scrypt, tienen un coste ajustable: unos milisegundos para tu servidor en cada login, pero una eternidad para quien quiera probar millones de combinaciones.
Úsalo cuando
Siempre que guardes contraseñas tú mismo. Mejor aún si puedes delegar la identidad en un proveedor especializado, como viste en el módulo de autenticación.
Ten en cuenta
Nunca uses MD5 ni SHA-256 a secas, ni siquiera con sal. Y nunca cifres las contraseñas «para poder recuperarlas»: si un usuario la olvida, ofrécele restablecerla.
# Bien: algoritmo, parámetros, sal y hash en una sola cadena
$argon2id$v=19$m=19456,t=2,p=1$c2FsQWxlYXRvcmlh$Kx0R0b7m...
# Mal: en claro
contrasena = "password"
# Mal: MD5 sin sal. Búscalo en Internet y aparece al instante
contrasena = "5f4dcc3b5aa765d61d8327deb882cf99"Argon2
El ganador de la Password Hashing Competition y la primera opción de OWASP.
- Desde:
- 2015
- Estándar:
- RFC 9106
- Variante:
- Argon2id
Argon2 ganó en 2015 la Password Hashing Competition, un concurso abierto para elegir un sucesor moderno de las funciones anteriores. Es memory-hard: cada intento necesita una cantidad de memoria que tú eliges, y eso limita cuántos intentos en paralelo puede hacer una tarjeta gráfica o un chip especializado.
Tiene tres variantes, y la que debes usar es Argon2id, una mezcla que resiste tanto los ataques con hardware como los de canal lateral. Se ajusta con tres parámetros: memoria, número de pasadas y grado de paralelismo. Como punto de partida, OWASP propone 19 MiB de memoria, 2 pasadas y 1 hilo.
Úsalo cuando
En cualquier sistema nuevo. Es la recomendación principal de OWASP para guardar contraseñas.
Ten en cuenta
Mide en tu servidor cuánto tarda y cuánta memoria consume con muchos logins a la vez: unos parámetros demasiado altos convierten el login en una puerta para tumbar tu servicio.
import * as argon2 from "argon2";
// Registro: los parámetros mínimos que propone OWASP
const hash = await argon2.hash(contrasena, {
type: argon2.argon2id,
memoryCost: 19456, // en KiB: unos 19 MiB
timeCost: 2,
parallelism: 1,
});
// Login: la sal y los parámetros van dentro del propio hash
const correcta = await argon2.verify(hash, intento);bcrypt
El clásico de 1999: probado, disponible en todas partes y con un límite de 72 bytes.
- Desde:
- 1999
- Límite:
- 72 bytes
bcrypt lleva décadas protegiendo contraseñas y hay implementaciones en todos los lenguajes. Su único parámetro es el factor de coste, que es logarítmico: cada punto más duplica el tiempo de cálculo. Por eso puedes subirlo a medida que los servidores son más rápidos.
El hash resultante incluye el algoritmo, el coste y la sal ($2b$12$…), así que basta con guardar esa cadena. Su gran limitación es que solo usa los primeros 72 bytes de la contraseña; con caracteres como la «ñ» o los emojis, que ocupan varios bytes, el límite llega antes de lo que parece.
Úsalo cuando
En sistemas que ya lo usan, o donde no tengas Argon2id ni scrypt. OWASP propone un coste de al menos 10; elige el valor más alto que tu servidor soporte sin que el login se vuelva lento.
Ten en cuenta
Muchas implementaciones recortan sin avisar lo que pasa de 72 bytes: dos frases largas que empiezan igual darían el mismo hash. Limita la longitud o, mejor, usa Argon2id. Y bcrypt casi no usa memoria, así que resiste peor las tarjetas gráficas.
import bcrypt from "bcrypt";
const COSTE = 12; // cada punto más duplica el tiempo
const hash = await bcrypt.hash(contrasena, COSTE);
// $2b$12$ + 22 caracteres de sal + 31 de hash
const correcta = await bcrypt.compare(intento, hash);scrypt
Una función memory-hard veterana, incluida de serie en Node.js.
- Desde:
- 2009
- Estándar:
- RFC 7914
- En Node.js:
- crypto.scrypt
scrypt se diseñó en 2009 para que atacar contraseñas con hardware a medida saliera caro: como Argon2, exige mucha memoria por intento. Tiene tres parámetros: N, el coste de CPU y memoria (una potencia de dos), r, el tamaño de bloque, y p, el paralelismo. La memoria necesaria crece con N y r.
Su ventaja práctica es que viene incluido en muchas plataformas: Node.js lo trae en el módulo crypto, sin instalar nada. OWASP propone como mínimo N = 2^17, r = 8 y p = 1, que necesita unos 128 MiB por cálculo.
Úsalo cuando
Cuando no puedes usar Argon2id, por ejemplo porque no puedes instalar módulos nativos, y quieres algo más resistente que bcrypt. OWASP lo sitúa justo detrás de Argon2id.
Ten en cuenta
A diferencia de Argon2 y bcrypt, la función de Node no genera una cadena con todo dentro: tienes que guardar tú la sal y los parámetros, y comparar con timingSafeEqual. Además, Node limita por defecto la memoria que puede usar, así que con esos parámetros tienes que subir maxmem.
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";
const scryptAsync = promisify(scrypt);
// N = 2^17 y r = 8 necesitan unos 128 MiB: hay que subir maxmem
const opciones = { N: 2 ** 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
const sal = randomBytes(16);
const hash = await scryptAsync(contrasena, sal, 64, opciones);
// Guarda la sal, los parámetros y el hash juntos
const intentoHash = await scryptAsync(intento, sal, 64, opciones);
const correcta = timingSafeEqual(hash, intentoHash);Migrar sin pedir a nadie que cambie su contraseña
Como el hash guarda su algoritmo y sus parámetros, puedes migrar poco a poco: cuando un usuario inicia sesión con éxito, tienes su contraseña en memoria, así que calculas el hash nuevo con Argon2id y sustituyes el antiguo.
Gestionar secretos
Un secreto es cualquier dato que da acceso a algo: la contraseña de la base de datos, una clave de API, la clave que firma tus JWT. El reto es que tu aplicación los use sin que acaben en el código, en los logs o en el portátil de cada desarrollador.
Gestión de secretos
Fuera del repositorio, con los mínimos permisos y fáciles de cambiar.
- Nunca en:
- Git, logs ni frontend
- Si se filtra:
- Rotar
La regla de oro es que ningún secreto entra en el repositorio. Git guarda el historial completo, así que un secreto que se subió y luego se borró sigue ahí. En desarrollo se usa un archivo .env que está en .gitignore, y se sube un .env.example con valores falsos que documenta qué variables hacen falta.
En producción, los secretos los inyecta la plataforma o un gestor de secretos. Cada servicio debe tener su propia credencial con los mínimos permisos necesarios, y las credenciales deben rotarse: cambiarse cada cierto tiempo y de inmediato si hay sospecha de fuga. Mejor aún si son temporales, como los roles de IAM, en lugar de claves que duran años.
Como red de seguridad, activa el escaneo de secretos: GitHub y GitLab lo ofrecen y pueden bloquear un push que contenga una clave, y herramientas como gitleaks lo hacen en tu equipo o en el pipeline.
Úsalo cuando
Desde el primer día de cualquier proyecto. Es mucho más fácil empezar con .env y un gestor de secretos que limpiar después un historial lleno de claves.
Ten en cuenta
Si un secreto llega a un commit, da por hecho que está comprometido y rótalo: hay bots que rastrean los repositorios públicos sin descanso. Y recuerda que todo lo que va en el código del frontend es público, aunque venga de una variable de entorno.
# .gitignore
.env
.env.*
!.env.example
# .env.example: sí se sube, con valores de mentira
DATABASE_URL=postgres://usuario:cambia-esto@localhost:5432/tienda
PAGOS_API_KEY=cambia-esto
JWT_SECRET=cambia-estoAWS Secrets Manager
Un almacén gestionado de secretos con cifrado KMS, permisos IAM y rotación automática.
- Cifrado:
- AWS KMS
- Rotación:
- Automática
Secrets Manager guarda cada secreto, un texto o un JSON, cifrado con una clave de AWS KMS. Quién puede leerlo se decide con políticas de IAM, por ejemplo, solo el rol de tu servicio de pedidos, y cada lectura queda registrada en CloudTrail. Tu aplicación lo pide al arrancar con el SDK, en lugar de tenerlo en su configuración.
Su punto fuerte es la rotación automática: una función Lambda cambia la credencial según un calendario, y para bases de datos como RDS y Aurora AWS ofrece rotación gestionada. RDS puede incluso guardar y rotar en Secrets Manager la contraseña maestra de la base de datos por su cuenta.
Úsalo cuando
Para credenciales de bases de datos y claves de API en AWS, sobre todo si quieres rotarlas sin intervención humana o replicarlas en varias regiones.
Ten en cuenta
Se paga por secreto y por llamada a la API, así que no lo consultes en cada petición: guarda el valor en memoria un tiempo. Y prepara tu aplicación para la rotación: si la credencial deja de funcionar, vuelve a leer el secreto antes de fallar.
import {
GetSecretValueCommand,
SecretsManagerClient,
} from "@aws-sdk/client-secrets-manager";
const cliente = new SecretsManagerClient({});
// El rol de este servicio solo tiene permiso para leer este secreto
const { SecretString } = await cliente.send(
new GetSecretValueCommand({ SecretId: "prod/tienda/db" }),
);
const { username, password, host } = JSON.parse(SecretString!);SSM Parameter Store
Configuración jerárquica de AWS, con parámetros cifrados para los secretos sencillos.
- Tipos:
- String, StringList, SecureString
Parameter Store forma parte de AWS Systems Manager y guarda valores con nombres jerárquicos, como /tienda/prod/db-url. Los parámetros pueden ser texto normal o de tipo SecureString, cifrados con KMS, y el acceso también se controla con IAM.
Los parámetros estándar no tienen coste adicional, lo que lo hace ideal para la configuración y para secretos que no necesitan rotarse solos. Lo que no trae es rotación automática ni integración con las bases de datos: para eso está Secrets Manager.
Úsalo cuando
Para la configuración de tus servicios por entorno (URLs, opciones, límites) y algún secreto sencillo que cambias a mano de vez en cuando.
Ten en cuenta
Es fácil acabar mezclando configuración y secretos con los mismos permisos. Usa SecureString para todo lo sensible y da acceso a cada servicio solo a su rama de la jerarquía.
# Guardar un secreto cifrado con KMS
aws ssm put-parameter --name /tienda/prod/pagos-api-key \
--type SecureString --value "valor-secreto"
# Leerlo descifrado (requiere permiso para usar la clave KMS)
aws ssm get-parameter --name /tienda/prod/pagos-api-key --with-decryptionInteractivo
Argon2id, scrypt y bcrypt
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Argon2id | scrypt | bcrypt |
|---|---|---|---|
| Año | 2015 | 2009 | 1999 |
| Exige mucha memoria | Sí | Sí | No |
| Parámetros | Memoria, pasadas y paralelismo | N, r y p | Factor de coste |
| Límite de longitud | Sin límite práctico | Sin límite práctico | 72 bytes |
| Según OWASP | Primera opción | Si no tienes Argon2id | Para sistemas heredados |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Empiezas una aplicación nueva en Node.js que guardará las contraseñas de sus usuarios y puedes instalar cualquier librería.
Para recordar
- Las contraseñas se guardan con un hash lento y con sal, nunca cifradas ni con MD5 o SHA-256 a secas.
- Argon2id es la primera opción; scrypt, la alternativa memory-hard; bcrypt sigue siendo válido, con su límite de 72 bytes.
- Ningún secreto entra en el repositorio:
.envfuera de Git, gestor de secretos en producción y escaneo de secretos como red. - Mínimos permisos y rotación: si un secreto se filtra, se cambia, no basta con borrarlo.
- Secrets Manager destaca por la rotación automática; Parameter Store sirve para configuración y secretos sencillos con menos coste.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Por qué SHA-256 no es adecuado para guardar contraseñas, aunque sea un hash seguro?
2.¿Qué consigue la sal en un hash de contraseña?
3.Subes el factor de coste de bcrypt de 10 a 12. ¿Cuánto más tarda cada hash?
4.Encuentras una clave de API en un commit de hace un mes. ¿Qué es lo primero que debes hacer?
5.¿Qué ventaja da que Argon2id y scrypt sean memory-hard?