La lección anterior te dejó con un mapa de primitivas y una promesa: empezar por la más antigua y la más usada, la que emplea una única clave compartida para cifrar y descifrar. Es la que mueve el 99 % de los bytes cifrados del planeta —el contenido de cada conexión TLS, cada disco cifrado, cada copia de seguridad de Nimbus— porque es rápida, sencilla de razonar y está resuelta desde hace décadas. Esta lección explica por qué AES es el estándar, qué es un modo de operación y por qué elegirlo mal arruina un algoritmo intachable, qué es un nonce y por qué repetirlo es una catástrofe, y cómo se cifra correctamente un adjunto de Nimbus con AES-GCM. Termina en el punto exacto donde lo simétrico se queda sin respuestas: cómo hacer llegar la clave al otro extremo.

Contenido

  1. Qué es la criptografía simétrica y cuándo se usa
  2. Cifrado de bloque y cifrado de flujo
  3. AES: por qué es el estándar
  4. Modos de operación: por qué un bloque no basta
  5. ECB, CBC y CTR: el catálogo clásico y sus trampas
  6. Cifrado autenticado (AEAD) y datos adicionales autenticados
  7. Cifrar un adjunto de Nimbus con AES-GCM
  8. La regla del nonce
  9. Cifrar una copia de seguridad con openssl desde la línea de órdenes
  10. Derivación de claves: HKDF
  11. Rendimiento y sus consecuencias de diseño
  12. El problema que queda abierto: distribuir la clave

  1. Qué es la criptografía simétrica y cuándo se usa

En un esquema simétrico, la misma clave sirve para cifrar y para descifrar. Emisor y receptor —o, muy a menudo, la misma máquina en dos momentos distintos— comparten ese secreto.

flowchart LR
    P["TEXTO EN CLARO\nadjunto de la clinica"] --> C["CIFRAR\nAES-GCM"]
    K["CLAVE UNICA\n256 bits"] --> C
    C --> X["TEXTO CIFRADO\n+ nonce + etiqueta"]
    X --> D["DESCIFRAR\nAES-GCM"]
    K --> D
    D --> P2["TEXTO EN CLARO\nrecuperado"]

Los escenarios donde es la respuesta natural comparten un rasgo: la clave ya está disponible en los dos extremos, sea porque solo hay un extremo o porque otro mecanismo la puso allí.

Escenario en Nimbus Por qué es simétrico
Cifrado de adjuntos en el bucket A-05 Cifra y descifra el mismo sistema, con la clave del gestor de secretos
Copias de seguridad cifradas (02-04) Se cifran hoy y se descifran meses después: un único poseedor de la clave
Cifrado en reposo de PostgreSQL y de los discos Transparente, alto volumen, alta velocidad
Contenido de una sesión TLS La clave la acordaron antes por medios asimétricos (03-03, 03-05)
Notas clínicas cifradas campo a campo Cifra y descifra la API; ver 03-07

Sus dos ventajas son la velocidad —órdenes de magnitud sobre la asimétrica, con instrucciones específicas en los procesadores actuales— y las claves cortas: 256 bits bastan para el muy largo plazo. Su única gran carencia es la distribución: si dos partes que nunca se han visto necesitan una clave común, lo simétrico solo no puede dársela; y el número de claves crece de forma cuadrática, porque n partes que quieran hablar por parejas necesitan n·(n−1)/2 claves (con 38 empleados, 703).


  1. Cifrado de bloque y cifrado de flujo

Hay dos maneras de construir un cifrador simétrico:

Cifrado de bloque Cifrado de flujo
Cómo opera Sobre trozos de tamaño fijo (128 bits en AES) Genera un chorro de bytes pseudoaleatorios y los combina con el texto (XOR)
Necesita relleno Sí, en algunos modos No: el cifrado tiene el mismo tamaño que el claro
Ejemplos vigentes AES ChaCha20
Ejemplos prohibidos DES, 3DES (obsoletos) RC4 (roto)
Riesgo característico Un modo mal elegido revela estructura Reutilizar el flujo con la misma clave y nonce es fatal

La frontera entre ambos es más difusa de lo que parece: un cifrador de bloque en modo CTR se comporta como uno de flujo, porque en lugar de cifrar los datos genera un chorro que luego se combina con ellos. Esa es exactamente la idea sobre la que se construye AES-GCM. Y ambos comparten la regla que arrastramos desde Vigenère: el mismo material de clave no se usa dos veces. En un cifrado de flujo esto es literal: si ciframos dos mensajes distintos con el mismo chorro, un XOR entre los dos cifrados elimina el chorro y deja los dos claros combinados entre sí, lo que suele bastar para recuperarlos.


  1. AES: por qué es el estándar

AES (Advanced Encryption Standard) es un cifrador de bloque adoptado por el NIST en 2001 tras un concurso público internacional de cinco años en el que participaron quince candidatos, con criptoanálisis abierto de toda la comunidad. Ganó el algoritmo belga Rijndael. Esa historia es la mejor ilustración de Kerckhoffs que existe: el algoritmo se hizo fuerte precisamente por ser público y por haber sido atacado durante décadas sin éxito práctico.

