Nivel 2 · 25 min
Hash de Contraseñas: bcrypt/argon2/scrypt
El hash de contraseñas es el cinturón de seguridad de la autenticación: inefectivo hasta que la DB se filtra, y entonces es lo único que se interpone entre los hashes adquiridos por el atacante y millones de contraseñas en plano. Usá un KDF memory-hard (argon2id, scrypt) o un slow hash tuneable (bcrypt). Nunca uses MD5, SHA-1, SHA-256 ni ningún hash rápido sin sal para contraseñas.
Por Qué los Hashes Rápidos Son Catastróficos
MD5, SHA-1, SHA-256 están diseñados para ser rápidos — miles de millones de hashes por segundo en una GPU moderna. Una RTX 4090 de consumo computa ~25 GH/s de SHA-256 (25.000 millones por segundo). Con una DB filtrada de hashes SHA-256 de password sin sal, cada contraseña de 8 caracteres en minúscula se brute-forcea en menos de 10 minutos; cada contraseña de 10 caracteres en mayúscula y minúscula en menos de un día; el wordlist rockyou.txt de 14M de entradas se procesa en segundos. Las sales (bytes random por usuario que se appendean a la contraseña antes de hashear) frenan rainbow tables pero no frenan el brute force contra un solo usuario — el atacante simplemente hashea wordlist + salt para cada usuario. El fix es hacer que la función de hash sea cara: el factor de costo 12 de bcrypt produce ~250ms por hash, reduciendo el throughput de GPU de miles de millones/s a ~3/s — un slowdown de 10.000 millones de veces. Argon2id requiere además memoria (64 MB+), que las GPUs y ASICs manejan mal; scrypt es similar.
Parámetros de Costo Concretos
bcrypt: factor de costo 12 en 2026 (subilo a medida que mejora el hardware; OWASP recomienda chequear el costo anualmente y subirlo en 1 cada 18-24 meses). 12 = 2^12 = 4096 iteraciones. El límite de input de 72 bytes de bcrypt es una limitación conocida — pre-hasheá inputs largos con HMAC-SHA-256 si lo necesitás. Argon2id (preferido para sistemas nuevos): m=64MB, t=3 iteraciones, p=4 paralelismo. El OWASP Password Storage Cheat Sheet es la referencia autoritativa. scrypt: N=2^17, r=8, p=1 — más pesado que Argon2id pero soportado por plataformas más viejas. PBKDF2: solo aceptable en entornos restringidos por FIPS, con iteraciones en los millones para SHA-256 (FIPS 140-3 requiere 1M+ en 2026). Apuntá a ~250-500ms por verificación en hardware de producción — más lento que eso y la latencia de login se degrada; más rápido y el trabajo del atacante se vuelve muy fácil. El tiempo por hash debe benchmarkearse en el hardware real del servidor de auth, no estimarse de blog posts.
Sal, Pepper, Verificación y Rehash en Login
Sal: random de 16+ bytes por usuario, embebida en el output del hash (bcrypt y argon2 lo hacen automáticamente). Pepper: un secreto opcional del lado servidor agregado a cada contraseña antes de hashear — guardado fuera de la DB (en un vault o config de la app) para que un leak solo de DB no habilite cracking offline. Pepper es defensa en profundidad, no un sustituto de un slow hash. Verificación: usá la comparación constant-time de la lib (bcrypt.compare, argon2.verify) — nunca strcmp de los hashes (timing leak, aunque menor). Rehash en login: cuando verificás contra un hash con factor de costo viejo, rehasheá transparentemente el plaintext (que tenés en el momento del login) con los parámetros actuales y actualizá la DB. Esto te deja subir el factor de costo a lo largo de los años sin forzar resets de contraseña. Migración de algoritmo (ej: bcrypt a argon2): mismo truco — guardá el prefijo del algoritmo en el hash ($2b$ para bcrypt, $argon2id$ para argon2), branch en él al verificar, y migrá usuarios en el próximo login.
Code example
// Java con Spring Security 6+
// Configurá: BCryptPasswordEncoder con costo 12
@Bean
PasswordEncoder passwordEncoder() {
return new DelegatingPasswordEncoder("argon2", Map.of(
"argon2", new Argon2PasswordEncoder(16, 32, 4, 65536, 3), // saltLen, hashLen, p=4, m=64MB, t=3
"bcrypt", new BCryptPasswordEncoder(12)
));
}
// Registración
String hash = encoder.encode(rawPassword);
// guardado: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
// Login — verify constant-time + rehash si está viejo
boolean ok = encoder.matches(rawPassword, storedHash);
if (ok) {
if (encoder.upgradeEncoding(storedHash)) {
// Cambió el factor de costo o el algoritmo — rehash con parámetros actuales
String newHash = encoder.encode(rawPassword);
userRepo.updatePasswordHash(userId, newHash);
}
// proceder con login
} else {
// log de falla (rate limit / lockout)
}
// Pepper (defensa en profundidad) — HMAC del password con un secreto del lado servidor primero
byte[] peppered = Mac.getInstance("HmacSHA256")
.doFinal((PEPPER + rawPassword).getBytes(UTF_8));
String hash = encoder.encode(Base64.getEncoder().encodeToString(peppered));