Cada divulgación de violación de datos sigue la misma trayectoria: “Tomamos la seguridad en serio.” Luego lees los detalles y descubres que las contraseñas se almacenaron en texto plano, o se hashearon con MD5, o se cifraron con una clave que estaba en la misma base de datos. El problema de almacenamiento de contraseñas está resuelto — los algoritmos existen, las bibliotecas existen, la documentación existe — y sin embargo los desarrolladores aún lo hacen mal.
Esta publicación cubre qué almacenar, qué no almacenar, la diferencia entre hashing y cifrado, y por qué SHA-256(password) no es un hash de contraseña.
La regla: nunca almacenes en texto plano
Si tu base de datos se ve comprometida y tienes contraseñas en texto plano, cada usuario que reutiliza esa contraseña en otros sitios ahora está comprometido. No solo tu sitio — su correo electrónico, su banco, sus redes sociales. El almacenamiento de contraseñas en texto plano no es un problema de seguridad. Es un problema moral.
Almacena un hash. Siempre.
Hashing vs cifrado
Hashing es una transformación unidireccional. No puedes revertirlo. Dado hash(password), no puedes recuperar password. Solo puedes verificar: hashear la entrada y comparar.
Cifrado es una transformación bidireccional. Dado encrypt(password, key), puedes recuperar password con la clave. Si la clave se ve comprometida, cada contraseña se ve comprometida.
Hashea contraseñas. Nunca las cifres. Si necesitas recuperar el texto plano (no lo necesitas), estás resolviendo el problema equivocado.
Por qué SHA-256 no es suficiente
Un error común: hash = SHA-256(password). Es rápido, es bien conocido, y es incorrecto para contraseñas.
El problema es la velocidad. SHA-256 en una GPU moderna procesa miles de millones de hashes por segundo. Un atacante con una base de datos de hashes de contraseñas SHA-256 puede probar cada contraseña común, cada palabra de diccionario, cada permutación, en minutos.
Los algoritmos de hashing de contraseñas son deliberadamente lentos. Usan estiramiento de claves — miles de iteraciones de una función de hashing — para hacer los ataques de fuerza bruta impracticables:
- bcrypt — el estándar desde 1999. Factor de costo ajustable. Contraseña + sal → hash. Cada lenguaje principal tiene una biblioteca.
- scrypt — resistente a memoria. Requiere grandes cantidades de RAM para calcular, haciendo los ataques con GPU más difíciles.
- Argon2 — ganador del Concurso de Hashing de Contraseñas (2015). Resistente a memoria, tiempo y costos de memoria configurables. La mejor práctica actual.
- PBKDF2 — recomendado por NIST, ampliamente disponible, usa iteraciones HMAC. Menos resistente a memoria que Argon2 pero bien probado.
Usa bcrypt, scrypt o Argon2. No inventes tu propio esquema. No hashees una vez con SHA-256 y lo des por terminado.
Sal: por qué cada contraseña necesita uno único
Un sal son datos aleatorios agregados a la contraseña antes de hashear. Sin un sal, dos usuarios con la misma contraseña producen el mismo hash. Un atacante puede precomputar hashes para contraseñas comunes (una tabla arcoíris) y compararlos contra tu base de datos.
Con un sal único por usuario, cada hash es diferente incluso si las contraseñas son idénticas. Las tablas arcoíris no funcionan. El atacante tiene que fuerza bruta cada contraseña individualmente.
Las funciones modernas de hashing de contraseñas (bcrypt, Argon2) generan un sal aleatorio automáticamente. No necesitas gestionar sales por separado. Pasa la contraseña a la función de hashing, y ella maneja el resto.
Qué almacenar
Para cada usuario, almacena:
- El hash de la contraseña (salida de bcrypt/Argon2, que incluye el sal y los parámetros de costo)
- El algoritmo de hash (bcrypt, argon2, etc.)
- Los parámetros de costo (rondas de bcrypt, memoria/tiempo de Argon2)
NO almacenes:
- La contraseña en texto plano
- El sal por separado (la salida de la función de hashing lo incluye)
- Una “clave de cifrado” que podría descifrar la contraseña
- Pistas de contraseñas (filtran información)
La salida de hash de bcrypt se ve así: $2b$12$LJ3m4ys3GzF4VQ.YOUR.HASH.HERE. El $2b$ identifica el algoritmo, 12 es el factor de costo, y el resto es sal + hash. Para verificar un intento de inicio de sesión, hashea la contraseña enviada con los mismos parámetros y compara.
Factores de costo
El factor de costo de bcrypt determina qué tan lento es el hash. Cada incremento duplica el tiempo de cómputo:
- Costo 10: ~10ms por hash (rápido para usuarios, rápido para atacantes)
- Costo 12: ~40ms por hash (razonable)
- Costo 14: ~160ms por hash (lento para atacantes, notable para usuarios)
- Costo 16: ~640ms por hash (demasiado lento para la mayoría de flujos de inicio de sesión)
El punto dulce en 2026: bcrypt costo 12-14, Argon2 con 64MB+ de memoria y 3+ iteraciones. El objetivo es hacer que cada intento de hash sea lo suficientemente lento como para que la fuerza bruta de miles de millones de contraseñas tome años, pero lo suficientemente rápido como para que un usuario no note un retraso al iniciar sesión.
Limitación de velocidad
El hashing solo no es suficiente. Si un atacante puede probar millones de contraseñas por segundo contra tu endpoint de inicio de sesión, incluso bcrypt no te salvará.
Limita la velocidad de intentos de inicio de sesión. Después de 5-10 intentos fallidos desde la misma IP o cuenta, bloquea la cuenta temporalmente o requiere un CAPTCHA. Esto limita los ataques de fuerza bruta a un goteo.
No reveles si el nombre de usuario o la contraseña fue incorrecta. “Nombre de usuario o contraseña inválidos” es más seguro que “No se encontró ninguna cuenta con ese correo electrónico.” Este último le dice a un atacante qué nombres de usuario existen.
Agrega bloqueo de cuenta después de fallos sostenidos. Después de 10-20 intentos fallidos, bloquea la cuenta por 15 minutos o requiere verificación por correo electrónico para desbloquear.
Generación de contraseñas
Al generar contraseñas (para usuarios, para claves de API, para tokens):
- Mínimo 12 caracteres. 8 ya no es suficiente contra hardware moderno.
- Incluye mayúsculas, minúsculas, dígitos y símbolos. Pero la longitud importa más que la complejidad.
- Usa un CSPRNG (generador de números pseudoaleatorios criptográficamente seguro).
Math.random()no es seguro. - Nunca generes patrones predecibles.
Password1!parece complejo pero es trivialmente adivinable.
La mejor contraseña es una cadena generada aleatoriamente de un CSPRNG. Usa un gestor de contraseñas para almacenarla. No pidas a los usuarios que recuerden contraseñas complejas — dales una generada y deja que el gestor la maneje.
Errores comunes
- Hashear una vez con SHA-256 o MD5 — demasiado rápido, sin sal, trivialmente fuerza brutable.
- Usar un sal estático — si cada usuario comparte el mismo sal, las tablas arcoíris funcionan.
- Almacenar el sal por separado — si el sal está en una tabla diferente, un atacante que comprometa la base de datos obtiene ambos.
- Cifrar en lugar de hashear — si puedes descifrar la contraseña, un atacante con la clave también puede.
- Sin limitación de velocidad — incluso bcrypt es vulnerable a intentos de inicio de sesión ilimitados.
- Revelar si un nombre de usuario existe — ayuda a los atacantes a enumerar cuentas válidas.
- Usar
Math.random()para generación de contraseñas — predecible, no criptográficamente seguro.
Referencia rápida
| Haz | No hagas |
|---|---|
| Usa bcrypt, Argon2 o scrypt | Usa SHA-256 o MD5 |
| Deja que la biblioteca maneje los sales | Gestiona los sales tú mismo |
| Limita la velocidad de intentos de inicio de sesión | Permite intentos ilimitados de contraseña |
| Genera contraseñas con CSPRNG | Usa Math.random() |
| Almacena solo el hash | Almacena contraseñas en texto plano o cifradas |
| Di “credenciales inválidas” | Di “contraseña incorrecta” |
Pruébalo
Si necesitas generar una contraseña aleatoria segura o hashear una cadena para pruebas, usa una herramienta basada en navegador para que la operación permanezca local. Tu contraseña nunca debe tocar un servidor para una tarea de generación simple.