Inicio Linux & Systems Networks & Infrastructure Cybersecurity Cloud & DevOps SIEM & Monitoring DFIR & Threat Intel Development & Other Todas las categorias Herramientas Proyectos Sobre

Critpografía basica: Cifrado por sustitución

Critpografía basica: Cifrado por sustitución

Tabla de contenidos

En la primera parte de esta serie cubrimos los cifrados monoalfabéticos César, Polybios y ROT13, todos vulnerables al análisis de frecuencia porque cada letra siempre se sustituye por la misma letra cifrada. Los cifrados polialfabéticos resuelven parcialmente este problema usando múltiples alfabetos de sustitución, lo que aplana la distribución de frecuencias. En esta segunda parte exploraremos dos cifrados más sofisticados: Vigenère y Playfair.

Cifrado de Vigenère

El cifrado de Vigenère, publicado por Blaise de Vigenère en 1586, fue considerado indescifrable durante casi tres siglos. Utiliza una palabra clave que determina un desplazamiento diferente para cada letra del texto, creando efectivamente múltiples cifrados César aplicados en secuencia:

text
Texto:       A T A C A R   A L   A M A N E C E R
Clave:       C L A V E C   L A   V E C L A V E C
Desplaz.:    2 11 0 21 4 2  11 0  21 4 2 11 0 21 4 2
Cifrado:     C E A X E T   L L   V Q C Y E X I T

Fórmula: C(i) = (texto[i] + clave[i % len(clave)]) mod 26

La misma letra 'A' se cifra de forma diferente según su posición: como C, A, L, V, etc. Esto destruye los patrones de frecuencia del texto original.

python
def vigenere(texto, clave, descifrar=False):
    """Cifrado/descifrado de Vigenère."""
    resultado = []
    clave = clave.upper()
    j = 0  # Índice de la clave
    for char in texto:
        if char.isalpha():
            base = ord('A') if char.isupper() else ord('a')
            k = ord(clave[j % len(clave)]) - ord('A')
            if descifrar:
                k = -k
            cifrado = chr((ord(char) - base + k) % 26 + base)
            resultado.append(cifrado)
            j += 1
        else:
            resultado.append(char)
    return ''.join(resultado)

# Cifrar
texto = "ATACAR AL AMANECER"
clave = "CLAVE"
cifrado = vigenere(texto, clave)
print(f"Cifrado: {cifrado}")  # CEAXET LL VQCYEXIT

# Descifrar
descifrado = vigenere(cifrado, clave, descifrar=True)
print(f"Descifrado: {descifrado}")  # ATACAR AL AMANECER

Tabla de Vigenère (Tabula Recta)

El cifrado se visualiza tradicionalmente usando la Tabula Recta: una cuadrícula de 26x26 donde cada fila es un cifrado César con desplazamiento diferente. Para cifrar, se busca la intersección entre la fila de la letra de la clave y la columna de la letra del texto:

python
def generar_tabula_recta():
    """Generar y mostrar la Tabula Recta de Vigenère."""
    print("    " + " ".join(chr(i) for i in range(65, 91)))
    print("   " + "-" * 52)
    for fila in range(26):
        letra_fila = chr(65 + fila)
        alfabeto = "".join(
            chr((fila + col) % 26 + 65) for col in range(26)
        )
        print(f"{letra_fila} | {' '.join(alfabeto)}")

generar_tabula_recta()
# Salida (primeras filas):
#     A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
#    ----------------------------------------------------
# A | A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
# B | B C D E F G H I J K L M N O P Q R S T U V W X Y Z A
# C | C D E F G H I J K L M N O P Q R S T U V W X Y Z A B

Criptoanálisis de Vigenère

Aunque Vigenère resistió siglos, fue roto por Charles Babbage (1854) y Friedrich Kasiski (1863). El ataque se realiza en dos fases:

1. Determinar la longitud de la clave (método Kasiski): Buscar secuencias repetidas en el texto cifrado. La distancia entre repeticiones es un múltiplo de la longitud de la clave.

python
from math import gcd
from functools import reduce
from collections import Counter

def kasiski(texto_cifrado, longitud_secuencia=3):
    """Método Kasiski para encontrar la longitud probable de la clave."""
    texto = texto_cifrado.replace(" ", "").upper()
    distancias = []

    for i in range(len(texto) - longitud_secuencia):
        secuencia = texto[i:i + longitud_secuencia]
        for j in range(i + 1, len(texto) - longitud_secuencia):
            if texto[j:j + longitud_secuencia] == secuencia:
                distancias.append(j - i)

    if distancias:
        longitud_clave = reduce(gcd, distancias)
        print(f"Distancias encontradas: {distancias}")
        print(f"MCD de distancias: {longitud_clave}")
        return longitud_clave
    return None

2. Análisis de frecuencia por posición: Una vez conocida la longitud de la clave, agrupar las letras por posición (todas las cifradas con la misma letra de la clave) y aplicar análisis de frecuencia a cada grupo como si fuera un cifrado César independiente.

