Llegamos a la última lección del módulo, y es la que ata todos los cabos. A lo largo del curso has acumulado dos familias de herramientas con virtudes opuestas: la criptografía simétrica (AES-GCM, ChaCha20) es rapidísima y cifra gigabytes, pero exige una clave compartida que no sabíamos distribuir; la criptografía asimétrica (RSA, ECC) resuelve la distribución de claves, pero es lenta y solo maneja mensajes diminutos. Cada una cojea justo donde la otra brilla. La solución no es elegir: es combinarlas. El patrón se llama cifrado híbrido o sobre digital (envelope encryption), y es, sin exagerar, cómo se cifra de verdad en el mundo real —desde TLS hasta el cifrado de tus copias de seguridad en la nube. En esta lección lo construirás entero: MediNube enviará un export completo de historiales a la Clínica Sol, y verás las dos formas de "cerrar el sobre" (RSA-OAEP clásico y el patrón moderno con X25519 efímero). Cerraremos el módulo con un mapa de decisión de todas las primitivas asimétricas y con la única pieza que aún nos falta.

Contenido

  1. El dilema, recapitulado en una tabla
  2. La idea del sobre digital
  3. Sobre digital con RSA-OAEP (variante clásica)
  4. Sobre digital con X25519 efímero (variante moderna, ECIES-like)
  5. El formato de sobre versionado de MediNube
  6. Dónde vive este patrón en el mundo real
  7. Mapa de decisión del módulo

El dilema, recapitulado en una tabla

Pongamos cara a cara lo que sabemos, porque el híbrido nace exactamente de esta tensión:

Simétrica (AES-256-GCM) Asimétrica (RSA-OAEP / ECC)
Velocidad Gigabytes por segundo (AES-NI) Miles de operaciones/s: miles de veces más lenta
Tamaño de mensaje En la práctica, ilimitado RSA: ≤318 bytes; ECC: no cifra datos directamente
Distribución de claves No resuelta: exige secreto compartido previo Resuelta: la pública se publica sin miedo
Punto fuerte Cifrar los datos Hacer llegar una clave
Punto débil ¿Cómo llega la clave? ¿Cómo cifro algo grande y rápido?

Lee la última fila y verás que las debilidades encajan como piezas de puzle: lo que la simétrica no sabe hacer (distribuir la clave), la asimétrica lo hace de sobra; lo que la asimétrica no sabe hacer (cifrar datos grandes y rápido), la simétrica lo hace de sobra. La conclusión que ya anticipábamos en 04-01 —"RSA cifra claves, no datos"— se convierte aquí en una arquitectura.

La idea del sobre digital

El cifrado híbrido usa cada primitiva para lo que sabe hacer:

  1. Se genera una clave de sesión simétrica aleatoria (una clave AES-256 fresca, del CSPRNG —regla de oro 3), de usar y tirar.
  2. Se cifran los datos (grandes) con esa clave usando AES-256-GCM: rápido y sin límite de tamaño.
  3. Se cifra solo la clave de sesión (32 bytes) con la criptografía asimétrica, usando la clave pública del destinatario. Ahí la lentitud y el límite de tamaño de la asimétrica no molestan: 32 bytes caben de sobra y se cifran una sola vez.
  4. Se envía el sobre: la clave de sesión cifrada + los datos cifrados, juntos.

El destinatario hace el camino inverso: con su clave privada abre el sobre y recupera la clave de sesión; con ella descifra los datos.

flowchart TD
    subgraph Emisor["MediNube (emisor)"]
        K["Clave de sesión AES-256 ALEATORIA"] --> C1["AES-256-GCM cifra el EXPORT (grande, rápido)"]
        PUB["Pública de la Clínica Sol"] --> C2["Cifra SOLO la clave de sesión (32 bytes)"]
        K --> C2
    end
    C2 --> S["SOBRE: clave cifrada + datos cifrados"]
    C1 --> S
    S -->|canal público| Dest
    subgraph Dest["Clínica Sol (destinatario)"]
        PRIV["Su PRIVADA"] --> D1["Abre el sobre -> recupera la clave de sesión"]
        D1 --> D2["AES-256-GCM descifra el export"]
    end