Característica Valor
Tamaño de bloque 128 bits (16 bytes), siempre
Tamaños de clave 128, 192 o 256 bits
Rondas 10, 12 o 14 según la clave
Estado en 2026 Sin ataques prácticos. Aceptado para información clasificada
Aceleración hardware Sí: instrucciones AES-NI en x86 y equivalentes en ARM

AES-128 frente a AES-256. Ambos están fuera del alcance de cualquier fuerza bruta (recuerda el cálculo de 03-01: 2^128 son once billones de años con una máquina imposible). AES-256 se elige cuando el dato debe seguir confidencial durante décadas, cuando una norma sectorial lo exige o cuando se quiere margen frente a la amenaza cuántica a largo plazo. En Nimbus usaremos 256 porque el coste de rendimiento es marginal y la conversación con auditores se simplifica. ChaCha20 es la alternativa moderna: un cifrador de flujo muy rápido en software puro, habitual en dispositivos móviles sin aceleración AES —y por tanto relevante para la app de Nimbus—; combinado con el autenticador Poly1305 da ChaCha20-Poly1305, un AEAD de la misma categoría que AES-GCM.

El bloque de 128 bits y el problema que crea

AES cifra exactamente 16 bytes. Un adjunto de una clínica puede pesar 3 MB. ¿Cómo se cifran 3 MB con una función que solo sabe manejar 16 bytes?

La respuesta es un modo de operación: el conjunto de reglas que dice cómo trocear el mensaje, cómo encadenar los bloques y cómo rellenar el último. Y aquí está la idea más importante de la lección:

AES es intachable; los modos son donde la gente se equivoca. Prácticamente todos los fallos de cifrado simétrico que verás en tu carrera son fallos de modo, de nonce o de gestión de clave. Nunca del cifrador.


  1. Modos de operación: por qué un bloque no basta

Para entender el problema, imagina que ciframos cada bloque de 16 bytes por separado con la misma clave y sin más. Como AES es determinista, dos bloques de texto claro idénticos producen dos bloques cifrados idénticos. La estructura del original sobrevive al cifrado.

La demostración clásica es la del pingüino: si se toma la imagen de un pingüino y se cifra en modo ECB, el resultado sigue siendo, visiblemente, un pingüino. Los colores cambian, pero las zonas de un mismo tono uniforme se convierten en zonas de otro tono uniforme, y la silueta se reconoce sin ningún esfuerzo. Ese es el sentido literal de «el cifrado no debe conservar estructura».

Trasladado a Nimbus: si se cifrasen en ECB los registros de citas, todas las filas cuyo campo estado valga CANCELADA tendrían el mismo bloque cifrado. Sin descifrar nada, un observador contaría cancelaciones por clínica y detectaría patrones. La confidencialidad se pierde sin romper AES.

Lo que hace falta es que el mismo texto claro cifrado dos veces produzca resultados distintos. Ese es el papel del IV (vector de inicialización) o del nonce: un valor variable que se combina con el proceso para que cada cifrado sea único.

Término Significado Requisito
IV Valor de inicialización, en modos como CBC Impredecible (aleatorio) y único
Nonce Number used once, en CTR y GCM Único por clave; no necesita ser impredecible, pero sí irrepetible
Etiqueta (tag) Código de autenticación producido por un AEAD Se verifica al descifrar; si falla, no se devuelve nada

Ni el IV ni el nonce son secretos: se almacenan junto al texto cifrado, en claro. Lo secreto es únicamente la clave.


  1. ECB, CBC y CTR: el catálogo clásico y sus trampas

Modo ¿Confidencialidad? ¿Integridad? ¿Paralelizable? Riesgo principal
ECB No de verdad No Conserva patrones. Prohibido siempre
CBC Sí, con IV impredecible No Solo al descifrar Oráculo de relleno; IV predecible; maleabilidad
CTR No Sí (cifrar y descifrar) Reutilizar el nonce es catastrófico
GCM (AEAD) Reutilizar el nonce rompe además la autenticación
ChaCha20-Poly1305 (AEAD) Parcialmente Reutilizar el nonce

Detalle de cada uno:

  • ECB (Electronic Codebook). Cada bloque se cifra independientemente. Es el pingüino. No hay ningún caso de uso legítimo en un sistema de información. Si lo encuentras en el código, es un hallazgo de auditoría, no una preferencia de estilo.
  • CBC (Cipher Block Chaining). Cada bloque se combina por XOR con el cifrado del anterior antes de cifrarse; el primero, con el IV. Resuelve los patrones, pero arrastra tres inconvenientes: necesita relleno —lo que históricamente abrió los ataques de padding oracle, en los que el atacante descifra sin la clave observando cómo responde el servidor a rellenos inválidos—, exige un IV impredecible (uno predecible habilita ataques de texto claro elegido), y no da integridad: un atacante puede alterar bits del cifrado y provocar cambios controlados en el claro.
  • CTR (Counter). Convierte AES en un cifrador de flujo: se cifra un contador creciente y el resultado se combina por XOR con los datos. Rápido, paralelizable y sin relleno. Pero si se repite el par (clave, nonce), se repite el chorro y el atacante recupera los claros. Tampoco da integridad.

