beginner Por Mathias Paulenko

Hash de Contraseñas con Argon2

Cómo hashear y verificar contraseñas de forma segura con Argon2id, ganador del Password Hashing Competition, con tuning correcto de parámetros y estrategias de migración desde bcrypt.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Descripción General

Argon2 ganó el Password Hashing Competition de 2015 y es el algoritmo recomendado por OWASP, NIST e IETF. Resiste cracking basado en GPU mediante computación memory-hard, haciendo los ataques de fuerza bruta miles de veces más costosos que con SHA-256 o incluso bcrypt. Argon2id combina las fortalezas de Argon2d (resistencia GPU) y Argon2i (resistencia a side-channels), siendo la recomendación por defecto para todos los sistemas nuevos.

Cuándo Usar

  • For alternatives, see Implement ABAC.

  • Almacenar contraseñas para cualquier sistema donde la resistencia a fuerza bruta importe

  • Reemplazar bcrypt, PBKDF2 o scrypt en sistemas existentes

  • Construir un sistema de autenticación desde cero

  • Cumplir con estándares de seguridad modernos (OWASP ASVS, NIST 800-63B)

  • Migrar desde algoritmos legacy (MD5, SHA-1) que ya no son seguros

Cuándo NO Usar

  • Ya usas bcrypt con factor de costo ≥ 12 y no hay mandato de compliance para migrar — bcrypt sigue siendo seguro
  • Necesitas hashear contraseñas en un dispositivo embebido con memoria limitada — Argon2 es intensivo en memoria

Implementación Paso a Paso

Python (argon2-cffi)

pip install argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

# Parámetros recomendados (OWASP 2023)
ph = PasswordHasher(
    time_cost=3,         # iteraciones
    memory_cost=65536,  # 64 MiB
    parallelism=4,       # threads
    hash_len=32,
    salt_len=16
)

def hash_password(password: str) -> str:
    return ph.hash(password)

def verify_password(password: str, hash_str: str) -> bool:
    try:
        ph.verify(hash_str, password)
        return True
    except VerifyMismatchError:
        return False

def verify_and_rehash(password: str, hash_str: str) -> tuple[bool, str | None]:
    try:
        ph.verify(hash_str, password)
        if ph.check_needs_rehash(hash_str):
            return True, ph.hash(password)
        return True, None
    except VerifyMismatchError:
        return False, None

hashed = hash_password("user_password_123")
valid, new_hash = verify_and_rehash("user_password_123", hashed)
if valid and new_hash:
    update_stored_hash_in_db(new_hash)

Node.js (argon2)

npm install argon2
import argon2 from 'argon2';

async function hashPassword(password) {
    return argon2.hash(password, {
        type: argon2.argon2id,
        memoryCost: 65536, timeCost: 3, parallelism: 4,
        hashLength: 32, saltLength: 16
    });
}

async function verifyPassword(password, hash) {
    try { return await argon2.verify(hash, password); }
    catch { return false; }
}

async function verifyAndRehash(password, hash) {
    const valid = await verifyPassword(password, hash);
    if (!valid) return { valid: false, newHash: null };
    const needsRehash = argon2.needsRehash(hash, { memoryCost: 65536, timeCost: 3, parallelism: 4 });
    return { valid: true, newHash: needsRehash ? await hashPassword(password) : null };
}

// Express middleware
app.post('/login', async (req, res) => {
    const user = await db.users.findOne({ email: req.body.email });
    if (!user) {
        await argon2.hash('dummy');  // timing constante
        return res.status(401).json({ error: 'Invalid credentials' });
    }
    const { valid, newHash } = await verifyAndRehash(req.body.password, user.password_hash);
    if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
    if (newHash) await db.users.updateOne({ _id: user._id }, { $set: { password_hash: newHash } });
    req.session.userId = user._id;
    res.json({ success: true });
});

Java (Spring Security + Bouncy Castle)

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>1.77</version>
</dependency>
public class Argon2PasswordHasher {
    private static final int SALT_LEN = 16, HASH_LEN = 32;
    private static final int ITERATIONS = 3, MEMORY = 65536, PARALLELISM = 4;
    private final SecureRandom random = new SecureRandom();

    public String hash(String password) {
        byte[] salt = new byte[SALT_LEN];
        random.nextBytes(salt);
        Argon2Parameters params = new Argon2Parameters.Builder()
            .withSalt(salt).withParallelism(PARALLELISM)
            .withMemoryAsKB(MEMORY).withIterations(ITERATIONS)
            .withVersion(Argon2Parameters.ARGON2_VERSION_13).build();
        Argon2BytesGenerator gen = new Argon2BytesGenerator();
        gen.init(params);
        byte[] result = new byte[HASH_LEN];
        gen.generateBytes(password.toCharArray(), result);
        return String.format("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
            MEMORY, ITERATIONS, PARALLELISM,
            Base64.toBase64String(salt), Base64.toBase64String(result));
    }