La belleza del patrón: obtienes la confidencialidad frente a un destinatario con clave pública (nadie más puede abrir el sobre) con la velocidad de la simétrica (los datos van con AES). Y como la clave de sesión es fresca en cada envío, dos exports idénticos producen sobres completamente distintos. Solo falta decidir cómo se cierra el sobre —el paso 3— y aquí hay dos escuelas.

Sobre digital con RSA-OAEP (variante clásica)

Si el destinatario tiene una clave RSA (como el hospital público de la integración de MediNube), cerrar el sobre es literalmente lo que aprendiste en 04-01: cifrar los 32 bytes de la clave de sesión con RSA-OAEP.

import os
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

OAEP = padding.OAEP(mgf=padding.MGF1(hashes.SHA256()),
                    algorithm=hashes.SHA256(), label=None)

def cifrar_hibrido_rsa(datos: bytes, aad: bytes, pub_rsa) -> dict:
    """Sobre digital: AES-GCM para los datos, RSA-OAEP para la clave."""
    clave_sesion = AESGCM.generate_key(bit_length=256)   # aleatoria, de un solo uso
    nonce = os.urandom(12)                               # nonce fresco (módulo 2)
    datos_cifrados = AESGCM(clave_sesion).encrypt(nonce, datos, aad)
    clave_cifrada = pub_rsa.encrypt(clave_sesion, OAEP)  # solo 32 bytes por RSA
    return {"esquema": "rsa-oaep", "clave_cifrada": clave_cifrada,
            "nonce": nonce, "datos_cifrados": datos_cifrados}

def descifrar_hibrido_rsa(sobre: dict, aad: bytes, priv_rsa) -> bytes:
    clave_sesion = priv_rsa.decrypt(sobre["clave_cifrada"], OAEP)  # abre el sobre
    return AESGCM(clave_sesion).decrypt(sobre["nonce"], sobre["datos_cifrados"], aad)

# --- Prueba con un export "grande" ---
priv = rsa.generate_private_key(public_exponent=65537, key_size=3072)
export = b"HISTORIALES CLINICA SOL\n" + b"ana.perez;A+;penicilina\n" * 5000
aad = b"destino=clinica-sol;formato=v1"

sobre = cifrar_hibrido_rsa(export, aad, priv.public_key())
print(len(export), "bytes de datos ->", len(sobre["clave_cifrada"]), "bytes de clave cifrada")
assert descifrar_hibrido_rsa(sobre, aad, priv) == export

Fíjate en el punto clave: export puede pesar megabytes y a RSA no le importa, porque RSA solo toca los 32 bytes de la clave de sesión. El límite de 318 bytes de 04-01 deja de ser un problema. Esto es "RSA cifra claves, no datos" hecho código.

Sobre digital con X25519 efímero (variante moderna, ECIES-like)

¿Y si el destinatario tiene una clave de curva elíptica (X25519), que —como viste en 04-04— no cifra directamente? Se combina el intercambio de claves de la lección anterior con el sobre: el emisor genera un par X25519 efímero, hace ECDH contra la pública estática del destinatario, deriva la clave de sesión con HKDF, y adjunta su pública efímera al sobre. El destinatario reconstruye el mismo secreto con su privada y la pública efímera recibida. Este patrón tiene nombre propio: ECIES (Elliptic Curve Integrated Encryption Scheme), y hereda gratis la forward secrecy de 04-04.

from cryptography.hazmat.primitives.asymmetric import x25519
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

def _derivar(secreto: bytes) -> bytes:
    return HKDF(algorithm=hashes.SHA256(), length=32, salt=None,
                info=b"medinube:sobre-hibrido:v1").derive(secreto)

