Introducción

Top 10 – Parte 3: Encriptación

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

PropiedadEncodingHashingEncriptación
ReversibleSí (sin clave)NoSí (con clave)
Requiere claveNoNo
Longitud salidaVariableFijaVariable (≈ input)
Uso principalTransporteIntegridadConfidencialidad

Tipos de encriptación

TipoClavesEjemplosVelocidadUsos
SimétricaUna sola clave compartidaAES, ChaCha20, DESRápidaCifrado de datos, archivos, tráfico
AsimétricaPar público/privadoRSA, ECCLentaIntercambio 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.
    strings no 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:

ModoDescripciónVulnerabilidad
ECBCada bloque se cifra independientemente¡No oculta patrones! Misma entrada = mismo cifrado
CBCCada bloque se XORea con el cifrado anterior (o IV)Padding oracle, bit flipping
CTRConvierte AES en stream cipher (cifra contadores)Reuso de nonce es fatal
GCMAutenticado (confidencialidad + integridad)Nonce reutilizado rompe todo
CCMAutenticado (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=Falseadmin=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) donde n = p × q
  • Clave privada: (n, d) donde d × 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.com es
    el mejor aliado.
  • e pequeño (e=3): Si el mensaje es pequeño, m^3 < n y 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_SHA256 es 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_serializer y 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

#AlgoritmoTipoClaveVelocidad (relativa)Seguridad
1XORStream cipher1-N bytes★★★★★ (nativa)Ninguna sola
2Caesar/ROTSustituciónShift (0-25)★★★★★Ninguna
3VigenèrePolialfabéticoPalabra★★★★★Ninguna (clave corta)
4AESBlock cipher128/192/256★★★★☆ (HW: AES-NI)Excelente
5DES/3DESBlock cipher56/112/168★★★☆☆Roto / En retiro
6RSAAsimétrico2048-4096★☆☆☆☆ (muy lento)Buena (con tamaño adecuado)
7ChaCha20Stream cipher256★★★★★ (SW)Excelente
8Blowfish/TwofishBlock cipher32-448 / 256★★★★☆Buena (Blowfish: bloques 64b)
9FernetSimétrico + MAC256★★★★☆Excelente (fácil de usar)
10Identificador

Modos de operación de AES: guía rápida

ModoTipoVulnerabilidadesCuándo evitarlo
ECBDetermínisticoMisma entrada = misma salidaSiempre (excepto CTFs)
CBCProbabilísticoPadding oracle, bit flippingSi hay oráculo de padding
CTRStreamNonce reutilizado = fatalSi no tienes nonce único
GCMAutenticadoNonce reutilizado = fatalSi no tienes nonce único
CCMAutenticadoIgual que GCMSi 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