La lectura de la tabla es contundente: ningún modo clásico protege la integridad. Y sin integridad, la confidencialidad es frágil, porque un atacante que puede modificar el texto cifrado y observar el comportamiento del sistema tiene un camino hacia los datos. De ahí la respuesta moderna.


  1. Cifrado autenticado (AEAD) y datos adicionales autenticados

AEAD significa Authenticated Encryption with Associated Data: cifrado y autenticación en una sola operación y con una sola clave. Al cifrar se produce, además del texto cifrado, una etiqueta de autenticación de 16 bytes. Al descifrar, la librería verifica la etiqueta antes de devolver nada: si no cuadra, lanza una excepción y no entrega ni un byte.

Por qué esto es hoy la respuesta por defecto:

  1. Elimina toda una familia de ataques. Sin integridad, existen los oráculos de relleno, la maleabilidad y la manipulación de bits. Con AEAD, alterar un solo bit del texto cifrado produce un error, no un texto claro distinto.
  2. Evita el error de combinar mal. Antes se hacía «cifrar y luego calcular un MAC», y el orden importaba: MAC-then-encrypt y encrypt-and-MAC tienen problemas conocidos; solo encrypt-then-MAC es correcto. AEAD quita esa decisión de las manos del desarrollador.
  3. Es lo que usan los protocolos serios. TLS 1.3 solo admite suites AEAD (03-05).

Los datos adicionales autenticados (AAD)

Un AEAD acepta un tercer dato de entrada: los AAD, que se autentican pero no se cifran. Son metadatos que viajan en claro pero quedan atados criptográficamente al texto cifrado: si alguien los cambia, el descifrado falla.

Su utilidad en Nimbus es directa. Los adjuntos del bucket A-05 son objetos independientes con nombres predecibles. Si solo ciframos el contenido, un atacante con permiso de escritura en el bucket podría mover el objeto cifrado de la clínica CL-014 a la carpeta de la clínica CL-207: el contenido seguiría descifrándose sin problema, porque la clave es la misma, y la clínica equivocada vería el documento de otra. Es una fuga entre tenants sin romper nada.

La solución es poner el contexto en los AAD:

AAD = "tenant:CL-014|adjunto:9f31c7|version:1"

Ahora el texto cifrado solo se descifra si se presenta con ese contexto exacto. Movido a otro tenant, el descifrado lanza una excepción. Es la traducción criptográfica del WHERE tenant_id que corrigió el IDOR en 01-01: el dato queda ligado a su dueño.


  1. Cifrar un adjunto de Nimbus con AES-GCM

Este es el ejemplo central de la lección. Usa la librería cryptography, que es la implementación de referencia en Python y la que debes utilizar en producción.

import secrets
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag

# ---------------------------------------------------------------
# 1. LA CLAVE. En produccion NO se genera aqui: se obtiene del
#    gestor de secretos o del KMS (03-06). Se genera una sola vez.
# ---------------------------------------------------------------
clave = AESGCM.generate_key(bit_length=256)     # 32 bytes de fuente segura
aead = AESGCM(clave)

# ---------------------------------------------------------------
# 2. LOS DATOS Y SU CONTEXTO
# ---------------------------------------------------------------
adjunto = b"Informe de evolucion. Paciente ficticio: Ana Ruiz. Sesion 7 de 10."
tenant_id = "CL-014"
adjunto_id = "9f31c7"
aad = f"tenant:{tenant_id}|adjunto:{adjunto_id}".encode()

# ---------------------------------------------------------------
# 3. CIFRAR. Nonce de 96 bits (12 bytes), NUEVO en cada operacion.
# ---------------------------------------------------------------
nonce = secrets.token_bytes(12)
cifrado = aead.encrypt(nonce, adjunto, aad)     # devuelve ciphertext || etiqueta

# ---------------------------------------------------------------
# 4. LO QUE SE GUARDA EN EL BUCKET: nonce + cifrado. El nonce NO es
#    secreto; sin el no se puede descifrar, asi que se guarda al lado.
# ---------------------------------------------------------------
objeto = nonce + cifrado
print(f"Claro: {len(adjunto)} B | Guardado: {len(objeto)} B "
      f"(+12 nonce +16 etiqueta)")

# ---------------------------------------------------------------
# 5. DESCIFRAR con el MISMO contexto
# ---------------------------------------------------------------
n_leido, c_leido = objeto[:12], objeto[12:]
recuperado = aead.decrypt(n_leido, c_leido, aad)
print("Descifrado OK:", recuperado.decode())

# ---------------------------------------------------------------
# 6. QUE PASA SI ALGUIEN ALTERA UN SOLO BYTE DEL TEXTO CIFRADO
# ---------------------------------------------------------------
manipulado = bytearray(objeto)
manipulado[20] ^= 0x01                          # se invierte UN bit
try:
    aead.decrypt(bytes(manipulado[:12]), bytes(manipulado[12:]), aad)
except InvalidTag:
    print("MANIPULACION DETECTADA: la etiqueta no valida. No se devuelve nada.")

