Por qué las contraseñas requieren un manejo especial
Cualquier otro secreto de tu sistema se puede rotar: API keys, tokens, certificados. Las contraseñas son diferentes. Nunca las posees en un formato que puedas regenerar, los usuarios las reutilizan en distintos sitios y, en el momento en que tu base de datos se filtra, el atacante tiene todo el tiempo del mundo para intentar adivinarlas offline.
El hashing de contraseñas es la práctica de almacenar una transformación unidireccional de la contraseña en lugar de la contraseña misma. Cuando un usuario inicia sesión, aplicas la misma transformación y comparas los resultados. Tú nunca conoces la contraseña, y tampoco lo hace quien robe la tabla.
Este no es lugar para intentar ser ingenioso. Las reglas son sencillas y están bien establecidas, y romperlas es lo que hace que las brechas de seguridad se vuelvan catastróficas.
El hashing no es cifrado
El cifrado es reversible. Si cifras las contraseñas, la clave reside en algún lugar de tu sistema, y cualquier persona que la obtenga —ya sea mediante una filtración, un backup, una variable de entorno mal configurada o alguien interno— podrá leer todas las contraseñas a la vez. No existe ningún escenario en el que necesites la contraseña original, por lo que la reversibilidad es un riesgo puro.
Una función de hash toma una entrada y produce un digest de longitud fija, y no hay una forma práctica de volver atrás. Para las contraseñas, un hash simple sigue sin ser suficiente: las funciones rápidas como SHA-256 están diseñadas para ser veloces, que es exactamente lo que busca un atacante. Una GPU puede probar miles de millones de candidatos por segundo.
Necesitas una función que sea deliberadamente lenta y memory-hard, para que intentar adivinar las contraseñas sea costoso a gran escala.
Salt: la misma contraseña, un hash diferente
Sin un salt, dos usuarios con la contraseña hunter2 producen el mismo digest. Un atacante puede precomputar una tabla gigante de contraseñas comunes y sus hashes —una rainbow table— y buscar coincidencias al instante. También pueden ver de un vistazo qué cuentas comparten la misma contraseña.
Un salt es un valor aleatorio único generado por cada contraseña y almacenado junto al digest. No es secreto; su única función es hacer que cada hash sea único. Ahora, el atacante debe atacar cada cuenta por separado y la precomputación resulta inútil.
Las librerías modernas generan el salt por ti y lo codifican, junto con los parámetros, en una sola cadena de texto. Tú simplemente almacenas esa cadena tal cual.
Cómo elegir un algoritmo
Hay tres funciones que vale la pena conocer. Todas son adaptativas: el coste puede aumentarse a medida que el hardware mejora.
Argon2id
El ganador de la Password Hashing Competition de 2015 y la recomendación actual. Es memory-hard, lo que significa que un atacante debe invertir tanto en memoria como en cómputo, mitigando la ventaja de las GPUs y los ASICs. Argon2id equilibra la resistencia a los ataques de canal lateral y de GPU, siendo la opción predeterminada ideal para sistemas nuevos.
scrypt
Estandarizado en el RFC 7914 y también memory-hard, scrypt es una opción sólida cuando Argon2 no está disponible. Permite configurar el coste, el tamaño del bloque y el paralelismo para ajustarlo, aunque sus parámetros son más fáciles de configurar incorrectamente que los de Argon2.
bcrypt
Lanzado en 1999 y todavía presente en todas partes, bcrypt es bien conocido y ha sido probado en batalla. Su factor de coste duplica el trabajo con cada incremento. La principal advertencia es que trunca la entrada a 72 bytes, por lo que las frases de contraseña muy largas se cortan silenciosamente; realiza un pre-hash si esto es importante para ti.
Independientemente de lo que elijas, nunca utilices MD5, SHA-1 o SHA-256 simple. No son la herramienta adecuada, y apilar rondas sobre ellos es un esquema casero en el que nadie debería confiar.
Ajustando el factor de trabajo
El costo debe equilibrar dos presiones. Si es demasiado bajo, el hash es vulnerable a ataques de fuerza bruta; si es demasiado alto, una ráfaga de intentos de inicio de sesión puede convertirse en un vector de denegación de servicio contra tu propia CPU.
Ajústalo para que un solo hash tarde aproximadamente entre 100 y 500 milisegundos en tu hardware de producción. Mide el endpoint de login bajo una carga realista, no solo mediante un bucle de benchmark, y define el número basándote en los datos. Luego, revísalo periódicamente: los mismos parámetros se vuelven más débiles cada año a medida que el hardware mejora.
// Argon2id parameters (memory in KiB, iterations, parallelism)
await hash(password, { memoryCost: 19456, timeCost: 2, parallelism: 1 });
Almacenamiento y verificación
El registro y el inicio de sesión son los únicos dos lugares donde manipulas una contraseña.
import { hash, verify } from "@node-rs/argon2";
// Registration: hash before the password ever reaches the database.
const digest = await hash(password);
await db.users.insert({ email, passwordHash: digest });
// Login: verify against the stored digest.
const user = await db.users.findByEmail(email);
const ok = user && (await verify(user.passwordHash, password));
if (!ok) return res.status(401).json({ error: "invalid_credentials" });
Devuelve el mismo error independientemente de si el correo electrónico o la contraseña eran incorrectos. Un mensaje como “el usuario no existe” le da a un atacante una forma de enumerar cuentas.
Ataques de tiempo y comparación segura
Comparar dos hashes con === se detiene en el primer byte que sea diferente. Por lo tanto, el tiempo que tarda depende de qué parte de la suposición fue correcta, y un atacante paciente puede medir eso para reconstruir un valor. Es un canal estrecho, pero la comparación de contraseñas es precisamente donde esto importa.
Utiliza la función de verificación de la librería, la cual realiza la comparación en tiempo constante. Nunca compares los digests por tu cuenta y nunca compares la contraseña en texto plano.
Actualización de hashes con el tiempo
Es probable que cambies de opinión: el coste de computación aumenta o decides migrar de bcrypt a Argon2. Como nunca almacenaste el texto plano, solo puedes volver a generar el hash durante el login, que es cuando la contraseña reside brevemente en memoria.
Almacena el algoritmo y los parámetros con cada digest —la cadena codificada ya lo hace— y verifícalos en cada inicio de sesión exitoso:
if (await verify(stored, password)) {
if (needsRehash(stored)) {
await db.users.update(user.id, { passwordHash: await hash(password) });
}
// continue the login
}
Los usuarios que nunca vuelvan a iniciar sesión conservarán su hash antiguo, lo cual está bien; sigue protegiéndolos. Con el tiempo, las cuentas activas migrarán de forma natural.
La política de contraseñas en la práctica
El algoritmo te protege después de una brecha de seguridad. La política reduce las probabilidades de que ocurra una.
- Prioriza la longitud sobre la complejidad. Una frase de contraseña es mejor que
P@ssw0rd!. - Verifica las nuevas contraseñas comparándolas con listas de brechas conocidas, como la API de k-anonymity de Have I Been Pwned.
- No impongas rotaciones arbitrarias. La expiración forzada conduce a patrones de
Summer2026!. - Aplica rate-limit a los intentos de inicio de sesión y restablecimiento, y añade un backoff exponencial o un bloqueo de cuenta.
- Utiliza un flujo de restablecimiento genérico con tokens de un solo uso que expiren, y nunca envíes una contraseña por email.
Mejores prácticas
- Usa Argon2id, scrypt o bcrypt para el hashing; nunca utilices un hash rápido de propósito general.
- Deja que la librería genere y almacene el salt.
- Ajusta el costo para que el proceso tarde entre 100 y 500 ms y auméntalo con el tiempo.
- Verifica las contraseñas utilizando la función de tiempo constante de la librería.
- Vuelve a aplicar el hash tras un inicio de sesión exitoso si los parámetros han cambiado.
- Devuelve errores genéricos y aplica rate-limit a los intentos.
- Realiza el hashing siempre en el servidor; nunca en el cliente.
Errores comunes
- Encriptar contraseñas o almacenarlas en texto plano “temporalmente”.
- Usar SHA-256 o MD5 con unas pocas rondas y llamarlo hashing.
- Usar un único salt global o reutilizar el mismo salt en varias cuentas.
- Comparar hashes con
==o===. - Registrar contraseñas en los logs o incluirlas en reportes de errores.
- Limitar la longitud de la contraseña a un valor bajo, lo que perjudica el uso de passphrases.
- Olvidar actualizar el factor de costo durante años.
Próximos pasos
Las credenciales son solo la primera barrera. Una vez que el usuario ha sido verificado, necesitas un lugar donde mantener ese estado: lee la guía de Session Auth para sesiones basadas en cookies, o la guía de JWT para tokens stateless. Cuando prefieras no gestionar contraseñas en absoluto, OAuth 2.0 y OpenID Connect delegan el inicio de sesión a un proveedor de identidad.