def cifrar_hibrido_x25519(datos: bytes, aad: bytes, pub_dest: x25519.X25519PublicKey) -> dict:
    efimera = x25519.X25519PrivateKey.generate()          # par EFÍMERO de un solo uso
    clave_sesion = _derivar(efimera.exchange(pub_dest))   # ECDH + HKDF (04-04)
    nonce = os.urandom(12)
    datos_cifrados = AESGCM(clave_sesion).encrypt(nonce, datos, aad)
    return {"esquema": "x25519-ecies",
            "pub_efimera": efimera.public_key().public_bytes_raw(),  # viaja EN CLARO
            "nonce": nonce, "datos_cifrados": datos_cifrados}

def descifrar_hibrido_x25519(sobre: dict, aad: bytes, priv_dest: x25519.X25519PrivateKey) -> bytes:
    pub_efimera = x25519.X25519PublicKey.from_public_bytes(sobre["pub_efimera"])
    clave_sesion = _derivar(priv_dest.exchange(pub_efimera))   # mismo secreto
    return AESGCM(clave_sesion).decrypt(sobre["nonce"], sobre["datos_cifrados"], aad)

# --- Prueba ---
priv_clinica = x25519.X25519PrivateKey.generate()
export = b"HISTORIALES\n" + b"ana.perez;A+\n" * 5000
aad = b"destino=clinica-sol;formato=v1"
sobre = cifrar_hibrido_x25519(export, aad, priv_clinica.public_key())
assert descifrar_hibrido_x25519(sobre, aad, priv_clinica) == export

Diferencias frente a la variante RSA, que conviene tener claras:

RSA-OAEP (clásico) X25519 efímero / ECIES (moderno)
Qué viaja para "abrir el sobre" La clave de sesión cifrada con la pública La pública efímera del emisor (en claro)
Cómo obtiene la clave el destinatario La descifra con su privada La re-deriva por ECDH + HKDF
Forward secrecy No (si roban la privada RSA, se abren todos los sobres pasados) (par efímero por sobre, se descarta)
Tamaño extra del sobre 384 bytes (clave cifrada, RSA-3072) 32 bytes (pública efímera)
Cuándo usarlo Destinatario con clave RSA / requisito heredado Elección moderna por defecto

En ECIES no se "cifra" la clave de sesión: se deriva de un intercambio, y lo único que viaja es la pública efímera —inofensiva, como toda pública. Por eso da forward secrecy casi gratis, la misma propiedad que hacía valiosas las claves efímeras en 04-04.

El formato de sobre versionado de MediNube

Los diccionarios de ejemplo están bien para aprender, pero para enviar el sobre por un canal o guardarlo en disco hace falta un formato de bytes concreto y versionado, coherente con el estilo v1 que arrastramos desde el módulo 2 (versión || ...). Así, medinube/sobre.py puede evolucionar sin romper sobres antiguos (criptoagilidad, regla de oro 8):

# medinube/sobre.py  — formato de sobre híbrido versionado
#
#   byte 0      : versión de formato
#     0x01  -> RSA-OAEP  : 0x01 || clave_cifrada(384) || nonce(12) || datos_cifrados
#     0x02  -> X25519    : 0x02 || pub_efimera(32)    || nonce(12) || datos_cifrados
#
# Los datos van con AES-256-GCM (tag incluido). La AAD del curso identifica destino.

def empaquetar_x25519(sobre: dict) -> bytes:
    return b"\x02" + sobre["pub_efimera"] + sobre["nonce"] + sobre["datos_cifrados"]

def desempaquetar_x25519(blob: bytes, aad: bytes, priv_dest) -> bytes:
    if blob[0] != 0x02:
        raise ValueError(f"versión de sobre no soportada: {blob[0]:#x}")
    sobre = {"pub_efimera": blob[1:33], "nonce": blob[33:45], "datos_cifrados": blob[45:]}
    return descifrar_hibrido_x25519(sobre, aad, priv_dest)