# ---------------------------------------------------------------
# 7. QUE PASA SI EL OBJETO SE MUEVE A OTRO TENANT
# ---------------------------------------------------------------
aad_falso = f"tenant:CL-207|adjunto:{adjunto_id}".encode()
try:
    aead.decrypt(n_leido, c_leido, aad_falso)
except InvalidTag:
    print("CONTEXTO INCORRECTO: el adjunto esta ligado a CL-014.")

Salida:

Claro: 65 B | Guardado: 93 B (+12 nonce +16 etiqueta)
Descifrado OK: Informe de evolucion. Paciente ficticio: Ana Ruiz. Sesion 7 de 10.
MANIPULACION DETECTADA: la etiqueta no valida. No se devuelve nada.
CONTEXTO INCORRECTO: el adjunto esta ligado a CL-014.

Puntos que hay que entender de este código, uno a uno:

  • AESGCM.generate_key(bit_length=256) usa internamente el generador seguro del sistema. Nunca derives una clave de un str fijo ni de una contraseña con sha256(...): eso se hace con un KDF (apartado 10 y 03-04).
  • El nonce es de 12 bytes (96 bits) porque es el tamaño para el que GCM está especificado y optimizado. Otros tamaños obligan a un procesamiento adicional y aumentan el riesgo de error.
  • secrets.token_bytes(12) dentro del flujo de cifrado, nunca fuera. Un nonce calculado una vez y reutilizado en un bucle es el fallo más frecuente (apartado 8).
  • encrypt devuelve el texto cifrado con la etiqueta ya concatenada al final. Por eso el resultado ocupa 16 bytes más que el original. No tienes que gestionar la etiqueta por separado.
  • decrypt verifica antes de descifrar. Si la etiqueta no cuadra lanza InvalidTag y no devuelve datos parciales. Nunca captures esa excepción para «seguir adelante con lo que haya»: no hay nada válido que recuperar.
  • El AAD no se guarda cifrado porque no hace falta: tenant_id y adjunto_id ya están en la ruta del objeto y en la base de datos. Lo que aporta el AAD es que no se puedan cambiar sin invalidar el cifrado.
  • El tamaño se conserva casi exactamente: 65 bytes de contenido producen 93 guardados. GCM no rellena, así que revela la longitud aproximada del original. Si esa longitud es sensible (por ejemplo, distinguir un informe largo de uno corto), se añade relleno explícito antes de cifrar.

El antipatrón: ECB

# =============== INCORRECTO — NO USAR NUNCA ===============
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

clave_mala = b"0" * 32
cifrador = Cipher(algorithms.AES(clave_mala), modes.ECB()).encryptor()

bloque_a = b"ESTADO: CANCELADA"[:16]
bloque_b = b"ESTADO: CANCELADA"[:16]
print(cifrador.update(bloque_a).hex())
print(cifrador.update(bloque_b).hex())
# Ambas lineas imprimen EXACTAMENTE lo mismo:
# el atacante deduce que las dos citas tienen el mismo estado sin la clave.
# ==========================================================

Tres cosas están mal aquí: el modo ECB (patrones visibles), la clave constante (no es un secreto) y la ausencia de integridad. La versión correcta es la del bloque anterior: AES-GCM, clave del gestor de secretos, nonce único, AAD con el contexto.


  1. La regla del nonce

Es la regla operativa más importante del cifrado simétrico moderno:

Con una misma clave, un nonce no se repite jamás.

Por qué en GCM es catastrófico y no simplemente malo:

  1. Se pierde la confidencialidad, igual que en CTR: dos mensajes cifrados con el mismo par (clave, nonce) comparten el chorro; el XOR de los cifrados revela el XOR de los claros.
  2. Se pierde la autenticación, y de forma permanente. GCM construye su etiqueta con una clave de autenticación derivada de la principal. Con dos mensajes bajo el mismo nonce, un atacante puede recuperar esa clave de autenticación y, a partir de ahí, falsificar etiquetas válidas para mensajes que él elija, indefinidamente y para todos los nonces. No es una fuga puntual: es la pérdida de la propiedad de integridad para esa clave.

Las dos estrategias válidas para generar nonces:

Estrategia Cómo Cuándo usarla Riesgo
Aleatorio de 96 bits secrets.token_bytes(12) en cada operación Sistemas distribuidos, varios procesos, sin estado compartido Colisión por cumpleaños. Seguro hasta ~2^32 mensajes por clave
Contador Contador monótono persistente, opcionalmente con prefijo de instancia Un único emisor con estado fiable Un reinicio que reponga el contador a cero repite nonces

Recomendación para Nimbus: nonce aleatorio de 12 bytes, porque la API corre en varios contenedores sin estado compartido y un contador exigiría coordinación. Con el límite práctico de rotar la clave antes de acercarse a 2^32 mensajes (unos cuatro mil millones), que en el volumen de Nimbus significa una rotación anual holgada (03-06).

Los tres errores concretos que hay que vigilar en revisión de código:

# INCORRECTO 1: nonce fuera del bucle -> se repite en cada iteracion
nonce = secrets.token_bytes(12)
for adj in adjuntos:
    guardar(aead.encrypt(nonce, adj, aad))     # todos con el MISMO nonce

# INCORRECTO 2: nonce derivado del identificador -> se repite al reeditar
nonce = adjunto_id.encode()[:12]

# INCORRECTO 3: nonce con random -> predecible (03-01)
nonce = bytes(random.randint(0, 255) for _ in range(12))

# CORRECTO: uno nuevo, de fuente segura, dentro del bucle
for adj in adjuntos:
    n = secrets.token_bytes(12)
    guardar(n + aead.encrypt(n, adj, aad))

  1. Cifrar una copia de seguridad con openssl

Lucía necesita cifrar el volcado nocturno antes de subirlo al almacenamiento externo. Desde la línea de órdenes:

# 1. Generar una clave de 256 bits en hexadecimal (64 caracteres hex)
openssl rand -hex 32 > clave_copias.hex
chmod 600 clave_copias.hex

# 2. Cifrar el volcado
openssl enc -aes-256-cbc \
  -pbkdf2 -iter 600000 -md sha256 \
  -salt \
  -in  copia-2026-08-02.tar.gz \
  -out copia-2026-08-02.tar.gz.enc \
  -pass file:clave_copias.hex

# 3. Descifrar (prueba de restauración: obligatoria, ver 02-04)
openssl enc -d -aes-256-cbc \
  -pbkdf2 -iter 600000 -md sha256 \
  -in  copia-2026-08-02.tar.gz.enc \
  -out copia-restaurada.tar.gz \
  -pass file:clave_copias.hex

Opción por opción:

Opción Qué hace Por qué importa
enc Subcomando de cifrado simétrico de ficheros
-aes-256-cbc Algoritmo y modo AES de 256 bits en CBC
-pbkdf2 Deriva la clave real con PBKDF2 en lugar del método heredado Sin esto, openssl enc usa una derivación antigua basada en MD5 y una sola iteración: inaceptable
-iter 600000 Iteraciones de la derivación Encarece los intentos si la contraseña fuera débil (03-04)
-md sha256 Hash de la derivación Evita el MD5 heredado
-salt Sal aleatoria por fichero (por defecto en versiones actuales) Dos copias iguales dan cifrados distintos
-pass file:... Lee el secreto de un fichero Nunca -pass pass:...: quedaría en el historial y en ps
-d Descifrar

Advertencia importante sobre esta herramienta. openssl enc produce cifrado sin autenticación: CBC no lleva etiqueta, así que no detecta manipulación del fichero cifrado. Es aceptable para una copia que además va acompañada de su hash verificado (03-04) y vive en un almacenamiento inmutable, pero no es el patrón recomendado para datos nuevos. Para copias de seguridad hoy conviene usar herramientas que hagan AEAD por diseño —age, restic, borg con cifrado, o el cifrado del lado del servidor del proveedor con claves gestionadas (03-06, 03-07)— en lugar de construir el esquema a mano. Y en todos los casos, la clave no puede vivir en la misma máquina ni en la misma cuenta cloud que la copia: si el atacante llega a las copias, llegaría también a la clave.


  1. Derivación de claves: HKDF

Un problema práctico frecuente: Nimbus tiene una clave maestra en el gestor de secretos, pero necesita claves distintas para adjuntos, para copias y para el índice ciego de búsqueda (03-07). Usar la misma clave para todo es mala práctica —viola la separación de dominios y multiplica el riesgo de colisión de nonces—, pero guardar cinco claves maestras multiplica el trabajo de custodia.

La solución es HKDF (HMAC-based Key Derivation Function): deriva múltiples claves independientes a partir de una sola, de forma que conocer una derivada no revela nada de la maestra ni de las hermanas.

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

clave_maestra = obtener_del_gestor_de_secretos()      # 32 bytes de alta entropia

def derivar(proposito: str) -> bytes:
    """Deriva una clave de 256 bits para un proposito concreto."""
    return HKDF(
        algorithm=hashes.SHA256(),
        length=32,                                    # 32 bytes = 256 bits
        salt=None,                                    # opcional; si hay sal, mejor
        info=proposito.encode(),                      # SEPARACION DE DOMINIOS
    ).derive(clave_maestra)

k_adjuntos = derivar("nimbus/v1/adjuntos")
k_copias   = derivar("nimbus/v1/copias")
k_indice   = derivar("nimbus/v1/indice-ciego")

Claves de este código:

  • info es la etiqueta de propósito. Cambiar una sola letra produce una clave completamente distinta: es lo que garantiza que las tres derivadas sean independientes. Incluir la versión (v1) permite rotar sin ambigüedad (03-06).
  • length=32 fija el tamaño de salida. HKDF puede producir cualquier longitud a partir de la misma entrada.
  • La clave maestra debe tener alta entropía ya. HKDF expande y separa, no fortalece. No sirve para derivar de una contraseña humana.

