10 Herramientas de Encriptación que Todo Hacker Debe Conocer
El encoding representa, el hashing huella, la encriptación protege.
Con una clave, el cifrado convierte datos legibles en basura ininteligible.
Sin esa clave, recuperar el original debe ser computacionalmente inviable.Este artículo cierra la trilogía. El primero cubrió encoding; el segundo,
hashing. Aquí abordamos la encriptación: simétrica, asimétrica, clásica
y moderna, con usos reales y ejemplos de CTF.
¿Qué es encriptación y por qué importa?
La encriptación transforma datos usando un algoritmo y una clave.
Sin la clave correcta, recuperar el original debe ser impracticable.
Encriptación ≠ Encoding ≠ Hashing
| Propiedad | Encoding | Hashing | Encriptación |
|---|---|---|---|
| Reversible | Sí (sin clave) | No | Sí (con clave) |
| Requiere clave | No | No | Sí |
| Longitud salida | Variable | Fija | Variable (≈ input) |
| Uso principal | Transporte | Integridad | Confidencialidad |
Tipos de encriptación
| Tipo | Claves | Ejemplos | Velocidad | Usos |
|---|---|---|---|---|
| Simétrica | Una sola clave compartida | AES, ChaCha20, DES | Rápida | Cifrado de datos, archivos, tráfico |
| Asimétrica | Par público/privado | RSA, ECC | Lenta | Intercambio de claves, firmas |
En la práctica se usan sistemas híbridos: asimétrica para intercambiar
una clave de sesión, y simétrica para cifrar los datos.
1. XOR
XOR (⊕) es la operación más fundamental de la criptografía. Sin ella, no
existen AES, ChaCha20 ni ningún cifrado moderno. Cada bit se combina con
un bit de clave: si ambos son iguales → 0; si son distintos → 1.
Propiedad clave: A ⊕ B ⊕ B = A. Aplicar XOR dos veces con el mismo
valor devuelve el original. Por eso es la base de todos los stream ciphers.
Usos en CTF
- Single-byte XOR: El reto de criptografía más básico. Un texto se
cifró con un solo byte como clave. Hay 256 posibilidades. Se fuerza
bruta y se busca texto legible en inglés. - Multi-byte XOR (repitiendo clave): Similar pero con clave de N
bytes. Se detecta la longitud de clave con el índice de coincidencia
o distancia de Hamming, luego se fuerza cada posición como single-byte. - Crib dragging: Si dos textos se cifraron con la misma clave XOR
(c1 ⊕ c2 = m1 ⊕ m2), se pueden recuperar ambos conociendo palabras
probables en uno de ellos. - Imagen XOR: XOR de dos imágenes revela la información combinada.
Común en retos de esteganografía. - Clave es la flag: A veces la clave misma es la flag. Resolver el
cifrado revela la clave.
Usos en el mundo real
- Stream ciphers: ChaCha20, Salsa20, RC4 son básicamente XOR con un
keystream generado a partir de la clave. - One-Time Pad (OTP): XOR con una clave tan larga como el mensaje,
usada solo una vez. Es el único cifrado perfecto (Shannon 1949). - Malware: Algunos malware ofuscan strings con XOR antes de
almacenarlos en el binario para evadir detección estática.stringsno revela las cadenas reales hasta aplicar XOR. - Protocolos de bajo nivel: Muchos protocolos simples usan XOR
como checksum o cifrado básico.
🧪 Código Python — XOR
import itertools
# ─── XOR con un solo byte ─────────────────────────────────
def xor_single_byte(data: bytes, key: int) -> bytes:
return bytes(b ^ key for b in data)
def xor_single_byte_bruteforce(ciphertext: bytes) -> list:
"""Fuerza bruta de XOR single-byte. Devuelve resultados con score."""
results = []
for key in range(256):
plain = xor_single_byte(ciphertext, key)
score = sum(1 for c in plain if chr(c) in "ETAOIN SHRDLUetaoinshrdlu")
if score > 0:
results.append((key, score, plain))
results.sort(key=lambda x: -x[1])
return results
# ─── XOR con clave multibyte ─────────────────────────────
def xor_repeating(data: bytes, key: bytes) -> bytes:
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
def xor_repeating_crack(ciphertext: bytes, max_key_len: int = 20) -> list:
"""Detecta longitud de clave y descifra XOR multibyte."""
def hamming_distance(b1: bytes, b2: bytes) -> int:
return sum(bin(b1[i] ^ b2[i]).count("1") for i in range(len(b1)))
# Detectar longitud de clave
key_sizes = []
for klen in range(2, max_key_len + 1):
chunks = [ciphertext[i:i+klen] for i in range(0, len(ciphertext), klen) if len(ciphertext[i:i+klen]) == klen]
if len(chunks) < 2:
continue
dist = sum(hamming_distance(chunks[i], chunks[i+1]) for i in range(min(4, len(chunks)-1)))
norm_dist = dist / (min(4, len(chunks)-1)) / klen
key_sizes.append((norm_dist, klen))
key_sizes.sort()
best_klen = key_sizes[0][1]
# Fuerza bruta cada byte de la clave
key = b""
for i in range(best_klen):
block = ciphertext[i::best_klen]
best_key_byte = max(range(256),
key=lambda k: sum(1 for c in xor_single_byte(block, k)
if chr(c) in "ETAOIN SHRDLUetaoinshrdlu.,;:!? "))
key += bytes([best_key_byte])
plaintext = xor_repeating(ciphertext, key)
return [{"key": key, "plaintext": plaintext}]
# ─── Crib dragging ───────────────────────────────────────
def crib_drag(c1: bytes, c2: bytes, crib: str) -> str:
"""c1 ⊕ c2 = m1 ⊕ m2. Si conocemos parte de m1, recuperamos m2."""
crib_bytes = crib.encode()
xor_combined = bytes(a ^ b for a, b in zip(c1, c2))
result = ""
for i in range(len(xor_combined) - len(crib_bytes) + 1):
chunk = xor_combined[i:i+len(crib_bytes)]
decrypted = bytes(c ^ crib_bytes[j] for j, c in enumerate(chunk))
try:
result += decrypted.decode("utf-8")
except:
result += "."
return result
# ─── XOR de dos imágenes (bytes) ─────────────────────────
def xor_bytes(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
2. Caesar / ROT / Atbash
Caesar: desplaza cada letra del alfabeto un número fijo de posiciones.
ROT13: desplazamiento 13 (simétrico: cifrar y descifrar es la misma operación).
Atbash: invierte el alfabeto (A→Z, B→Y, etc.).
Son cifrados de sustitución monoalfabética. Cada letra del texto original
se reemplaza siempre por la misma letra cifrada. Extremadamente débiles:
26 posibles desplazamientos (Caesar) o un único alfabeto invertido (Atbash).
Usos en CTF
- Crypto 101: Es el primer reto de criptografía en cualquier CTF.
synt{ebg13_vf_rnfl}→flag{rot13_is_easy}. - Fuerza bruta de Caesar: Probar los 25 desplazamientos y buscar
texto legible. Un script de 5 líneas resuelve cualquier Caesar. - ROT13 combinado: A veces aparece ROT13 + Base64 como doble capa.
- ROT47: Variante que incluye símbolos (desplazamiento 47 sobre
ASCII imprimible 33-126). Común en retos de «misc». - Atbash: Fácil de detectar: la primera letra del alfabeto se
reemplaza por la última, la segunda por la anteúltima, etc. - Flags con múltiples capas: Una flag puede estar en Caesar, luego
en Base64, luego en hex. Hay que revertir cada capa.
Usos en el mundo real
- Ofuscación trivial en malware: Algunos malware antiguo ofuscaban
strings con ROT13 para evadir detección básica. - Foros y Usenet: ROT13 se usaba para ocultar spoilers o contenido
sensible en mensajes de texto. - Puzzles de reclutamiento: Empresas como Google o Microsoft han
usado Caesar en acertijos de reclutamiento. - Casi nulo en seguridad real: Cualquier cifrado por sustitución
simple se rompe en segundos con análisis de frecuencias.
🧪 Código Python — Caesar / ROT / Atbash
import string
# ─── Caesar cipher ────────────────────────────────────────
def caesar_encrypt(text: str, shift: int) -> str:
result = []
for c in text:
if c.isupper():
result.append(chr((ord(c) - 65 + shift) % 26 + 65))
elif c.islower():
result.append(chr((ord(c) - 97 + shift) % 26 + 97))
else:
result.append(c)
return "".join(result)
def caesar_decrypt(text: str, shift: int) -> str:
return caesar_encrypt(text, -shift)
def caesar_bruteforce(ciphertext: str) -> list:
"""Prueba los 25 desplazamientos y devuelve todos los resultados."""
results = []
for shift in range(26):
plain = caesar_decrypt(ciphertext, shift)
score = sum(1 for c in plain if c in "ETAOIN SHRDLUetaoinshrdlu")
results.append((shift, score, plain))
results.sort(key=lambda x: -x[1])
return results
# ─── ROT13 (simétrico: cifrar = descifrar) ───────────────
def rot13(text: str) -> str:
return caesar_encrypt(text, 13)
# ─── ROT47 (ASCII 33-126) ────────────────────────────────
def rot47(text: str) -> str:
result = []
for c in text:
if 33 <= ord(c) <= 126:
result.append(chr(33 + (ord(c) + 14) % 94))
else:
result.append(c)
return "".join(result)
# ─── Atbash (invertir alfabeto) ──────────────────────────
def atbash(text: str) -> str:
result = []
for c in text:
if c.isupper():
result.append(chr(90 - (ord(c) - 65)))
elif c.islower():
result.append(chr(122 - (ord(c) - 97)))
else:
result.append(c)
return "".join(result)
# ─── Detector automático ─────────────────────────────────
import re
def detectar_y_descifrar(text: str) -> list:
"""Intenta identificar y descifrar Caesar, ROT13, ROT47, Atbash."""
results = []
# ROT13 (solo letras)
if re.search(r'[A-Za-z]', text):
for shift in range(26):
plain = caesar_decrypt(text, shift)
if any(word in plain for word in ["the", "flag", "this", "that", "with"]):
results.append(("Caesar shift=" + str(shift), plain))
break
# ROT47 (símbolos)
if any(ord(c) > 126 or (33 <= ord(c) <= 126) for c in text):
results.append(("ROT47", rot47(text)))
# Atbash
results.append(("Atbash", atbash(text)))
return results
3. Vigenère
Cifrado de sustitución polialfabética. Usa una palabra clave para
determinar el desplazamiento de cada letra. A diferencia de Caesar, la
misma letra en el texto original puede cifrarse como letras diferentes
dependiendo de su posición y la clave.
Fue considerado «indescifrable» durante siglos (le chiffre indéchiffrable).
Hoy se rompe con análisis de frecuencias y el test de Kasiski para
determinar la longitud de la clave.
Usos en CTF
- Clave corta: El reto clásico. Se detecta la longitud de la clave
con el índice de coincidencia (IC) o Kasiski, luego cada columna se
resuelve como un Caesar independiente. - Clave es una palabra del diccionario: Se fuerza bruta con una
wordlist de palabras comunes como clave. - Clave es la flag (meta): Descifrar el mensaje revela la clave,
que a su vez es la flag. O la flag está en el texto descifrado. - Vigenère con alfabeto no estándar: A veces usan solo números,
solo mayúsculas, o un alfabeto reducido. - Autoclave: Variante donde el texto original se usa como parte
de la clave. Más difícil de romper.
Usos en el mundo real
- Criptografía histórica: Usado en guerras civiles y conflictos
del siglo XIX. Ejemplo famoso: la Confederación en la Guerra Civil
de EE.UU. usaba Vigenère. Sus mensajes fueron descifrados por la Unión. - Práctico: ninguno. Cualquier cifrado polialfabético con clave
corta se rompe con análisis de frecuencias. - Académico: Se enseña en universidades como introducción a la
criptoanálisis y al índice de coincidencia.
🧪 Código Python — Vigenère
import re
from collections import Counter
# ─── Cifrar / Descifrar ───────────────────────────────────
def vigenere_encrypt(plaintext: str, key: str) -> str:
result = []
key = key.upper()
key_idx = 0
for c in plaintext:
if c.isupper():
shift = ord(key[key_idx % len(key)]) - 65
result.append(chr((ord(c) - 65 + shift) % 26 + 65))
key_idx += 1
elif c.islower():
shift = ord(key[key_idx % len(key)]) - 65
result.append(chr((ord(c) - 97 + shift) % 26 + 97))
key_idx += 1
else:
result.append(c)
return "".join(result)
def vigenere_decrypt(ciphertext: str, key: str) -> str:
result = []
key = key.upper()
key_idx = 0
for c in ciphertext:
if c.isupper():
shift = ord(key[key_idx % len(key)]) - 65
result.append(chr((ord(c) - 65 - shift) % 26 + 65))
key_idx += 1
elif c.islower():
shift = ord(key[key_idx % len(key)]) - 65
result.append(chr((ord(c) - 97 - shift) % 26 + 97))
key_idx += 1
else:
result.append(c)
return "".join(result)
# ─── Detectar longitud de clave con Índice de Coincidencia ─
def indice_coincidencia(text: str) -> float:
freqs = Counter(c.upper() for c in text if c.isalpha())
n = sum(freqs.values())
if n <= 1:
return 0
return sum(f * (f - 1) for f in freqs.values()) / (n * (n - 1))
def detectar_longitud_clave(ciphertext: str, max_len: int = 20) -> int:
best_len = 1
best_ic = 0
for klen in range(1, max_len + 1):
ics = []
for i in range(klen):
column = ciphertext[i::klen]
ic = indice_coincidencia(column)
ics.append(ic)
avg_ic = sum(ics) / len(ics)
# El IC del inglés es ~0.0667, el IC aleatorio es ~0.038
if abs(avg_ic - 0.0667) < abs(best_ic - 0.0667):
best_ic = avg_ic
best_len = klen
return best_len
# ─── Descifrar Vigenère sin conocer clave ────────────────
def vigenere_crack(ciphertext: str) -> dict:
klen = detectar_longitud_clave(ciphertext)
key = ""
for i in range(klen):
column = ciphertext[i::klen]
# Fuerza bruta Caesar en esta columna
best_shift = 0
best_score = 0
for shift in range(26):
decrypted = ""
for c in column:
if c.isupper():
decrypted += chr((ord(c) - 65 - shift) % 26 + 65)
elif c.islower():
decrypted += chr((ord(c) - 97 - shift) % 26 + 97)
else:
decrypted += c
score = sum(1 for c in decrypted if c in "ETAOIN SHRDLUetaoinshrdlu")
if score > best_score:
best_score = score
best_shift = shift
key += chr(best_shift + 65)
return {
"key_length": klen,
"key": key,
"plaintext": vigenere_decrypt(ciphertext, key),
}
4. AES (Advanced Encryption Standard)
AES es el estándar de cifrado simétrico desde 2001. Reemplazó a DES
después de un concurso público organizado por el NIST. Bloques de
128 bits, claves de 128, 192 o 256 bits.
Opera en modos de operación que determinan cómo se cifran bloques
múltiples. Los más comunes en CTFs:
| Modo | Descripción | Vulnerabilidad |
|---|---|---|
| ECB | Cada bloque se cifra independientemente | ¡No oculta patrones! Misma entrada = mismo cifrado |
| CBC | Cada bloque se XORea con el cifrado anterior (o IV) | Padding oracle, bit flipping |
| CTR | Convierte AES en stream cipher (cifra contadores) | Reuso de nonce es fatal |
| GCM | Autenticado (confidencialidad + integridad) | Nonce reutilizado rompe todo |
| CCM | Autenticado (CTR + CBC-MAC) | Similar a GCM |
Usos en CTF
- ECB byte-at-a-time: Si controlas parte del texto a cifrar, puedes
recuperar el texto secreto byte a byte explotando la ausencia de
difusión entre bloques. - ECB cut-and-paste: Reordenas bloques cifrados para promover tu
rol de usuario (ej: copias el bloque «admin» sobre el bloque «user»). - Padding Oracle Attack: El servidor revela si el padding es válido
o no. Con eso se descifra o cifra cualquier texto sin conocer la clave.
Clásico en retos web con parámetros cifrados en cookies. - CBC bit flipping: Modificas un bloque cifrado para alterar el
bloque descifrado siguiente. Se usa para cambiar el valor de
parámetros (ej:admin=False→admin=True). - CTR nonce reuse: Si dos mensajes se cifran con el mismo nonce,
se puede XORear los cifrados para obtener el XOR de los textos
originales (crib dragging). - Key recovery: Claves débiles, bytes de clave recuperables por
side-channels o análisis de memoria.
Usos en el mundo real
- TLS/SSL: AES-GCM es el cifrado más usado en HTTPS.
- Cifrado de discos: BitLocker, FileVault, LUKS usan AES.
- WiFi: WPA2/WPA3 usa AES-CCMP.
- VPNs: WireGuard, IPsec, OpenVPN usan AES.
- Almacenamiento en la nube: Cifrado del lado del servidor en
AWS S3, Google Cloud Storage, Azure Blob. - ZIP cifrados: AES (WinZIP AES) estándar desde 2006.
🧪 Código Python — AES
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
import os
# ─── AES-ECB (útil para CTFs, inseguro para producción) ──
def aes_ecb_encrypt(plaintext: str, key: bytes) -> str:
cipher = AES.new(key, AES.MODE_ECB)
padded = pad(plaintext.encode(), AES.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode()
def aes_ecb_decrypt(ciphertext: str, key: bytes) -> str:
cipher = AES.new(key, AES.MODE_ECB)
decoded = base64.b64decode(ciphertext)
return unpad(cipher.decrypt(decoded), AES.block_size).decode()
# ─── AES-CBC ──────────────────────────────────────────────
def aes_cbc_encrypt(plaintext: str, key: bytes, iv: bytes = None) -> dict:
iv = iv or os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
padded = pad(plaintext.encode(), AES.block_size)
return {
"iv": base64.b64encode(iv).decode(),
"ciphertext": base64.b64encode(cipher.encrypt(padded)).decode(),
}
def aes_cbc_decrypt(ciphertext: str, key: bytes, iv_b64: str) -> str:
iv = base64.b64decode(iv_b64)
cipher = AES.new(key, AES.MODE_CBC, iv)
decoded = base64.b64decode(ciphertext)
return unpad(cipher.decrypt(decoded), AES.block_size).decode()
# ─── AES-CTR ──────────────────────────────────────────────
def aes_ctr_encrypt(plaintext: str, key: bytes, nonce: bytes = None) -> dict:
nonce = nonce or os.urandom(8)
cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
ciphertext = cipher.encrypt(plaintext.encode())
return {
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
}
def aes_ctr_decrypt(ciphertext: str, key: bytes, nonce_b64: str) -> str:
nonce = base64.b64decode(nonce_b64)
cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
decoded = base64.b64decode(ciphertext)
return cipher.decrypt(decoded).decode()
# ─── AES-GCM (autenticado) ───────────────────────────────
def aes_gcm_encrypt(plaintext: str, key: bytes) -> dict:
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode())
return {
"nonce": base64.b64encode(cipher.nonce).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
"tag": base64.b64encode(tag).decode(),
}
def aes_gcm_decrypt(ciphertext: str, key: bytes, nonce_b64: str, tag_b64: str) -> str:
nonce = base64.b64decode(nonce_b64)
tag = base64.b64decode(tag_b64)
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
decoded = base64.b64decode(ciphertext)
try:
return cipher.decrypt_and_verify(decoded, tag).decode()
except (ValueError, KeyError) as e:
return f"[ERROR] Integridad comprometida: {e}"
# ─── ECB byte-at-a-time (ataque clásico) ─────────────────
def ecb_detect_block_size(oracle_callable) -> int:
"""Detecta el tamaño de bloque de un cifrado ECB."""
initial_len = len(oracle_callable(b""))
for i in range(1, 33):
new_len = len(oracle_callable(b"A" * i))
if new_len != initial_len:
return new_len - initial_len
return None
def ecb_detect_mode(oracle_callable) -> str:
"""Detecta si un oráculo usa ECB o CBC."""
payload = b"A" * 64
ciphertext = oracle_callable(payload)
# ECB produce bloques iguales para datos iguales
if ciphertext[16:32] == ciphertext[32:48]:
return "ECB"
return "CBC"
5. DES / 3DES (Data Encryption Standard)
DES fue el estándar federal de cifrado de EE.UU. desde 1977 hasta 2001.
Bloques de 64 bits, clave de 56 bits (efectivos). Hoy se rompe
en horas con hardware especializado o en minutos en la nube ($15 en AWS).
3DES (Triple DES) aplica DES tres veces con dos o tres claves distintas.
Aún considerado mínimamente seguro (bloques de 64 bits lo hacen vulnerable),
pero está en retiro activo desde NIST SP 800-131A.
Usos en CTF
- DES con clave débil: DES tiene claves débiles y semi-débiles
conocidas (0000000000000000, FFFFFFFFFFFFFFFF). Un reto puede usar
una de ellas para facilitar el criptoanálisis. - DES con clave de 8 caracteres: Cada carácter ASCII de 7 bits
forma un byte de clave, y el octavo bit de cada byte es parity.
La clave «password» tiene solo 56 bits reales. - 3DES en protocolos legacy: Retos que involucran PEM (Privacy
Enhanced Mail), PKCS#12, o formatos antiguos que aún usan 3DES. - Meet-in-the-Middle: 3DES con dos claves (EDE) tiene un ataque
MITM que reduce la seguridad de 112 a ~56 bits. - Sweet32 (birthday attack): Con bloques de 64 bits, después de
~32 GB de datos cifrados aparece una colisión que permite recuperar
información. Ataque real en TLS.
Usos en el mundo real
- Legacy bancario: Muchos sistemas financieros legacy aún usan 3DES
en transacciones ATM y tarjetas de crédito (EMV). - Tarjetas de crédito: El PIN de las tarjetas se cifraba con 3DES.
Migración a AES en curso. - ePassports: Algunos pasaportes electrónicos usan 3DES en el
chip (Active Authentication). - Retiro activo: NIST desautorizó 3DES para nuevos usos en 2023.
Todo sistema que aún lo use debe migrar a AES.
🧪 Código Python — DES / 3DES
from Crypto.Cipher import DES, DES3
from Crypto.Util.Padding import pad, unpad
import base64
import os
# ─── DES ──────────────────────────────────────────────────
def des_encrypt(plaintext: str, key: bytes) -> str:
"""Clave DES: 8 bytes. Solo 56 bits efectivos."""
cipher = DES.new(key, DES.MODE_ECB)
padded = pad(plaintext.encode(), DES.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode()
def des_decrypt(ciphertext: str, key: bytes) -> str:
cipher = DES.new(key, DES.MODE_ECB)
decoded = base64.b64decode(ciphertext)
return unpad(cipher.decrypt(decoded), DES.block_size).decode()
# ─── Claves débiles de DES ───────────────────────────────
DES_WEAK_KEYS = [
b"\x00" * 8,
b"\xFF" * 8,
bytes([0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01]),
bytes([0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE]),
]
def des_es_clave_debil(key: bytes) -> bool:
return key in DES_WEAK_KEYS
# ─── 3DES ────────────────────────────────────────────────
def des3_encrypt(plaintext: str, key: bytes) -> str:
"""Clave 3DES: 16 bytes (2 keys) o 24 bytes (3 keys)."""
cipher = DES3.new(key, DES3.MODE_ECB)
padded = pad(plaintext.encode(), DES3.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode()
def des3_decrypt(ciphertext: str, key: bytes) -> str:
cipher = DES3.new(key, DES3.MODE_ECB)
decoded = base64.b64decode(ciphertext)
return unpad(cipher.decrypt(decoded), DES3.block_size).decode()
# ─── 3DES con 2 claves (EDE) ─────────────────────────────
def des3_ede_encrypt(plaintext: str, key1: bytes, key2: bytes) -> str:
"""3DES EDE: cifrar con k1, descifrar con k2, cifrar con k1."""
from Crypto.Cipher import DES3
# Construir clave de 16 bytes y usar MODE_EDE
full_key = key1 + key2
cipher = DES3.new(full_key, DES3.MODE_ECB)
padded = pad(plaintext.encode(), DES3.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode()
# ─── Sweet32: detectar uso de cifrado con bloques de 64 bits ─
def sweet32_vulnerable(algoritmo: str) -> bool:
"""Sweet32 afecta a cifrados con bloques de 64 bits (DES, 3DES, Blowfish)."""
return algoritmo.upper() in ("DES", "3DES", "BLOWFISH")
6. RSA (Rivest-Shamir-Adleman)
RSA es el algoritmo de cifrado asimétrico más usado. Se basa en la
dificultad de factorizar el producto de dos números primos grandes.
- Clave pública:
(n, e)donden = p × q - Clave privada:
(n, d)donded × e ≡ 1 (mod φ(n)) - Cifrado:
c = m^e mod n - Descifrado:
m = c^d mod n
Es lento (miles de veces más lento que AES). Por eso se usa solo para
intercambiar claves de sesión (sistemas híbridos) o firmar pequeños datos.
Usos en CTF
- Factorización de n: Si p y q son pequeños o están mal generados,
se factoriza n para recuperar la clave privada.factordb.comes
el mejor aliado. - e pequeño (e=3): Si el mensaje es pequeño,
m^3 < ny la raíz
cúbica entera revela el mensaje. Ataque de broadcast si mismo mensaje
a 3 destinatarios con e=3. - e grande (Wiener attack): Si d es pequeño en relación a n, se
puede recuperar con fracciones continuas. - Fermat factoring: Si p y q están muy cerca (|p-q| pequeña), se
factoriza n con el método de Fermat. - Mismo n, múltiples e: Dos claves públicas con el mismo n pueden
compartir clave privada. Si además los e son coprimos… ataque
Håstad. - Mismo m, mismos e, n diferentes: Broadcast attack con teorema
chino del resto. - Common factor: Si dos n comparten un factor primo, se calcula
el MCD y se recuperan ambos pares de claves. - PKCS#1 v1.5 padding oracle: Similar al padding oracle de AES,
pero en RSA. Permite descifrar mensajes usando un oráculo que
indica si el padding es válido (Bleichenbacher attack).
Usos en el mundo real
- TLS/SSL: Las conexiones HTTPS usan RSA (o ECDH) para el
intercambio inicial de claves. - Firmas digitales: Código firmado, documentos PDF firmados,
actualizaciones de software (Authenticode, APK signing). - PGP/GPG: Cifrado de emails y archivos.
- SSH: Autenticación con clave pública RSA.
- Tarjetas inteligentes: DNI electrónico, tarjetas bancarias,
pasaportes.
🧪 Código Python — RSA
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5
from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long
import base64
import math
# ─── Generar par de claves ────────────────────────────────
def rsa_generar_claves(bits: int = 2048) -> dict:
key = RSA.generate(bits)
return {
"privada": key.export_key().decode(),
"publica": key.publickey().export_key().decode(),
"n": key.n,
"e": key.e,
"d": key.d,
"p": key.p,
"q": key.q,
}
# ─── Cifrar / Descifrar con PKCS#1 OAEP (seguro) ─────────
def rsa_encrypt(plaintext: str, pub_key_pem: str) -> str:
key = RSA.import_key(pub_key_pem)
cipher = PKCS1_OAEP.new(key)
return base64.b64encode(cipher.encrypt(plaintext.encode())).decode()
def rsa_decrypt(ciphertext: str, priv_key_pem: str) -> str:
key = RSA.import_key(priv_key_pem)
cipher = PKCS1_OAEP.new(key)
decoded = base64.b64decode(ciphertext)
return cipher.decrypt(decoded).decode()
# ─── Cifrado directo (m^e mod n) para CTFs ───────────────
def rsa_raw_encrypt(message: int, e: int, n: int) -> int:
return pow(message, e, n)
def rsa_raw_decrypt(ciphertext: int, d: int, n: int) -> int:
return pow(ciphertext, d, n)
# ─── Ataque: e pequeño (m^3 < n) ─────────────────────────
def rsa_attack_small_e(ciphertext: int, e: int) -> int:
"""Si m^e < n, calcular la raíz e-ésima entera."""
def iroot(k, n):
u, s = n, n + 1
while u < s:
s = u
t = (k - 1) * s + n // pow(s, k - 1)
u = t // k
return s
return iroot(e, ciphertext)
# ─── Ataque: Wiener (d pequeño) ──────────────────────────
def rsa_attack_wiener(n: int, e: int) -> int:
"""Recupera d si d < n^0.25 / 3 usando fracciones continuas."""
def continued_fraction(num, den):
cf = []
while den:
q = num // den
cf.append(q)
num, den = den, num - q * den
return cf
def convergents_from_cf(cf):
convs = []
for i in range(len(cf)):
if i == 0:
num, den = cf[0], 1
elif i == 1:
num = cf[0] * cf[1] + 1
den = cf[1]
else:
num = cf[i] * num_prev + num_prev2
den = cf[i] * den_prev + den_prev2
num_prev2, den_prev2 = num_prev, den_prev
num_prev, den_prev = num, den
convs.append((num, den))
if i == 0:
num_prev2, den_prev2 = 1, 0
return convs
cf = continued_fraction(e, n)
convs = convergents_from_cf(cf)
for k, d in convs:
if k == 0:
continue
if (e * d - 1) % k != 0:
continue
phi = (e * d - 1) // k
# n = pq, phi = (p-1)(q-1) = pq - p - q + 1 = n - p - q + 1
# p+q = n - phi + 1
s = n - phi + 1
# (p-q)^2 = (p+q)^2 - 4n
discriminant = s * s - 4 * n
if discriminant < 0:
continue
sqrt_disc = int(math.isqrt(discriminant))
if sqrt_disc * sqrt_disc == discriminant:
return d
return None
# ─── Factorizar con MCD compartido ───────────────────────
def rsa_common_factor(n1: int, n2: int) -> int:
"""Si dos n comparten un factor primo, lo devuelve."""
return math.gcd(n1, n2)
# ─── Cargar clave desde PEM ──────────────────────────────
def rsa_parse_pem(pem: str) -> dict:
key = RSA.import_key(pem)
return {
"n": key.n,
"e": key.e,
"d": key.d if key.has_private() else None,
}
7. ChaCha20
ChaCha20 es un stream cipher moderno diseñado por Daniel J. Bernstein
en 2008 (variante mejorada de Salsa20). Es el estándar de cifrado simétrico
en TLS 1.3 y el único cifrado de WireGuard.
Genera un keystream pseudoaleatorio a partir de una clave (256 bits),
un nonce (96 bits) y un contador (32 bits). Luego XORea ese keystream
con el texto original. Es extremadamente rápido en software (más que AES
sin aceleración hardware).
Usos en CTF
- Nonce reutilizado: Como con cualquier stream cipher, dos mensajes
con el mismo nonce permiten recuperar el XOR de los textos (crib dragging). - Contador manipulado: Si controlas el contador, puedes descifrar
bloques arbitrarios. - Clave derivada de contraseña débil: La clave de ChaCha20 puede
derivarse de una contraseña con un KDF débil. - Identificación: El output de ChaCha20 es indistinguible de
aleatorio.
Usos en el mundo real
- TLS 1.3:
TLS_CHACHA20_POLY1305_SHA256es uno de los cifrados
estrella (junto con AES-GCM). - WireGuard: El protocolo VPN moderno usa exclusivamente ChaCha20
- Poly1305.
- OpenSSH: Soporta ChaCha20-Poly1305 desde OpenSSH 6.5 (2014).
- Google: Usado en Google Cloud y Google Apps.
- DNS over HTTPS: Algunas implementaciones usan ChaCha20.
- IoT: Excelente para dispositivos sin aceleración AES hardware
(ESP32, Arduino, etc.).
🧪 Código Python — ChaCha20
from Crypto.Cipher import ChaCha20
import base64
import os
# ─── Cifrar / Descifrar ───────────────────────────────────
def chacha20_encrypt(plaintext: str, key: bytes = None, nonce: bytes = None) -> dict:
"""Clave: 32 bytes. Nonce: 12 bytes (recomendado) u 8 bytes."""
key = key or os.urandom(32)
nonce = nonce or os.urandom(12)
cipher = ChaCha20.new(key=key, nonce=nonce)
return {
"key": base64.b64encode(key).decode(),
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(cipher.encrypt(plaintext.encode())).decode(),
}
def chacha20_decrypt(key_b64: str, nonce_b64: str, ciphertext_b64: str) -> str:
key = base64.b64decode(key_b64)
nonce = base64.b64decode(nonce_b64)
cipher = ChaCha20.new(key=key, nonce=nonce)
decoded = base64.b64decode(ciphertext_b64)
return cipher.decrypt(decoded).decode()
# ─── ChaCha20-Poly1305 (autenticado) ──────────────────────
from Crypto.Cipher import ChaCha20_Poly1305
def chacha20_poly1305_encrypt(plaintext: str, aad: bytes = None) -> dict:
"""Autenticado: incluye tag de integridad."""
key = os.urandom(32)
nonce = os.urandom(12)
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
if aad:
cipher.update(aad)
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode())
return {
"key": base64.b64encode(key).decode(),
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
"tag": base64.b64encode(tag).decode(),
"aad": base64.b64encode(aad).decode() if aad else None,
}
def chacha20_poly1305_decrypt(key_b64: str, nonce_b64: str,
ciphertext_b64: str, tag_b64: str,
aad_b64: str = None) -> str:
key = base64.b64decode(key_b64)
nonce = base64.b64decode(nonce_b64)
tag = base64.b64decode(tag_b64)
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
if aad_b64:
cipher.update(base64.b64decode(aad_b64))
decoded = base64.b64decode(ciphertext_b64)
try:
return cipher.decrypt_and_verify(decoded, tag).decode()
except (ValueError, KeyError) as e:
return f"[ERROR] Integridad comprometida: {e}"
# ─── Ataque: nonce reutilizado ────────────────────────────
def chacha20_nonce_reuse(c1: bytes, c2: bytes) -> bytes:
"""Si mismo nonce y clave, c1 ⊕ c2 = m1 ⊕ m2."""
return bytes(a ^ b for a, b in zip(c1, c2))
# ─── Extraer keystream (si conoces el original y cifrado) ─
def chacha20_extract_keystream(plaintext: bytes, ciphertext: bytes) -> bytes:
return bytes(a ^ b for a, b in zip(plaintext, ciphertext))
8. Blowfish / Twofish
Blowfish: Diseñado por Bruce Schneier en 1993. Bloques de 64 bits,
claves de 32 a 448 bits. Fue uno de los primeros cifrados sin patentes
ni licencias. Rápido y gratuito.
Twofish: Sucesor de Blowfish, finalista del concurso AES en 1998-2000.
Bloques de 128 bits, claves hasta 256 bits. Diseñado por el mismo equipo
(Counterpane). No ganó el concurso (perdió contra Rijndael/AES), pero
sigue siendo seguro y usado en sistemas legacy.
Ambos son cifrados de bloques. Blowfish usa bloques de 64 bits
(como DES), lo que lo hace vulnerable a Sweet32. Twofish usa 128 bits
(como AES).
Usos en CTF
- Blowfish con clave corta: El setup de clave de Blowfish es lento
(521 rondas de expansión), pero una vez expandido es rápido. Claves
de 4 bytes son triviales de forzar. - Sweet32 en Blowfish: Blowfish tiene bloques de 64 bits. Después
de ~32 GB de datos cifrados aparecen colisiones. Usado en retos
que simulan ataques a TLS. - bcrypt: Internamente bcrypt usa Blowfish (de ahí el nombre).
Blowfish con un cost factor elevado es la base de bcrypt. - Twofish en TrueCrypt/VeraCrypt: El disco encriptado puede usar
Twofish. CTFs de forense donde hay que descifrar un volumen.
Usos en el mundo real
- bcrypt: Usa Blowfish modificado (Blowfish + expansión de clave).
- TrueCrypt / VeraCrypt: Ofrecían Twofish como algoritmo de cifrado
para volúmenes encriptados. - SSH: Blowfish fue soportado por OpenSSH (hoy deshabilitado).
- Retiro activo: Blowfish está en retiro por su bloque de 64 bits.
Twofish es seguro pero no es estándar (poco uso práctico).
🧪 Código Python — Blowfish / Twofish
from Crypto.Cipher import Blowfish
from Crypto.Util.Padding import pad, unpad
import base64
import os
# ─── Blowfish (bloques de 64 bits) ────────────────────────
def blowfish_encrypt(plaintext: str, key: bytes) -> str:
cipher = Blowfish.new(key, Blowfish.MODE_ECB)
padded = pad(plaintext.encode(), Blowfish.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode()
def blowfish_decrypt(ciphertext: str, key: bytes) -> str:
cipher = Blowfish.new(key, Blowfish.MODE_ECB)
decoded = base64.b64decode(ciphertext)
return unpad(cipher.decrypt(decoded), Blowfish.block_size).decode()
def blowfish_cbc_encrypt(plaintext: str, key: bytes, iv: bytes = None) -> dict:
iv = iv or os.urandom(8) # Blowfish block size = 8
cipher = Blowfish.new(key, Blowfish.MODE_CBC, iv)
padded = pad(plaintext.encode(), Blowfish.block_size)
return {
"iv": base64.b64encode(iv).decode(),
"ciphertext": base64.b64encode(cipher.encrypt(padded)).decode(),
}
def blowfish_cbc_decrypt(ciphertext: str, key: bytes, iv_b64: str) -> str:
iv = base64.b64decode(iv_b64)
cipher = Blowfish.new(key, Blowfish.MODE_CBC, iv)
decoded = base64.b64decode(ciphertext)
return unpad(cipher.decrypt(decoded), Blowfish.block_size).decode()
# ─── Sweet32: detectar vulnerabilidad en Blowfish ────────
def blowfish_sweet32_vulnerable(mb_cifrados: float) -> str:
"""Sweet32 se vuelve práctico después de ~32 GB de datos."""
if mb_cifrados >= 32000:
return "VULNERABLE: suficientes bloques para ataque de cumpleaños"
prob = (mb_cifrados * 1024 * 1024) / (2**33)
return f"Probabilidad de colisión: ~{prob:.4f}%"
# ─── Twofish (128-bit blocks) ────────────────────────────
# Twofish no está en pycryptodome. Usar biblioteca externa.
# pip install twofish o usar cryptography.io
def twofish_disponible() -> bool:
try:
import twofish
return True
except ImportError:
return False
# Nota: si se necesita Twofish, instalar:
# pip install twofish
# from twofish import Twofish
# key = b"\\x00" * 16 # 128, 192 o 256 bits
# cipher = Twofish(key)
# plaintext = b"Hello World!!!!" # múltiplo de 16
# encrypted = cipher.encrypt(plaintext)
# decrypted = cipher.decrypt(encrypted)
9. Fernet (Symmetric + Authentication)
Fernet es un formato de cifrado simétrico todo-en-uno definido en
la especificación FERNET.
Incluye automáticamente:
- Clave: 256 bits (128 para AES, 128 para HMAC-SHA256)
- Cifrado: AES-128-CBC
- Autenticación: HMAC-SHA256 sobre el ciphertext + timestamp
- Protección contra replay: Timestamp integrado
- Serialización: Base64URL-safe
Es el cifrado más fácil de usar correctamente en Python. Una sola función
hace todo: generar clave, cifrar con IV aleatorio, firmar con HMAC,
serializar a string. Y una para descifrar y verificar.
Usos en CTF
- Clave hardcodeada: El error más común. La clave Fernet está en el
código fuente del servidor. Si la encuentras (código, backup, dump),
descifras todos los tokens. - Token de sesión: Algunos CTFs usan Fernet como token de sesión.
Si obtienes la clave, puedes forjar tokens de cualquier usuario. - Timestamp manipulation: Fernet incluye un timestamp. Si la
validación de tiempo es laxa, puedes reusar tokens antiguos. - Identificación: Los tokens Fernet comienzan con
gAAAAA...
(Base64URL-safe con ~150-200 caracteres).
Usos en el mundo real
- Flask (Python):
flask.url_safe_serializery sesiones de Flask
pueden usar Fernet internamente. - Apache Airflow: Usa Fernet para cifrar conexiones a bases de
datos y variables de entorno en el metastore. - Tus propios proyectos: Ideal para cifrar datos sensibles en
aplicaciones Python sin riesgos de implementación incorrecta
(olvidar HMAC, IV fijo, etc.). - Almacenamiento de secretos: Cifrar tokens de API, contraseñas
de servicios, o configuraciones sensibles en disco.
🧪 Código Python — Fernet
from cryptography.fernet import Fernet as FernetCipher, InvalidToken
import base64
# ─── Generar clave ────────────────────────────────────────
def fernet_generar_clave() -> str:
"""Genera una clave Fernet de 256 bits (32 bytes en Base64URL)."""
return FernetCipher.generate_key().decode()
# ─── Cifrar ───────────────────────────────────────────────
def fernet_encrypt(plaintext: str, key: str) -> str:
"""Devuelve un token Fernet. Incluye timestamp + IV + HMAC."""
cipher = FernetCipher(key.encode())
return cipher.encrypt(plaintext.encode()).decode()
# ─── Descifrar ────────────────────────────────────────────
def fernet_decrypt(token: str, key: str) -> str:
"""Descifra y verifica integridad. Lanza InvalidToken si falla."""
cipher = FernetCipher(key.encode())
try:
return cipher.decrypt(token.encode()).decode()
except InvalidToken as e:
return f"[ERROR] Token inválido: {e}"
# ─── Descifrar sin verificar timestamp (tolerar expirados) ─
def fernet_decrypt_no_ttl(token: str, key: str) -> str:
"""Descifra incluso si el token expiró."""
cipher = FernetCipher(key.encode())
try:
return cipher.decrypt(token.encode(), ttl=None).decode()
except InvalidToken as e:
return f"[ERROR] Token inválido: {e}"
# ─── Extraer metadatos del token ──────────────────────────
def fernet_parse_token(token: str) -> dict:
"""Analiza un token Fernet sin descifrarlo."""
import base64 as b64
import struct
import time
try:
raw = b64.urlsafe_b64decode(token + "==")
except:
return {"error": "token inválido"}
if len(raw) < 57:
return {"error": "token demasiado corto"}
version = raw[0]
timestamp = struct.unpack(">Q", raw[1:9])[0]
iv = raw[9:25]
ciphertext = raw[25:-32]
hmac_sig = raw[-32:]
return {
"version": version,
"timestamp": timestamp,
"fecha": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(timestamp)),
"iv_hex": iv.hex(),
"ciphertext_len": len(ciphertext),
"hmac_hex": hmac_sig.hex(),
"expired": time.time() > timestamp + 86400, # default TTL = 24h
}
# ─── Forzar token desde la nada (si tienes la clave) ─────
def fernet_forjar(payload: str, key: str) -> str:
"""Crea un token Fernet con cualquier payload."""
return fernet_encrypt(payload, key)
# ─── Detectar si una cadena es un token Fernet ────────────
import re
def es_fernet_token(cadena: str) -> bool:
"""Los tokens Fernet comienzan con gAAAAA y son Base64URL-safe."""
return bool(re.fullmatch(r'[A-Za-z0-9\-_]+={0,2}', cadena)) and cadena.startswith("gAAAAA")
10. Identificador de Cifrado
Combina detectores de todos los cifrados anteriores para identificar
automáticamente qué tipo de cifrado se usó, basándose en formato,
longitud, prefijos y propiedades.
🧪 Código Python — Identificador de cifrado
import re
import base64
CIPHER_PATTERNS = [
("Fernet", lambda s: s.startswith("gAAAAA") or s.startswith("gAAAAAB"),
"Clave simétrica + HMAC"),
("RSA (PEM pub)", lambda s: s.startswith("-----BEGIN RSA PUBLIC KEY-----"),
"Clave pública RSA"),
("RSA (PEM priv)",lambda s: s.startswith("-----BEGIN RSA PRIVATE KEY-----"),
"Clave privada RSA"),
("SSH pub key", lambda s: s.startswith("ssh-rsa") or s.startswith("ssh-ed25519"),
"Clave pública SSH"),
("Base64 (AES?)", lambda s: bool(re.fullmatch(r'[A-Za-z0-9+/]+={0,2}', s)) and len(s) > 20 and len(s) % 4 == 0,
"Posible cifrado en Base64"),
]
def identificar_cifrado(data: str) -> list:
"""Identifica posibles cifrados en una cadena."""
results = []
for nombre, detector, desc in CIPHER_PATTERNS:
if detector(data):
results.append({"tipo": nombre, "descripcion": desc})
if not results:
results.append({"tipo": "desconocido", "descripcion": "No se pudo identificar el formato"})
return results
# ─── Detectar modo de cifrado por longitud ────────────────
def detectar_modo_aes_por_longitud(ciphertext_b64: str) -> str:
"""Estima el modo AES según la longitud del Base64."""
raw_len = len(base64.b64decode(ciphertext_b64))
block_size = 16 # AES block
if raw_len % block_size == 0:
return "ECB o CBC (requiere IV/vetor)"
else:
return "CTR, GCM o CFB (streaming, no requiere padding)"
# ─── Detectar algoritmo por tamaño de clave ───────────────
def detectar_algoritmo_por_clave(key_b64: str) -> str:
"""Identifica algoritmo probable según tamaño de clave."""
key_len = len(base64.b64decode(key_b64))
return {
8: "DES (56 bits efectivos)",
16: "AES-128 / ChaCha20-128 / 3DES-2key",
24: "3DES-3key / AES-192",
32: "AES-256 / ChaCha20-256",
}.get(key_len, f"Clave de {key_len} bytes (inusual)")
# ─── Detectar RSA por longitud de n ───────────────────────
def detectar_rsa_por_longitud(n_bits: int) -> str:
if n_bits < 1024:
return f"RSA-{n_bits} (ROTO: factorizable)"
elif n_bits < 2048:
return f"RSA-{n_bits} (DÉBIL: desaconsejado)"
elif n_bits == 2048:
return f"RSA-{n_bits} (SEGURO: estándar actual)"
elif n_bits == 4096:
return f"RSA-{n_bits} (EXCESIVO: sobrado para casi todo)"
else:
return f"RSA-{n_bits} bits"
# ─── Analizador completo ──────────────────────────────────
def analizar_material_criptografico(data: str) -> dict:
return {
"longitud": len(data),
"primeros_50": data[:50],
"posibles_tipos": identificar_cifrado(data),
"es_base64": bool(re.fullmatch(r'[A-Za-z0-9+/]+={0,2}', data.strip())),
"es_hex": bool(re.fullmatch(r'[0-9a-fA-F]+', data.strip())),
"tiene_prefijo_pem": "-----BEGIN" in data,
}
Bonus: Comparación de velocidad y seguridad
| # | Algoritmo | Tipo | Clave | Velocidad (relativa) | Seguridad |
|---|---|---|---|---|---|
| 1 | XOR | Stream cipher | 1-N bytes | ★★★★★ (nativa) | Ninguna sola |
| 2 | Caesar/ROT | Sustitución | Shift (0-25) | ★★★★★ | Ninguna |
| 3 | Vigenère | Polialfabético | Palabra | ★★★★★ | Ninguna (clave corta) |
| 4 | AES | Block cipher | 128/192/256 | ★★★★☆ (HW: AES-NI) | Excelente |
| 5 | DES/3DES | Block cipher | 56/112/168 | ★★★☆☆ | Roto / En retiro |
| 6 | RSA | Asimétrico | 2048-4096 | ★☆☆☆☆ (muy lento) | Buena (con tamaño adecuado) |
| 7 | ChaCha20 | Stream cipher | 256 | ★★★★★ (SW) | Excelente |
| 8 | Blowfish/Twofish | Block cipher | 32-448 / 256 | ★★★★☆ | Buena (Blowfish: bloques 64b) |
| 9 | Fernet | Simétrico + MAC | 256 | ★★★★☆ | Excelente (fácil de usar) |
| 10 | Identificador | — | — | — | — |
Modos de operación de AES: guía rápida
| Modo | Tipo | Vulnerabilidades | Cuándo evitarlo |
|---|---|---|---|
| ECB | Determínistico | Misma entrada = misma salida | Siempre (excepto CTFs) |
| CBC | Probabilístico | Padding oracle, bit flipping | Si hay oráculo de padding |
| CTR | Stream | Nonce reutilizado = fatal | Si no tienes nonce único |
| GCM | Autenticado | Nonce reutilizado = fatal | Si no tienes nonce único |
| CCM | Autenticado | Igual que GCM | Si no tienes nonce único |
Regla de oro: En producción, usa siempre un modo autenticado
(GCM o ChaCha20-Poly1305). Nunca uses ECB. Siempre usa un nonce/IV
único y aleatorio.
🧪 Código Python — Benchmark de velocidad
import time
import os
from Crypto.Cipher import AES, ChaCha20, Blowfish, DES
def benchmark_cipher(nombre: str, encrypt_fn, data: bytes, iterations: int = 1000):
start = time.perf_counter()
for _ in range(iterations):
encrypt_fn(data)
elapsed = time.perf_counter() - start
mb_per_sec = (len(data) * iterations) / (1024 * 1024 * elapsed)
print(f"{nombre:15s} → {mb_per_sec:>8.1f} MB/s")
data = b"A" * (1024 * 1024) # 1 MB de datos
key_16 = os.urandom(16)
key_32 = os.urandom(32)
iv_16 = os.urandom(16)
nonce_12 = os.urandom(12)
# benchmark_cipher("AES-128-CBC", lambda d: AES.new(key_16, AES.MODE_CBC, iv_16).encrypt(d), data)
# benchmark_cipher("AES-256-CBC", lambda d: AES.new(key_32, AES.MODE_CBC, iv_16).encrypt(d), data)
# benchmark_cipher("ChaCha20", lambda d: ChaCha20.new(key=key_32, nonce=nonce_12).encrypt(d), data)
# benchmark_cipher("Blowfish", lambda d: Blowfish.new(key_16, Blowfish.MODE_CBC, nonce_12[:8]).encrypt(d), data[:8000])
# benchmark_cipher("DES", lambda d: DES.new(key_16[:8], DES.MODE_CBC, nonce_12[:8]).encrypt(d[:8000]), data[:8000])
Conclusión
La encriptación es la herramienta fundamental para la confidencialidad
de los datos. Cada algoritmo tiene su lugar:
- En CTFs: domina XOR (single-byte, multibyte, crib dragging), los
ataques a AES (ECB byte-at-a-time, padding oracle, bit flipping) y
las vulnerabilidades de RSA (factorización, e pequeño, Wiener). - En el mundo real: usa AES-GCM o ChaCha20-Poly1305 para cifrado
simétrico, RSA/ECC para intercambio de claves y firmas, y Fernet
si trabajas en Python y necesitas hacerlo bien a la primera. - Nunca: implementes tu propio cifrado, reuses nonces/IVs, uses
ECB, o confíes en Caesar/Vigenère para nada serio.
Este artículo completa la trilogía. Artículos anteriores:
- Encoding — representación reversible de datos
- Hashing — funciones unidireccionales, identificación y cracking