# El export de historiales de ana.perez, listo para viajar como un solo blob:
blob = empaquetar_x25519(cifrar_hibrido_x25519(export, aad, priv_clinica.public_key()))
print(blob[0], len(blob))          # 2  <longitud total>
assert desempaquetar_x25519(blob, aad, priv_clinica) == export

El primer byte manda: quien recibe el sobre lee la versión y sabe exactamente cómo interpretarlo. El día que MediNube añada un esquema post-cuántico (06-05), será 0x03; los sobres 0x01 y 0x02 seguirán descifrándose sin tocar nada. Es la misma disciplina del formato v1 de AES-GCM (02-03) y de la firma v1=<hex> de las recetas (04-03): la versión, siempre delante.

Recordatorio de compliance. Un export de historiales es dato sanitario de categoría especial. Este código muestra la mecánica criptográfica con datos ficticios; un despliegue real exige revisión de seguridad y de protección de datos (RGPD) —igual que advertimos con las recetas.

Dónde vive este patrón en el mundo real

El cifrado híbrido no es un truco de este curso: es el patrón dominante. Casi cualquier cosa cifrada que uses a diario es un sobre digital:

  • TLS / HTTPS (módulo 5, lección 05-02): cada visita a https://portal.medinube.example negocia una clave de sesión simétrica mediante intercambio asimétrico (ECDHE) y cifra el tráfico con AES-GCM o ChaCha20-Poly1305. Es exactamente lo de esta lección, con autenticación por certificado.
  • PGP/GPG y age: cifrado de ficheros y correo. Generan una clave de sesión por mensaje y la protegen con la pública del destinatario —sobre digital de manual. age es la versión moderna y minimalista, construida sobre X25519 como tu variante ECIES.
  • KMS con envelope encryption (anticipo de 06-01): los servicios de gestión de claves en la nube cifran tus datos con una "clave de datos" (DEK) simétrica y, a su vez, cifran esa DEK con una "clave maestra" (KEK) que nunca sale del servicio. El vocabulario cambia (DEK/KEK), el patrón es idéntico: cifra los datos con una clave simétrica, protege esa clave con otra criptografía. Volverás a verlo en el módulo 6.
  • Mensajería E2E (Signal, WhatsApp): usan variantes muy sofisticadas (el protocolo de doble ratchet) que van más allá del sobre simple, pero su cimiento es el mismo —X25519 para acordar claves, simétrica para los mensajes. Solo lo mencionamos; su diseño da para un curso propio.

Que reconozcas el patrón bajo todos esos nombres es una de las metas del curso: no son diez tecnologías distintas, es una idea aplicada diez veces.

Mapa de decisión del módulo

Cierra los ojos y repasa el módulo entero: ¿qué primitiva asimétrica usar según lo que necesites? Esta tabla es el resumen operativo de las cinco lecciones:

Necesito... Objetivo (módulo 1) Primitiva Lección
Cifrar hacia alguien un dato pequeño (una clave) Confidencialidad RSA-OAEP 04-01
Cifrar hacia alguien datos grandes Confidencialidad Híbrido: AES-GCM + (RSA-OAEP o X25519/ECIES) 04-05
Firmar algo verificable por cualquiera, con no repudio Autenticidad + no repudio Ed25519 (o RSA-PSS) 04-03
Acordar una clave por un canal público (habilita confidencialidad) ECDH con X25519 (+ HKDF) 04-04
Claves pequeñas y rápidas para todo lo anterior Curva elíptica (Ed25519 / X25519) 04-02

Regla de bolsillo para elegir de memoria: cifrar-hacia-alguien → híbrido (sobre); demostrar quién lo emitió → firma (Ed25519); ponerse de acuerdo en una clave → intercambio (X25519). Tres verbos, tres herramientas. Y todas, por debajo, apoyándose en la simétrica y los hashes de los módulos 2 y 3: nada se ha tirado, todo se ha combinado.