La distinción que no debes confundir: HKDF deriva claves a partir de otras claves y es deliberadamente rápido. Derivar a partir de contraseñas humanas requiere una función deliberadamente lenta —Argon2id, scrypt, bcrypt, PBKDF2—, porque la entrada tiene poca entropía y hay que encarecer cada intento del atacante. Eso se estudia en 03-04.


  1. Rendimiento y sus consecuencias de diseño

Los órdenes de magnitud explican por qué los sistemas reales están construidos como están. Cifras orientativas en una CPU de servidor moderna con AES-NI:

Operación Rendimiento aproximado Relación
AES-256-GCM (cifrar/descifrar) ~1–5 GB/s por núcleo Referencia
ChaCha20-Poly1305 ~1–2 GB/s (mejor sin AES-NI) Comparable
SHA-256 ~1–2 GB/s Comparable
Firma Ed25519 ~decenas de miles/s Muy inferior en volumen
RSA-2048, operación privada ~1.000–2.000/s ~10.000 veces más lento por byte
RSA-4096, operación privada ~150–300/s Prohibitivo para datos

Las tres consecuencias de diseño: (1) nunca se cifran datos grandes con criptografía asimétrica —RSA ni siquiera puede cifrar más bytes que su módulo—, de donde nace el cifrado híbrido de 03-03; (2) cifrar en reposo es prácticamente gratis, así que «ralentiza la base de datos» dejó de ser un argumento aceptable hace quince años; y (3) el coste está en las operaciones asimétricas por conexión, no en los bytes, razón por la que TLS reutiliza sesiones y una API con muchas conexiones cortas gasta más CPU en handshakes que en cifrar tráfico (03-05).


  1. El problema que queda abierto: distribuir la clave

Todo lo anterior funciona si las dos partes ya comparten la clave. Y ahí es donde lo simétrico choca contra su límite. Piensa en el caso más cotidiano de Nimbus:

Una fisioterapeuta abre la app en su móvil, en la sala de espera, conectada a la wifi de la clínica. Su dispositivo y la API de Nimbus nunca se han visto antes. Necesitan una clave simétrica común para cifrar la sesión. ¿Cómo se la comunican, si el único canal que tienen es precisamente el que quieren proteger?

Enviarla por el canal no sirve: quien escucha la captura. Repartirla por adelantado tampoco: los clientes son miles y cambian a diario. Y el problema de escala es demoledor: con 10.000 usuarios finales harían falta casi cincuenta millones de claves. Este es el problema de la distribución de claves, que estuvo sin resolver durante los primeros cuatro mil años de historia de la criptografía; se resolvió en los años setenta, y su solución es la razón por la que existe el comercio en internet.


Errores Comunes y Consejos

Errores de modo y parámetros

  1. Usar ECB. Sin excepciones ni matices. Si aparece en el código, es un hallazgo.
  2. Elegir CBC para código nuevo. No está roto, pero exige relleno correcto, IV impredecible y un MAC añadido bien. AEAD te ahorra las tres decisiones.
  3. Reutilizar el nonce. El fallo más grave y más común. Nonce nuevo dentro del bucle, siempre, con secrets.
  4. Creer que cifrar da integridad. Solo la da el AEAD.
  5. Capturar InvalidTag y continuar. Esa excepción significa «estos datos han sido alterados o el contexto es incorrecto». Se registra el evento y se aborta.

Errores de clave

  1. Derivar la clave con sha256(contraseña). No es un KDF. Contraseñas: Argon2id (03-04). Claves a partir de claves: HKDF.
  2. Clave escrita en el código o en un .env versionado. Es exactamente el fallo que permitió la escalada en el ransomware de 02-06 y se trata en 03-06.
  3. Una única clave para todo el sistema, o guardarla junto al dato cifrado. Deriva por propósito con HKDF, y recuerda que una copia cifrada con su clave en el mismo bucket es cifrado decorativo.

Consejos

  • La respuesta por defecto de este curso para cifrado simétrico nuevo es: AES-256-GCM (o ChaCha20-Poly1305 sin aceleración hardware), nonce aleatorio de 12 bytes, AAD con el contexto de negocio, clave del gestor de secretos.
  • Guarda el objeto cifrado como versión || nonce || ciphertext+etiqueta. Ese primer byte de versión te permitirá cambiar de algoritmo dentro de tres años sin reescribir los datos antiguos.
  • Ata el cifrado al contexto con AAD siempre que el dato tenga dueño: en un SaaS multi-tenant, eso es siempre. Cifrar es barato; diseñar dónde vive la clave es el trabajo de verdad.

Ejercicios

Ejercicio 1 — Diagnosticar una implementación

Iván ha escrito esta función para cifrar las notas internas de las reservas:

import random
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

CLAVE = b"NimbusReservas2026SecretKey12345"        # (1)
IV = b"0123456789abcdef"                            # (2)

def cifrar_nota(nota: str) -> bytes:
    relleno = 16 - (len(nota) % 16)
    nota += chr(relleno) * relleno
    c = Cipher(algorithms.AES(CLAVE), modes.CBC(IV)).encryptor()   # (3)
    return c.update(nota.encode()) + c.finalize()

def id_operacion() -> str:
    return "".join(random.choice("abcdef0123456789") for _ in range(16))  # (4)