    public boolean verify(String password, String encoded) {
        String[] parts = encoded.split("\\$");
        int memory = Integer.parseInt(parts[3].split(",")[0].split("=")[1]);
        int iterations = Integer.parseInt(parts[3].split(",")[1].split("=")[1]);
        int parallelism = Integer.parseInt(parts[3].split(",")[2].split("=")[1]);
        byte[] salt = Base64.decode(parts[4]);

        Argon2BytesGenerator gen = new Argon2BytesGenerator();
        gen.init(new Argon2Parameters.Builder().withSalt(salt)
            .withParallelism(parallelism).withMemoryAsKB(memory)
            .withIterations(iterations).withVersion(Argon2Parameters.ARGON2_VERSION_13).build());
        byte[] expected = new byte[HASH_LEN];
        gen.generateBytes(password.toCharArray(), expected);
        return Arrays.equals(expected, Base64.decode(parts[5]));
    }
}

Selección de Parámetros

ParámetroMínimo OWASP 2023Racional
Memoria64 MiB (65536 KiB)Suficiente para exceder cache de GPU, baja para capacidad del servidor
Iteraciones3Balancea costo de CPU sin latencia excesiva (>250ms por hash es aceptable)
Paralelismo4Coincide con conteo típico de cores de servidor
Salt length16 bytesPreviene rainbow tables; entropía de 128-bit es suficiente
Hash length32 bytesOutput de 256-bit; más largo no mejora seguridad contra fuerza bruta

Migrando desde bcrypt

def verify_password(password: str, hash_str: str) -> bool:
    if hash_str.startswith("$2"):
        import bcrypt
        return bcrypt.checkpw(password.encode(), hash_str.encode())
    elif hash_str.startswith("$argon2"):
        return verify_argon2(password, hash_str)
    return False

async def login(email, password):
    user = await get_user(email)
    if verify_password(password, user.hash):
        if user.hash.startswith("$2"):
            await update_hash(user.id, hash_password(password))
        return create_session(user)
    return None

Lo que funciona

  • Nunca implementes tu propio hash de contraseñas. Usa librerías bien auditadas. Implementaciones custom introducen ataques de timing y bugs de memory safety.
  • Siempre usa Argon2id, no Argon2d o Argon2i. Argon2id es la variante recomendada que balancea resistencia GPU y protección contra side-channels.
  • Ajusta parámetros a tu hardware. El hashing debería tardar 250-500ms en hardware de producción. Perfiliza con valores de time_cost hasta alcanzar este objetivo.
  • Usa comparación en tiempo constante para todo el path de verificación. Incluso el path “usuario no encontrado” debería realizar un hash dummy para prevenir timing attacks.
  • Almacena hashes, no contraseñas, no contraseñas encriptadas. El hashing es unidireccional; la encriptación es reversible.

Errores Comunes

  • Usar SHA-256, MD5 o SHA-1 para contraseñas. Diseñados para velocidad, son triviales de fuerza bruta en GPUs (miles de millones de intentos por segundo).
  • Salting con una constante global. Un salt único por usuario es obligatorio. Reusar salt entre usuarios permite ataques de rainbow table.
  • Olvidar manejar encoding consistentemente. UTF-8, Latin-1 y ASCII producen diferentes secuencias de bytes. Estandariza en UTF-8.
  • Configurar memoria demasiado alta. Argon2 con 1 GiB puede causar OOM kills bajo carga. Comienza con 64 MiB.
  • No actualizar parámetros con el tiempo. El hardware mejora. Programa revisiones anuales y rehashea contraseñas en login.

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de authentication y security para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica hash de contraseñas con argon2 cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Troubleshooting

  • Login works for some users but not others: check identity provider configuration, user claims, and role mappings. Look for case sensitivity in identifiers.
  • Token expires too quickly: verify token lifetime, refresh logic, and clock skew. Short tokens with secure refresh are preferred.
  • Session is not shared across subdomains: set the cookie domain and SameSite policy correctly.
  • Brute force attempts increase: implement rate limiting, account lockout, and CAPTCHA.
  • OIDC flow fails with invalid_state: ensure the state parameter is stored, transmitted, and validated in the same user session.

Errores Comunes en Producción

  • Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
  • Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
  • Codificar valores fijos que deberían ser configurables por entorno.
  • Olvidar agregar logging y monitoreo en cada paso.
  • Desplegar sin plan de rollback ni estrategia de backup probada.
  • Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
  • No documentar la versión y configuración usadas en producción.
  • Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.

Preguntas frecuentes

¿Por qué Argon2id es preferido sobre bcrypt?
Argon2id es memory-hard, lo que hace que los ataques con GPU y ASIC sean mucho más costosos. bcrypt sigue siendo seguro, pero Argon2id es la recomendación actual de OWASP y NIST para sistemas nuevos.
¿Cómo elijo los parámetros de Argon2?
Apunta a un tiempo de hashing de 250-500ms en hardware de producción. Un punto de partida común es time_cost=3, memory_cost=65536 (64 MiB) y parallelism=4. Perfila y ajusta anualmente.
¿Cómo migro contraseñas desde bcrypt?
Acepta hashes tanto de bcrypt como de Argon2 durante la verificación. En un login exitoso con bcrypt, rehashea transparentemente la contraseña con Argon2 y almacena el nuevo hash.
¿Qué pasa si configuro memory_cost demasiado alto?
El servidor puede quedarse sin memoria bajo carga, causando OOM kills o denegación de servicio. Comienza con 64 MiB y aumenta solo después de pruebas de carga.