Ejercicio 1 — El hash
- Formato argon2:
argon2id$v=19$m=65536,t=3,p=4$<salt>$<digest>— 64MB de memoria, 3 iteraciones, 4 hilos. El salt va incluido (aleatorio por usuario). - Típicamente 50-200ms por verificación en una portátil → ~5-20 verificas/seg/hilo. Un atacante offline con 10.000 GPUs (cada GPU con sus GB): miles de veces más rápido, pero el coste de memoria de argon2 limita el paralelismo real por GPU — ese es el diseño de argon2id (CPU+memoria juntos suben el coste del crack masivo).
- Duplicar parámetros ~duplica tu tiempo de login (100→200ms) y más que duplica el coste del atacante por intento (el suyo escala por intento, el tuyo por login legítimo). El equilibrio: ~100-250ms de login es imperceptible; ajustas según hardware real (benchmark en producción-like).
- Pwned Passwords con k-anonymity: envías solo los primeros 5 caracteres del SHA-1 (el servidor nunca ve tu contraseña) y compara localmente el resto — validador que rechaza contraseñas filtradas conocidas.
Ejercicio 2 — Enumeración
- Los tiempos difieren si el flujo corta antes (email no existe → no calculas hash): un atacante mide y enumera emails. Igualar: hacer el mismo trabajo (hash dummy) o dormir: misma respuesta, mismo tiempo aproximado. Mensaje único "Credenciales inválidas".
- Respuesta única del forgot: "Si existe una cuenta, recibirás un email" exista o no. El filtrado por tiempo también aplica aquí (mismo trabajo siempre).
Ejercicio 3 — TOTP
- QR con
otpauth://totp/TicketFlow:{email}?secret={b32}&issuer=TicketFlow: la app del usuario guarda el secreto; tu BD guarda el suyo (cifrado).confirmedevita activar MFA sin demostrar que el usuario escaneó bien. - Flujo: login ok →
mfa_requiredsin tokens →POST /auth/mfa/verifycon el código → tokens emitidos. Sin este paso intermedio, el password solo sería suficiente y el MFA decorativo. valid_window=1acepta el código del periodo anterior/siguiente (±30s): cubre desincronización leve de relojes sin abrir la puerta a códigos de minutos atrás (con replay: el mismo código puede verificarse dos veces dentro de la ventana — Redis/bd "last used counter" por usuario lo impide; pyotp no lo hace por ti).
Ejercicio 4 — Códigos de recuperación
- 10 códigos
xxxx-xxxx; en BD SOLO el hash (como contraseñas): si filtra la BD, los códigos no son usables. - Uso único bajo concurrencia:
UPDATE... SET used_at = now() WHERE id = X AND used_at IS NULL(una fila, afecta 0 o 1 — la 10 aplicada): dos usos simultáneos, uno gana. El check del hash antes, el UPDATE atómico del flag decide. - Contador visible + regenerar (invalida los viejos) + aviso: el usuario no descubre que no tiene códigos el día que pierde el móvil.
Ejercicio 5 — Reset
- Respuesta única siempre; el email real solo si existe. Token:
token_urlsafe(32), hash en BD, TTL 1h, single-use (misma técnica del Ej. 4). - Segundo uso del token: rechazo (used_at). Token vencido: rechazo (exp). Tras reset: blacklist de todos los refresh del usuario (18) + notificación "tu contraseña cambió" (si no fuiste tú, contacta) — el atacante que ya tenía sesión pierde el acceso.
- Con MFA: el link del email NO basta — exige TOTP o código de recuperación al usar el token. Sin esto, el atacante que controla el email de la víctima resetea y entra: el reset es el punto que por BYPASEA el hash, y por eso no puede bypasea el MFA.
Resumen del profesor
- argon2id con memoria: el hash lento convierte un leak de BD en un problema de años, no de horas.
- No enumeres usuarios (tiempos y mensajes idénticos); deja que HIBP diga qué contraseñas están quemadas.
- TOTP: secreto cifrado, ventana honesta, códigos de recuperación hasheados y de un solo uso bajo transacción.
- El reset de contraseña es la puerta que bypasea todo: exige el segundo factor y revoca sesiones.
Después: Lección 21 — Autorización.