Se pide: (a) enumera los cuatro problemas marcados y explica el ataque concreto que habilita cada uno; (b) indica qué propiedad de seguridad falta por completo y qué consecuencia tiene; (c) reescribe la función con AES-GCM, incluyendo el AAD adecuado para Nimbus y el formato de almacenamiento; (d) explica qué hay que hacer con las notas ya cifradas con el código antiguo.

Ejercicio 2 — Elegir modo y parámetros

Para cada caso de Nimbus, decide: algoritmo y modo, tamaño de clave, cómo se genera el nonce/IV, qué pones en el AAD (o por qué no procede) y dónde vive la clave.

  1. Adjuntos de clínicas en el bucket A-05, hasta 20 MB, miles al día, varios contenedores de API en paralelo.
  2. Volcado nocturno de PostgreSQL, 8 GB, un solo proceso, restauración posible dentro de cinco años.
  3. Campo notas_clinicas de la tabla de citas, unos 500 caracteres, cifrado a nivel de aplicación.
  4. Contenido de la sesión entre la app móvil y la API.
  5. Fichero de configuración con credenciales de terceros que se despliega con los contenedores.

Ejercicio 3 — Explicar el nonce a la dirección

Marta ha leído que «un fallo de nonce» tumbó a otra empresa y te pide una explicación de tres párrafos, sin matemáticas, para el comité.

Se pide: (a) explica con una analogía qué es un nonce y por qué debe ser único; (b) explica por qué repetirlo en GCM es peor que repetirlo en CTR; (c) di qué dos controles concretos implantarías en Nimbus para que este fallo no pueda ocurrir, y quién es responsable de cada uno.


Soluciones

Ejercicio 1

(a) Los cuatro problemas:

# Problema Ataque que habilita
1 Clave escrita en el código, y además derivada de una frase legible Cualquiera con acceso al repositorio, a un backup del código o al contenedor tiene la clave. Es el patrón del .env de 02-06. Además no es entropía real: son caracteres ASCII
2 IV constante Con IV fijo, CBC vuelve a ser determinista para el primer bloque: dos notas con el mismo comienzo dan el mismo cifrado inicial. Permite correlacionar y habilita texto claro elegido
3 CBC sin autenticación y con relleno artesanal Maleabilidad (alterar bits del cifrado altera el claro de forma predecible) y riesgo de oráculo de relleno si el servidor distingue errores
4 random para un identificador Predecible (03-01). Si ese identificador se usa en URLs o referencias, es enumerable

(b) Falta la integridad/autenticidad. Consecuencia: nadie detecta que una nota cifrada ha sido sustituida o modificada; el sistema descifrará basura o texto manipulado y lo mostrará como legítimo.

(c) Versión correcta:

import secrets
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

VERSION = b"\x01"

def cifrar_nota(nota: str, tenant_id: str, reserva_id: int, clave: bytes) -> bytes:
    aead = AESGCM(clave)                                   # clave del gestor (03-06)
    nonce = secrets.token_bytes(12)                        # NUEVO en cada llamada
    aad = f"v1|tenant:{tenant_id}|reserva:{reserva_id}".encode()
    cifrado = aead.encrypt(nonce, nota.encode("utf-8"), aad)
    return VERSION + nonce + cifrado                       # formato versionado

def descifrar_nota(blob: bytes, tenant_id: str, reserva_id: int, clave: bytes) -> str:
    assert blob[:1] == VERSION, "version de cifrado desconocida"
    nonce, cifrado = blob[1:13], blob[13:]
    aad = f"v1|tenant:{tenant_id}|reserva:{reserva_id}".encode()
    return AESGCM(clave).decrypt(nonce, cifrado, aad).decode("utf-8")

(d) Las notas antiguas. No basta con cambiar el código: (1) se considera la clave antigua comprometida desde el momento en que estuvo en el repositorio, así que se rota; (2) se ejecuta una migración que descifra con el esquema antiguo y recifra con el nuevo, tenant a tenant; (3) el byte de versión permite que ambos formatos convivan durante la migración; (4) se elimina la clave antigua del historial del repositorio sabiendo que borrar el commit no revoca nada (03-06); (5) se registra el incidente y se evalúa si hubo acceso indebido.

Ejercicio 2

Caso Algoritmo/modo Clave Nonce/IV AAD Dónde vive la clave
1. Adjuntos AES-256-GCM (o cifrado por trozos para archivos grandes) 256 bits derivada con HKDF info="adjuntos" Aleatorio 12 B por objeto (varios contenedores: no cabe contador) tenant_id, adjunto_id, versión KMS/gestor de secretos; clave de datos por objeto con envelope encryption (03-06)
2. Volcado AEAD mediante herramienta dedicada (age, restic) mejor que openssl enc 256 bits La gestiona la herramienta Identificador de la copia y fecha Fuera de la cuenta cloud de producción. Copia en custodia offline
3. Campo notas_clinicas AES-256-GCM a nivel de aplicación Derivada con HKDF info="notas" Aleatorio 12 B por escritura tenant_id, cita_id, version KMS. Ver 03-07 y el problema de la búsqueda
4. Sesión app-API No lo implementes tú: es TLS 1.3 con AES-GCM o ChaCha20-Poly1305 La negocia el protocolo El protocolo El protocolo Efímera, en memoria (03-05)
5. Configuración con credenciales No cifres un fichero de secretos: no lo tengas. Inyección desde el gestor de secretos en el arranque Gestor de secretos (03-06)