python
def romper_vigenere(texto_cifrado, longitud_clave):
    """Romper Vigenère conociendo la longitud de la clave."""
    texto = texto_cifrado.replace(" ", "").upper()
    # Frecuencia esperada de la E en español: ~13.7%
    freq_e_espanol = 4  # Posición de E en el alfabeto

    clave = []
    for i in range(longitud_clave):
        grupo = texto[i::longitud_clave]
        freq = Counter(grupo)
        letra_mas_comun = freq.most_common(1)[0][0]
        # Asumimos que la letra más frecuente corresponde a E
        desplazamiento = (ord(letra_mas_comun) - ord('E')) % 26
        clave.append(chr(desplazamiento + ord('A')))

    clave_encontrada = ''.join(clave)
    print(f"Clave probable: {clave_encontrada}")
    return clave_encontrada

Cifrado Playfair

El cifrado Playfair, inventado por Charles Wheatstone en 1854 y popularizado por Lord Playfair, opera sobre pares de letras (digramas) en lugar de letras individuales. Esto lo hace significativamente más resistente al análisis de frecuencia simple, ya que hay 600 digramas posibles frente a solo 26 letras.

El cifrado usa una cuadrícula 5x5 construida a partir de una palabra clave:

text
Clave: SEGURIDAD (eliminar duplicados: SEGURIDA)

Cuadrícula (I=J, rellenar con letras restantes):

  S E G U R
  I D A B C
  F H K L M
  N O P Q T
  V W X Y Z

Reglas de cifrado por digramas:
1. Misma fila    → cada letra se reemplaza por la siguiente a la derecha (circular)
2. Misma columna → cada letra se reemplaza por la siguiente abajo (circular)
3. Rectángulo    → cada letra toma la letra de su fila en la columna de la otra
python
def generar_cuadricula_playfair(clave):
    """Generar la cuadrícula 5x5 de Playfair."""
    clave = clave.upper().replace('J', 'I')
    vista = []
    for char in clave + "ABCDEFGHIKLMNOPQRSTUVWXYZ":
        if char not in vista and char.isalpha():
            vista.append(char)
    return [vista[i:i+5] for i in range(0, 25, 5)]

def encontrar_posicion(cuadricula, letra):
    """Encontrar fila y columna de una letra en la cuadrícula."""
    for i, fila in enumerate(cuadricula):
        if letra in fila:
            return i, fila.index(letra)
    return None

def preparar_texto(texto):
    """Preparar texto para Playfair: digramas sin letras repetidas."""
    texto = texto.upper().replace('J', 'I').replace(' ', '')
    resultado = []
    i = 0
    while i < len(texto):
        if i + 1 < len(texto) and texto[i] == texto[i+1]:
            resultado.append(texto[i] + 'X')
            i += 1
        elif i + 1 < len(texto):
            resultado.append(texto[i] + texto[i+1])
            i += 2
        else:
            resultado.append(texto[i] + 'X')
            i += 1
    return resultado

def playfair_cifrar(texto, clave):
    """Cifrar texto con Playfair."""
    cuad = generar_cuadricula_playfair(clave)
    digramas = preparar_texto(texto)
    resultado = []

    for digrama in digramas:
        f1, c1 = encontrar_posicion(cuad, digrama[0])
        f2, c2 = encontrar_posicion(cuad, digrama[1])

        if f1 == f2:  # Misma fila
            resultado.append(cuad[f1][(c1+1) % 5] + cuad[f2][(c2+1) % 5])
        elif c1 == c2:  # Misma columna
            resultado.append(cuad[(f1+1) % 5][c1] + cuad[(f2+1) % 5][c2])
        else:  # Rectángulo
            resultado.append(cuad[f1][c2] + cuad[f2][c1])

    return ''.join(resultado)

# Ejemplo
clave = "SEGURIDAD"
texto = "HOLA MUNDO"
cifrado = playfair_cifrar(texto, clave)
print(f"Playfair cifrado: {cifrado}")

# Mostrar la cuadrícula
cuad = generar_cuadricula_playfair(clave)
for fila in cuad:
    print(' '.join(fila))

Comparativa de cifrados por sustitución

text
Cifrado    | Tipo           | Espacio de claves | Unidad    | Roto por
-----------|----------------|-------------------|-----------|------------------
César      | Monoalfabético | 25                | Letra     | Fuerza bruta
Polybios   | Fraccional     | 1                 | Letra     | Sin seguridad
ROT13      | Monoalfabético | 1                 | Letra     | Sin seguridad
Vigenère   | Polialfabético | 26^n              | Letra     | Kasiski (1863)
Playfair   | Poligráfico    | 25!               | Digrama   | Frecuencia digramas

Todos los cifrados clásicos por sustitución son inseguros frente a técnicas modernas. Su valor reside en la comprensión de los principios criptográficos fundamentales: confusión (cada letra del texto cifrado depende de varias partes de la clave) y difusión (cada letra del texto original influye en varias letras del texto cifrado). Estos mismos principios, aplicados con matemáticas más sofisticadas, son la base de los cifrados modernos como AES.

:wq!

Comentarios