Llega la deuda más antigua y más importante del curso: cómo guarda MediNube las contraseñas con las que ana.perez y miles de usuarios más inician sesión en portal.medinube.example. La rozamos en 01-03 (tokens y entropía), la esquivamos a propósito en 02-04 ("derivar claves para cifrar es otro problema; el de autenticar llegará en 03-03") y la lección anterior nos dejó en la puerta: los hashes rápidos valen para secretos de alta entropía, y una contraseña humana es justo lo contrario. Esta lección recorre la evolución histórica del error (texto claro → hash rápido → hash con salt → funciones deliberadamente lentas), fija la recomendación actual — Argon2id con argon2-cffi — y la aterriza en el flujo completo de registro y login del portal, incluida la migración transparente de los hashes heredados. Importa porque es, con diferencia, el fallo criptográfico con más víctimas reales: las filtraciones de bases de datos de usuarios se cuentan por miles de millones de credenciales, y la diferencia entre un incidente molesto y una catástrofe es exactamente lo que vas a aprender aquí.
Contenido
- El escenario de amenaza: te van a robar la base de datos
- Verificador vs clave: en qué se diferencia esto de 02-04
- La evolución del error, etapa por etapa
- Las funciones correctas: bcrypt, scrypt y Argon2id
- Argon2id en Python con
argon2-cffi - El formato modular: el hash que lleva todo dentro
- Caso práctico: registro y login en MediNube, con migración incluida
- Qué NO hacer (aunque parezca buena idea)
- Más allá de la contraseña: MFA y passkeys
El escenario de amenaza: te van a robar la base de datos
El diseño empieza por asumir lo peor (regla de oro 2: el enemigo conoce el sistema — y aquí, además, tiene tu disco). El escenario contra el que se diseña el almacenamiento de contraseñas es concreto: el atacante obtiene una copia completa de la tabla de usuarios. Vías sobradamente reales: inyección SQL, un backup en un bucket mal configurado, un portátil de un desarrollador, un empleado descontento.
Dos matices que definen bien el problema:
- No diseñamos para el login online. Los intentos contra el formulario de
portal.medinube.examplese frenan con límites de intentos y bloqueos — capa de aplicación (regla 9). Diseñamos para el ataque offline: el atacante, con la tabla en su máquina, prueba candidatos a la velocidad de su hardware, sin límites ni logs. - El objetivo no es que ninguna contraseña caiga (la de quien usa
123456caerá siempre), sino comprar tiempo y encarecer el ataque masivo: que descifrar el grueso de la tabla cueste años de GPU en vez de una tarde, dando margen para detectar la brecha, forzar reseteos y avisar a los afectados. Con datos sanitarios de por medio, además, una brecha real activa obligaciones del RGPD (notificación a la autoridad y a los afectados) — recordatorio de siempre: MediNube es ficticio, y un sistema real de este tipo pasa por revisión de seguridad y compliance antes de producción.
Verificador vs clave: en qué se diferencia esto de 02-04
En 02-04 usamos scrypt para convertir la contraseña del administrador de Clínica Sol en una clave AES con la que cifrar un backup. Aquí usamos la misma familia de funciones... para no cifrar nada. La distinción merece su tabla, porque confundir ambos casos genera diseños absurdos:
| Derivar clave para cifrar (02-04) | Guardar verificador para autenticar (hoy) | |
|---|---|---|
| Objetivo | Obtener 32 bytes utilizables como clave | Poder comprobar "¿es esta la contraseña?" |
| ¿Se guarda la salida? | No — la clave se usa y se descarta | Sí — el verificador vive en la BD |
| ¿La salida abre algo? | Sí: descifra los datos | No: por sí sola no sirve para nada |
| Si un atacante la roba | Desastre: descifra el backup | Debe seguir atacando: solo es un verificador |
| Necesita determinismo externo | Sí: mismos parámetros para re-derivar | Autocontenido: los parámetros viajan en el hash |
| Función típica | scrypt/Argon2 como KDF | bcrypt/scrypt/Argon2id como password hasher |
La idea nueva es la de verificador: un valor que permite comprobar una contraseña presentada pero que no contiene la contraseña ni sustituye a tenerla. En el login, MediNube recalcula la función sobre lo que el usuario tecleó y compara con lo guardado. La misma matemática lenta de 02-04, propósito opuesto.
La evolución del error, etapa por etapa
Cada etapa de esta historia sigue viva en código heredado de producción. Conviene conocerlas todas, porque te las encontrarás.
Etapa 0: texto claro
def guardar_password_MAL(usuario: str, password: str):
bd.insertar(usuario, password) # NO: la BD robada ES la lista de credencialesRobo de BD = todas las credenciales de todos los usuarios, al instante. Y como la gente reutiliza contraseñas, el daño se propaga a sus cuentas de correo y banca. Agravante conocido: RockYou (2009, 32 millones de contraseñas en claro) — su fichero es hoy el diccionario estándar de los atacantes. Si tu aplicación puede enseñarte tu contraseña actual o enviártela por email, está en esta etapa.
Etapa 1: hash rápido sin salt
def guardar_password_MAL_v2(usuario: str, password: str):
bd.insertar(usuario, hashlib.sha256(password.encode()).hexdigest())Parece resolverlo ("es unidireccional") y falla por los dos flancos que ya conoces de 03-01:
- Velocidad. SHA-256 está diseñado para volar. Cifras orientativas del hardware de un aficionado con unas pocas GPUs modernas: del orden de decenas de miles de millones de SHA-256 por segundo (y ~10× más para MD5). El diccionario RockYou entero contra un hash: milésimas de segundo. Todas las combinaciones de 8 caracteres alfanuméricos: cuestión de horas.
- Determinismo global, sin salt. La misma contraseña produce el mismo hash en todas las filas y en todas las empresas del mundo. Eso habilita las rainbow tables — tablas precalculadas hash→contraseña que se computan una vez y se reutilizan contra cualquier BD — y de regalo delata patrones: si tres usuarios comparten hash, comparten contraseña.
Etapa 2: hash rápido con salt
Aquí reaparece el salt de 02-04: un valor único por usuario, aleatorio (CSPRNG), no secreto, que se guarda junto al hash y se mezcla con la contraseña antes de hashear.
- Lo que arregla: mata las rainbow tables (habría que precalcular una tabla por cada salt posible) y hace que contraseñas iguales den hashes distintos. El ataque ya no es "toda la tabla de golpe": es fila a fila.
- Lo que NO arregla: la velocidad. El atacante ya no precalcula, pero sigue probando RockYou y sus derivados contra cada fila a miles de millones de intentos por segundo.
sha256(salt + password)con GPUs sigue cayendo a mansalva. (Y ojo: seguiría siendo la construcción con length extension de 03-02 — segunda razón para no fabricarla a mano.)
Etapa 3: funciones deliberadamente lentas
La conclusión a la que la industria tardó décadas en llegar: contra secretos de baja entropía, la única palanca del defensor es el coste de cada intento. Si verificar una contraseña cuesta ~100 ms y una buena ración de RAM a propósito, el usuario legítimo ni lo nota (un login por sesión), pero el atacante que necesita miles de millones de intentos ve su presupuesto multiplicado por millones. Es exactamente la asimetría de las KDFs lentas de 02-04, ahora aplicada a verificadores. Y con la memoria como arma anti-GPU: una GPU tiene miles de núcleos, pero no miles de gigabytes de RAM rápida para darles.
flowchart LR
A["Etapa 0<br/>texto claro"] -->|"hash 'unidireccional'"| B["Etapa 1<br/>SHA-256 sin salt"]
B -->|"rainbow tables"| C["Etapa 2<br/>SHA-256 + salt"]
C -->|"GPUs: sigue siendo rapido"| D["Etapa 3<br/>lento a proposito:<br/>bcrypt / scrypt / Argon2id"]
Las funciones correctas: bcrypt, scrypt y Argon2id
Las tres candidatas serias, dos de las cuales ya conoces de 02-04:
- bcrypt (1999): el veterano honorable. Coste puramente de CPU regulado por un factor de coste exponencial (coste 12 ≈ 4096 iteraciones internas de su núcleo; cada +1 duplica el tiempo). Sigue siendo razonable, con dos peros: no es memory-hard (las GPUs le van comiendo terreno año a año) y tiene una peculiaridad peligrosa: solo procesa los primeros 72 bytes de la contraseña — todo lo que pase de ahí se ignora en silencio, y más de una implementación ha "resuelto" esto truncando o pre-hasheando mal. Si heredas bcrypt, no es una emergencia; para código nuevo hay mejor opción.
- scrypt (2009): el primero memory-hard, ya lo usaste para el backup exportable con
n=2^17, r=8, p=1. Perfectamente válido también como password hasher; su punto débil es tener los costes de CPU y memoria acoplados en un solo parámetro principal. - Argon2id (2015): ganador de la Password Hashing Competition y recomendación actual (también de OWASP). Tres mandos independientes — memoria, iteraciones (time cost), paralelismo — y la variante id combina resistencia a ataques por GPU con resistencia a ataques de canal lateral. Es la que adoptará MediNube.
| bcrypt | scrypt | Argon2id | |
|---|---|---|---|
| Año / origen | 1999 | 2009 | 2015 (ganador PHC) |
| Memory-hard | No | Sí | Sí |
| Parámetros | factor de coste | n, r, p (acoplados) | memoria, tiempo, paralelismo |
| Límite de entrada | 72 bytes | No | No |
| En Python | paquete bcrypt |
pyca/cryptography, hashlib |
argon2-cffi |
| Veredicto | Aceptable si ya lo usas | Buena opción | Recomendada para código nuevo |
Parámetros orientativos de Argon2id para un servidor de aplicaciones en 2026 (y punto de partida de OWASP): memoria 64 MiB, 3 iteraciones, paralelismo 4, salt de 16 bytes, salida de 32. La regla práctica: apunta a 50–200 ms por verificación en tu hardware de producción y sube la memoria tanto como tu servidor tolere bajo carga de logins concurrentes. Son justo los valores por defecto actuales de argon2-cffi, lo que hace el código que sigue muy poco dramático.
Argon2id en Python con argon2-cffi
pip install argon2-cffi (lo presentamos en 02-04; ahora lo usamos de verdad). La pieza central es PasswordHasher:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher() # Argon2id, 64 MiB, t=3, p=4 - defaults sanos
# Registro: hashear
hash_guardado = ph.hash("CorrectoCaballoBateriaGrapa")
print(hash_guardado)
# $argon2id$v=19$m=65536,t=3,p=4$YWxndW5zYWx0cnJyYQ$G3Kx0...
# Login: verificar
try:
ph.verify(hash_guardado, "CorrectoCaballoBateriaGrapa") # OK: devuelve True
ph.verify(hash_guardado, "contrasena-incorrecta") # lanza excepcion
except VerifyMismatchError:
print("Contrasena incorrecta")Desglose, pieza a pieza:
PasswordHasher()fija los parámetros de la política actual. Puedes ajustarlos (PasswordHasher(memory_cost=131072, time_cost=3, parallelism=4), memoria en KiB), pero no los bajes de los defaults sin una razón medida.ph.hash(password)genera él solo un salt fresco de 16 bytes del CSPRNG y devuelve el hash completo en formato texto. Fíjate: no le pasamos salt — imposible olvidarlo o reutilizarlo. Aceptastrdirectamente (se encarga del UTF-8).ph.verify(hash, password)re-ejecuta Argon2id con el salt y los parámetros que lee del propio hash y compara en tiempo constante. Con contraseña incorrecta lanzaVerifyMismatchError— el patrón de "excepción, no booleano silencioso" que ya viste enHistorialManipulado(02-03): un fallo de verificación debe ser imposible de ignorar por accidente.- Cada llamada a
hashcon la misma contraseña da un resultado distinto (salt nuevo). Por eso no se puede "buscar el hash en la BD" como hacíamos con los tokens en 03-02: aquí se busca la fila por el nombre de usuario y se verifica su hash. Compara con la tabla verificador vs clave: esto es un verificador autocontenido, no una clave reproducible.
El formato modular: el hash que lleva todo dentro
Ese churro con $ es el formato modular (PHC string format), y entenderlo evita todo un género de errores:
$argon2id$v=19$m=65536,t=3,p=4$YWxndW5zYWx0cnJyYQ$G3Kx0...
│ │ │ │ │
│ │ │ │ └─ hash (Base64)
│ │ │ └─ salt (Base64)
│ │ └─ parametros: 65536 KiB, 3 iteraciones, paralelismo 4
│ └─ version del algoritmo
└─ algoritmo- El salt y los parámetros viajan dentro del hash. No necesitas columnas
salt,iteraciones,algoritmoen la tabla de usuarios: una sola columna de texto (~100 caracteres) lo contiene todo.verifylee ahí lo que necesita. - Que el salt sea visible no es un fallo: como fijamos en 02-04, el salt es único y aleatorio, pero público. Lo que protege la contraseña es el coste de la función, no el secreto del salt.
- Consecuencia potente: cada fila declara sus parámetros, así que en una misma tabla pueden convivir hashes antiguos (
m=65536) y nuevos (m=131072), o incluso bcrypt ($2b$12$...) y Argon2id durante una migración. Esto es criptoagilidad (regla 8) aplicada a contraseñas, y es la base del apartado siguiente. La pieza que lo cierra esph.check_needs_rehash(hash): devuelveTruesi el hash guardado se generó con parámetros más débiles que la política actual delPasswordHasher.
Caso práctico: registro y login en MediNube, con migración incluida
Contexto completo del hilo: el portal arrastra la tabla usuarios de la etapa 2 — SHA-256 con salt, el clásico heredado. El código encontrado:
# codigo heredado - NO usar
import hashlib, os
def guardar_password_MAL(usuario: str, password: str):
salt = os.urandom(16)
h = hashlib.sha256(salt + password.encode("utf-8")).hexdigest()
bd.guardar(usuario, esquema="sha256", salt=salt.hex(), hash=h)
def login_MAL(usuario: str, password: str) -> bool:
fila = bd.buscar(usuario)
h = hashlib.sha256(bytes.fromhex(fila.salt) + password.encode()).hexdigest()
return h == fila.hash # ademas, comparacion no constanteDiagnóstico ya conocido: salt correcto, pero función rápida (etapa 2) y comparación con ==. El reto real no es escribir la versión buena — es migrar sin resetear a todos los usuarios. No podemos convertir los hashes nosotros (no tenemos las contraseñas, ¡de eso se trata!), pero hay un momento en que cada contraseña pasa legítimamente por el servidor: el login. La estrategia estándar es migrar en ese momento, usuario a usuario:
# medinube/auth.py - version correcta, con migracion transparente
import hashlib, hmac
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher() # politica actual: Argon2id, 64 MiB, t=3, p=4
def registrar(usuario: str, password: str):
"""Alta de un usuario nuevo: directamente Argon2id."""
bd.guardar(usuario, esquema="argon2", hash=ph.hash(password))
def login(usuario: str, password: str) -> bool:
fila = bd.buscar(usuario)
if fila is None:
ph.hash(password) # gastar el mismo tiempo aunque no exista
return False # (no delatar por timing que el usuario no existe)
if fila.esquema == "sha256": # hash heredado (etapa 2)
h = hashlib.sha256(bytes.fromhex(fila.salt) + password.encode()).hexdigest()
if not hmac.compare_digest(h, fila.hash):
return False
# Contrasena correcta y en memoria: MIGRAR ahora
bd.guardar(usuario, esquema="argon2", hash=ph.hash(password), salt=None)
return True
# Esquema moderno
try:
ph.verify(fila.hash, password)
except VerifyMismatchError:
return False
if ph.check_needs_rehash(fila.hash): # politica endurecida?
bd.guardar(usuario, esquema="argon2", hash=ph.hash(password))
return TrueLéelo con calma, porque condensa media lección:
registrar: tres líneas. Toda la complejidad (salt, parámetros, formato) vive enargon2-cffi. Nada que inventar (regla 1). La política de calidad de la contraseña (longitud mínima, no estar en listas de filtradas) es de la capa de aplicación y complementa, no sustituye, a esto.- Usuario inexistente: se ejecuta igualmente un hash para que la respuesta tarde lo mismo que con un usuario real — si no, el tiempo de respuesta delataría qué nombres de usuario existen (el mismo espíritu de la regla 6 a nivel de aplicación).
- Rama heredada: se verifica contra el esquema antiguo (con
compare_digest, arreglando de paso el==) y, si la contraseña es correcta, se aprovecha que la tenemos en memoria para re-hashearla con Argon2id y sobreescribir la fila. El usuario no nota nada; la etiquetaesquemade la fila cambia para siempre. Con el tiempo, la ramasha256se vacía — y para los rezagados que nunca vuelven, se fuerza un reseteo por email pasado un plazo (con el token de recuperación bien guardado de 03-02, todo conecta). check_needs_rehash: la misma jugada, pero hacia el futuro. Cuando en 2029 subáismemory_cost, los hashes viejos se irán renovando solos en cada login. Criptoagilidad de serie, sin migraciones traumáticas.- El campo
esquemaexplícito cumple el papel del byte de versión de nuestro formatov1de 02-03: cada dato declara cómo está protegido.
Qué NO hacer (aunque parezca buena idea)
- Limitar la longitud de la contraseña ("máximo 16 caracteres"). No hay razón criptográfica: el hash produce salida fija pese lo que pese la entrada. Un límite corto solo recorta la entropía del usuario diligente. Un límite generoso anti-DoS (p. ej. 1024 caracteres) sí es razonable. Y si usas bcrypt, recuerda su truncado silencioso a 72 bytes: es un motivo para Argon2id, no para prohibir frases de paso largas.
- Truncar o "normalizar" la contraseña (recortar espacios, pasar a minúsculas "por usabilidad"). Cada transformación funde entropía y crea clases de equivalencia que el atacante explota gratis.
- El "pepper" mal entendido. Un pepper es un secreto global del servidor que se añade al cálculo (típicamente
HMAC(pepper, password)antes del hash lento), guardado fuera de la BD — en el gestor de secretos que veremos en 06-01. Bien hecho, añade una capa real: la BD robada sin el pepper no es atacable ni por diccionario. Pero tiene riesgos serios que lo dejan en "opcional para equipos maduros": si el pepper se pierde, todos los logins mueren sin recuperación posible; rotarlo es difícil (no puedes recalcular los hashes sin las contraseñas); y mal implementado (concatenar en vez de HMAC, guardarlo en la misma BD) es teatro. Argon2id primero; pepper, solo con gestión de secretos solvente. - Componer esquemas caseros:
md5(sha1(password)), "mi Argon2 pero con mi XOR antes", doble Argon2 "para más seguridad". Regla de oro 1, una vez más: las combinaciones caseras no suman fuerzas, suman superficie de error.PasswordHashertal cual. - Cifrar las contraseñas en vez de hashearlas. Cifrar es reversible por diseño: quien tenga la clave (o la robe junto a la BD) recupera todas las contraseñas. Un verificador no debe poder revertirse ni por el propio MediNube — regla de oro 7: el objetivo aquí es verificar, no recuperar.
Más allá de la contraseña: MFA y passkeys
Cerremos con honestidad sobre los límites: Argon2id protege la contraseña en tu base de datos; no puede hacer nada si el usuario la teclea en un portal de phishing o la reutiliza en un sitio ya filtrado. Por eso el estado del arte añade capas: MFA (un segundo factor: TOTP, códigos de un solo uso) y, cada vez más, passkeys (WebAuthn) — credenciales basadas en firmas de clave pública donde el servidor ya no guarda ningún secreto verificable por diccionario. No las desarrollaremos en este curso, pero fíjate en el detalle: passkeys = firmas digitales, la criptografía asimétrica que empieza en el módulo que viene. Para MediNube, con datos sanitarios, MFA para el personal clínico no sería opcional en un despliegue real.
Errores Comunes y Consejos
- "Uso SHA-256 con salt, ya estoy en la etapa buena". No: el salt solo mata el precálculo. Sin coste por intento sigues en la etapa 2, y las GPUs la mastican. La pregunta de control: "¿cuánto tarda verificar una contraseña?" — si la respuesta es "nada", ese es el problema.
- Guardar el salt en columna aparte y pasárselo a
argon2-cffi. Señal de estar peleándose contra la librería:hash()genera el salt y lo incrusta en el formato modular;verify()lo lee de ahí. Si tu esquema de BD tiene columnasaltjunto a hashes Argon2, algo se está haciendo a mano que no tocaba. - Devolver errores distintos para "usuario no existe" y "contraseña incorrecta" (o tardar distinto). Enumeración de usuarios servida. Mismo mensaje, mismo tiempo.
- Ajustar los parámetros a ojo o copiarlos de un tutorial de 2015. Mídelos en tu hardware de producción apuntando a 50–200 ms, revisa la política cada pocos años y deja que
check_needs_rehashpropague la subida. - Migrar "cuando haya tiempo" dejando
login_MALen producción. La migración en el login cuesta una tarde de trabajo y se paga sola en la primera brecha. Prioridad alta, de verdad. - Consejo: añade a tu registro (y a tus tests) contraseñas del top de RockYou y verifica que tu capa de aplicación las rechaza. El mejor Argon2id no salva a
medinube2026de un diccionario de diez mil entradas.
Ejercicios
Ejercicio 1. La tabla de usuarios de un servicio filtrado contiene estas tres filas (misma columna hash). Indica en qué etapa de la evolución está cada esquema, qué ataque las afecta y ordénalas de peor a mejor:
alice | 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 bob | a1b2c3d4e5f6a7b8:9c87b1cbb62d5a7ac43276b8b12ab5a4adcb1e83e5c2e6f1... carol | $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub...
Ejercicio 2. Sin ejecutarlo, razona qué imprime cada línea y por qué:
from argon2 import PasswordHasher
ph = PasswordHasher()
h1 = ph.hash("gato-azul-7")
h2 = ph.hash("gato-azul-7")
print(h1 == h2)
print(ph.verify(h1, "gato-azul-7"), ph.verify(h2, "gato-azul-7"))
ph2 = PasswordHasher(memory_cost=131072)
print(ph2.check_needs_rehash(h1))Ejercicio 3. MediNube absorbe una clínica cuyo sistema guardaba contraseñas con bcrypt, factor de coste 10 (formato $2b$10$..., paquete bcrypt: bcrypt.checkpw(password_bytes, hash_bytes)). Escribe la rama adicional de la función login que verifique esos hashes y los migre a Argon2id en el primer login correcto. ¿Hace falta tabla nueva o columna nueva de salt?
Soluciones
Solución 1. alice: 64 hex = SHA-256 a secas, etapa 1 (de hecho es sha256("password")): vulnerable a rainbow tables y a diccionario a máxima velocidad; los hashes repetidos entre usuarios delatan contraseñas compartidas. bob: formato salt:hash, etapa 2 (hash rápido con salt): inmune al precálculo, pero cada fila cae a ritmo de GPU con un diccionario. carol: formato modular Argon2id, etapa 3: cada intento cuesta 64 MiB y ~decenas de ms también al atacante. De peor a mejor: alice < bob < carol. Detalle de examen: el salt visible en las filas de bob y carol no es una debilidad.
Solución 2. h1 == h2 → False: cada hash() genera un salt fresco del CSPRNG, así que la misma contraseña produce hashes distintos (y eso es deseable: no delata contraseñas repetidas). Ambos verify → True True: cada hash lleva su salt dentro, y la verificación re-deriva con él. ph2.check_needs_rehash(h1) → True: h1 declara m=65536 en su formato modular, por debajo de la política de ph2 (131072 KiB); en un login real eso dispararía el re-hash transparente.
Solución 3. Basta una rama más; ni tabla ni columna de salt — bcrypt también usa formato modular con el salt dentro ($2b$10$<salt><hash>):
import bcrypt
# ... dentro de login(), tras buscar la fila:
if fila.esquema == "bcrypt":
if not bcrypt.checkpw(password.encode("utf-8"), fila.hash.encode("utf-8")):
return False
# correcta: migrar a la politica actual de MediNube
bd.guardar(usuario, esquema="argon2", hash=ph.hash(password))
return TruePuntos a valorar: checkpw exige bytes (de ahí los .encode), ya compara en tiempo constante internamente, y la migración reutiliza el momento en que la contraseña está legítimamente en memoria. Recordatorio del límite de bcrypt: si el portal permite frases de paso de más de 72 bytes, los hashes $2b$ heredados solo verificaron los primeros 72 — un motivo más para completar la migración (Argon2id no trunca).
Conclusión
La deuda más antigua del curso está saldada: el portal de MediNube guarda verificadores Argon2id ($argon2id$..., con salt y parámetros dentro), registra con tres líneas de PasswordHasher, verifica con verify + VerifyMismatchError, migra los SHA-256 heredados en el primer login y se re-endurece solo con check_needs_rehash. Y quedó clara la distinción con 02-04: misma familia de funciones lentas, pero allí la salida era una clave que cifra y aquí un verificador que solo comprueba.
Con esto se cierra el Módulo 3 completo, y las tres piezas encajan en una escalera: el hash (03-01) da integridad cuando la huella vive a salvo — exports verificables, deduplicación, git; el HMAC (03-02) añade una clave compartida y da autenticidad — webhooks firmados con X-MediNube-Firma, tokens de recuperación guardados como sha256(token); y Argon2id (03-03) invierte la virtud de la velocidad para proteger el único secreto que no podemos elegir fuerte: las contraseñas humanas. Integridad, autenticidad, credenciales: las tres sin cifrar un solo byte.
Pero repasa la letra pequeña de todo el arsenal que MediNube tiene hasta hoy: AES-GCM necesita que emisor y receptor ya compartan la clave; HMAC, también; hasta el pepper era otro secreto compartido. Todo nuestro edificio descansa en una suposición que llevamos aparcando desde 02-01: que las claves llegaron a ambos lados por arte de magia. ¿Cómo acuerdan una clave MediNube y una clínica nueva que jamás se han visto, hablando por una red donde cualquiera escucha? ¿Y cómo firma un médico una receta de modo que cualquiera — no solo quien comparte una clave — pueda verificarla, con no repudio incluido? Ese es el salto conceptual más grande del curso: la criptografía asimétrica, donde las claves vienen en pares y la mitad de ellas se publica a los cuatro vientos. Empieza en el Módulo 4 con la lección 04-01: Fundamentos de Clave Pública y RSA. Nos vemos al otro lado.
Curso de Criptografía Aplicada
Módulo 1: Fundamentos de la Criptografía
- Qué es la criptografía y para qué sirve
- Codificación, ofuscación y cifrado
- Aleatoriedad y entropía
- El principio de Kerckhoffs y las reglas de oro
Módulo 2: Criptografía Simétrica
- Cifrado simétrico: AES y ChaCha20
- Modos de operación
- Cifrado autenticado (AEAD)
- Derivación de claves (KDF)
Módulo 3: Hashes, MAC y Contraseñas
- Funciones hash criptográficas
- Autenticación de mensajes con HMAC
- Almacenamiento seguro de contraseñas
Módulo 4: Criptografía Asimétrica
- Fundamentos de clave pública y RSA
- Criptografía de curva elíptica
- Firmas digitales
- Intercambio de claves: Diffie-Hellman
- Cifrado híbrido
Módulo 5: PKI, Certificados y TLS
- Certificados X.509 y autoridades de certificación
- TLS en la práctica
- Gestión del ciclo de vida de certificados