Errores Comunes y Consejos

  • Trocear datos grandes con RSA en vez de usar el híbrido. El antipatrón que ya avisábamos en 04-01: cifrar un fichero en bloques de 318 bytes con RSA es lentísimo y criptográficamente frágil. El sobre digital es la respuesta —una clave de sesión, un cifrado AES.
  • Reutilizar la clave de sesión entre mensajes. Debe ser aleatoria y de un solo uso por sobre. Reutilizarla reintroduce el determinismo (dos exports iguales darían cifrados iguales) y arriesga la regla del nonce de AES-GCM.
  • Olvidar la AAD del sobre. Ata el cifrado a su contexto (destino, formato) igual que en 02-03. Sin AAD, un atacante podría reencaminar un sobre válido a otro destinatario o formato sin que el descifrado lo detecte.
  • Guardar el sobre sin versión. El byte de versión delante no es decorativo: es lo que permite añadir ECIES o post-cuántica mañana sin romper los sobres de hoy. Formato sin versionar = migración imposible.
  • Creer que el sobre autentica al emisor. El cifrado híbrido da confidencialidad, no autenticidad: cualquiera con tu pública puede fabricarte un sobre. Si necesitas saber quién lo envió, hay que firmar además de cifrar (04-03) —firmar-luego-cifrar, o esquemas combinados; no lo da el sobre solo.
  • Usar la variante RSA y esperar forward secrecy. RSA-OAEP no la tiene: si un día roban la privada RSA, todos los sobres pasados grabados se abren. Si la forward secrecy importa, X25519 efímero.
  • Y el de siempre: dar por buena la clave pública del destinatario. Todo el híbrido presupone que la pública de la Clínica Sol es de verdad de la Clínica Sol. No lo sabemos aún —es la deuda que cierra el módulo, justo abajo.

Ejercicios

  1. Sobre versionado de ida y vuelta. Usando cifrar_hibrido_x25519 y el formato de medinube/sobre.py: cifra el export b"HISTORIAL ana.perez\n" * 1000 con AAD b"destino=clinica-sol;formato=v1", empaquétalo a un blob de bytes, imprime su primer byte y su longitud, y desempaquétalo comprobando que recuperas el original. Después, cambia un byte de los datos cifrados del blob y comprueba qué excepción salta al desempaquetar y por qué (pista: AES-GCM, módulo 2).

  2. Los dos sobres, comparados. Cifra el mismo export con la variante RSA-OAEP y con la X25519. (a) Compara el tamaño del material que viaja para "abrir el sobre" en cada caso. (b) Cifra dos veces el mismo export con la variante X25519 y comprueba que las públicas efímeras (y por tanto los sobres) son distintas. (c) Explica qué propiedad de seguridad da esa diferencia y cuál de las dos variantes la tiene.

  3. Confidencialidad no es autenticidad. MediNube recibe un sobre híbrido X25519 que descifra sin error y contiene una orden: "dar de alta a un nuevo médico". Razona (sin código): (a) ¿demuestra el descifrado correcto que el sobre lo envió la Clínica Sol? (b) Si no, ¿quién ha podido fabricarlo? (c) ¿Qué primitiva de este módulo habría que añadir para que MediNube pudiera confiar en el emisor, y qué problema pendiente reaparecería al hacerlo?

Soluciones

export = b"HISTORIAL ana.perez\n" * 1000
aad = b"destino=clinica-sol;formato=v1"
blob = empaquetar_x25519(cifrar_hibrido_x25519(export, aad, priv_clinica.public_key()))
print(blob[0], len(blob))                       # 2  <longitud>
assert desempaquetar_x25519(blob, aad, priv_clinica) == export

roto = blob[:-1] + bytes([blob[-1] ^ 1])        # alteramos un byte del final (datos)
try:
    desempaquetar_x25519(roto, aad, priv_clinica)
except Exception as e:
    print(type(e).__name__)                     # InvalidTag

Salta InvalidTag: el tag de autenticación de AES-256-GCM (módulo 2) detecta que un byte del cifrado ha cambiado y se niega a descifrar. El sobre híbrido hereda la integridad del AEAD interior: no solo es confidencial, también detecta manipulación de los datos.