Nota sobre el caso 3: si esas notas contienen información de salud, el diseño debe validarse con el responsable de protección de datos antes de implantarlo; el marco legal se trata en 06-03.

Ejercicio 3

(a) Analogía. Un nonce es como el número de una habitación de hotel combinado con la llave maestra: la llave es siempre la misma, pero abre una habitación distinta cada vez. Si dos huéspedes reciben el mismo número, acaban en la misma habitación y ven las cosas del otro. El nonce es lo que hace que el mismo secreto produzca un resultado diferente en cada uso; si se repite, se repite el resultado y aparecen relaciones entre mensajes que no deberían existir.

(b) Por qué en GCM es peor. En CTR, repetir el nonce rompe la confidencialidad de esos dos mensajes: es un daño acotado. En GCM, además, permite al atacante deducir el valor interno con el que se calculan las etiquetas de autenticación y, con él, fabricar mensajes falsos que el sistema aceptará como auténticos, para siempre y con cualquier nonce. Se pasa de «dos mensajes leídos» a «cualquiera puede firmar en tu nombre con esa clave».

(c) Dos controles.

  1. Técnico: una única función interna de cifrado en la base de código —una caja_fuerte.cifrar(datos, contexto)— que genere el nonce internamente y no acepte que se le pase desde fuera. Si el parámetro no existe, no se puede reutilizar. Responsable: Iván, con revisión de Marta.
  2. De proceso: regla de revisión obligatoria de dos personas para cualquier cambio que toque ese módulo, más una comprobación automática en el CI (02-04) que rechace Cipher(, modes.ECB, random. y nonces literales fuera de la caja fuerte. Responsable: Lucía (pipeline), con Marta como aprobadora.

Conclusión

Ya sabes usar la mitad rápida de la criptografía. Tienes claro qué es un esquema simétrico —una clave que cifra y descifra—, cuándo es la respuesta natural en Nimbus (adjuntos, copias, cifrado en reposo, contenido de sesión) y cuáles son sus dos virtudes, velocidad y claves cortas. Distingues cifrado de bloque de cifrado de flujo, y sabes por qué AES es el estándar: un concurso público, veinticinco años de criptoanálisis abierto y aceleración en hardware; la mejor demostración práctica del principio de Kerckhoffs.

Pero la lección que debes llevarte es la del modo: AES es intachable y aun así ECB deja ver al pingüino, CBC exige IV impredecible y relleno correcto, y CTR se desmorona si repites el nonce. Ninguno de los tres protege la integridad, y por eso la respuesta por defecto de hoy es el cifrado autenticado: AES-GCM o ChaCha20-Poly1305, con etiqueta verificada antes de devolver un solo byte, y con AAD para atar el texto cifrado a su contexto —el tenant_id de Nimbus—, que es la versión criptográfica del WHERE tenant_id con el que se corrigió el IDOR. Lo has visto funcionar: alterar un bit produce InvalidTag, y mover un adjunto de CL-014 a CL-207 también. Te llevas además la regla más importante del apartado, un nonce jamás se repite bajo la misma clave, con el porqué exacto de que en GCM eso no sea un fallo puntual sino la pérdida permanente de la autenticación; la práctica con openssl enc y su advertencia —cifra pero no autentica, y la clave nunca vive junto a la copia—; HKDF para derivar claves por propósito a partir de una maestra; y los órdenes de magnitud que explican por qué nunca se cifran datos grandes con criptografía asimétrica.

Y te queda un problema sin resolver, el mismo que tuvo la humanidad durante cuatro mil años: si el único canal entre la app móvil de la fisioterapeuta y la API de Nimbus es el que queremos proteger, ¿cómo acuerdan la clave? No hay respuesta dentro del mundo simétrico.

En Criptografía Asimétrica (03-03) verás la idea que lo cambió todo: dos claves distintas y relacionadas matemáticamente, una pública que se reparte sin miedo y una privada que no sale nunca. Con ella se resuelve la distribución mediante el intercambio Diffie-Hellman y su propiedad más valiosa, la forward secrecy; aparece el cifrado híbrido, que es el patrón real de TLS; y llega la firma digital, la única primitiva capaz de dar no repudio. Al final quedará abierta otra pregunta, la que dará sentido a la PKI: esta clave pública, ¿de quién es realmente?

Curso de Fundamentos de Seguridad Informática

Módulo 1: Introducción a la Seguridad Informática

Módulo 2: Ciberseguridad

Módulo 3: Criptografía

Módulo 4: Gestión de Riesgos y Medidas de Protección

Módulo 5: Herramientas y Técnicas de Seguridad

Módulo 6: Buenas Prácticas y Normativas

Módulo 7: Proyecto Final

© Copyright 2026. Todos los derechos reservados