priv_rsa = rsa.generate_private_key(public_exponent=65537, key_size=3072)
s_rsa = cifrar_hibrido_rsa(export, aad, priv_rsa.public_key())
s_ec1 = cifrar_hibrido_x25519(export, aad, priv_clinica.public_key())
s_ec2 = cifrar_hibrido_x25519(export, aad, priv_clinica.public_key())

print(len(s_rsa["clave_cifrada"]))               # (a) 384 bytes
print(len(s_ec1["pub_efimera"]))                 # (a) 32 bytes
print(s_ec1["pub_efimera"] != s_ec2["pub_efimera"])   # (b) True: efímeras distintas

(a) RSA hace viajar 384 bytes (la clave de sesión cifrada); X25519, solo 32 (la pública efímera). (b) Cada cifrado X25519 genera un par efímero nuevo, así que las públicas —y los sobres enteros— difieren aunque el export sea idéntico. (c) Esa renovación por sobre da forward secrecy: la tiene la variante X25519 efímera, no la RSA-OAEP (cuya única privada, si se roba, abre todos los sobres pasados).

  1. (a) No. El cifrado híbrido solo prueba que alguien con la clave pública de MediNube fabricó el sobre —y esa clave es pública. Descifrar bien demuestra confidencialidad e integridad de los datos, no la identidad del emisor. (b) Cualquiera: la pública de MediNube está publicada, así que un atacante puede fabricar un sobre perfectamente válido con la orden que quiera. (c) Habría que firmar el contenido con la clave privada del emisor (Ed25519, 04-03) y que MediNube verifique con la pública de la Clínica: eso añade autenticidad y no repudio. Pero al hacerlo reaparece la pregunta del módulo: ¿cómo sabe MediNube que esa clave pública de verificación es de verdad de la Clínica Sol? La respuesta —certificados y autoridades de confianza— es el módulo 5.

Conclusión

Has cerrado el círculo. El cifrado híbrido combina lo mejor de las dos familias del curso: una clave de sesión simétrica aleatoria cifra los datos con AES-256-GCM (rápido, sin límite de tamaño), y la criptografía asimétrica protege solo esa clave (donde su lentitud y su límite no importan). Lo montaste en sus dos formas —RSA-OAEP clásico y el patrón moderno X25519 efímero (ECIES), este último con forward secrecy de regalo—, en un formato de sobre versionado (0x01/0x02, la versión siempre delante) coherente con todo el curso, y reconociste el mismo patrón latiendo bajo TLS, PGP/age, el envelope encryption de los KMS y la mensajería E2E. Con esto termina el módulo 4 y, con él, tu caja de herramientas queda completa: tienes la simétrica (módulo 2) para cifrar rápido, los hashes y MAC (módulo 3) para integridad y autenticidad con secreto compartido, y la asimétrica (este módulo) para firmar, acordar claves y cifrar hacia desconocidos sin secreto previo. Puedes cifrar, firmar, verificar y acordar claves por un canal público. Y sin embargo, lección tras lección —04-01, 04-03, 04-04, y ahora mismo en el último ejercicio— ha reaparecido siempre la misma grieta: toda la matemática es impecable, pero descansa sobre una suposición sin verificar, que la clave pública que tienes es de quien crees. MalloryClinic sigue ahí fuera, esperando a que alguien confíe en una clave sin comprobarla. Nos faltan primitivas nuevas, no: nos falta confianza —una forma de saber, con respaldo verificable, de quién es cada clave pública. Esa es la única pieza que queda, y es justo lo que construye el Módulo 5: PKI, Certificados y TLS, empezando por los certificados X.509 y las autoridades de certificación en la lección 05-01. Allí, por fin, cerraremos la grieta que llevamos arrastrando todo el módulo. Nos vemos en el módulo 5.

© Copyright 2026. Todos los derechos reservados