Cypher: Códigos de Python para resolver rompecabezas avanzados (3-4, 4-3, 5-*, 6-5)

Cómo resolví algunos acertijos avanzados (3-4, 4-3, 5-1, 5-2, 5-3, 6-5) usando Python

 

Introducción

Esta guía muestra cómo resolví algunos de los acertijos avanzados con la ayuda de Python. Originalmente, estaba resolviendo los acertijos con un lápiz e imprimía capturas de pantalla (como la imagen de abajo para 3-3; no hagas clic para hacer zoom si no quieres que te echen a perder), pero para los problemas posteriores me cansé de pruebas y errores. Así que conseguí que Python lo hiciera.

Nota: para publicar mis códigos en Steam Community, tuve que cambiar el nombre de las variables para los índices de lista de i a x para evitar que se analizaran como etiquetas de marcado (en mi opinión, los marcados deberían ignorarse dentro de un elemento de código).

3-4: Análisis de frecuencia

El siguiente código cuenta la frecuencia de cada letra. Una vez hecho, asigna cada letra de acuerdo con la salida y ETAOIN SHRDLU, e identificarás los THE. Entonces el problema se resuelve fácilmente con lápiz y papel.
from collections import Counter

text = """
AJLPNYRJZJFLZYASGSKQGSMME
JKEJPFSVLPJLKKEJELNNSPZKY
NNYGNSGASGYZJGAKEYZYGASVW
NJKSWSKECNLUJZKEJZEYCYZWS
VOEKLGAHYKKJAZEJNYJZLKLGU
ESPPJLAFHSPZJLFSVGJRJPYTL
OYGJALZMJJKJPZUESSGJPLUEY
NATYOEKZLYNEJPKMSEVGAPJAK
SGZGLTJEYZCLGYSNL
""".replace("\n", "")

print(Counter(text))


El resultado (no hagas zoom si no quieres que te echen a perder):

4-3: Análisis de frecuencia

Para resolver un cifrado de sustitución polialfabético (cifrado de Vigenère), lo primero que hay que hacer es averiguar la longitud de la clave. De acuerdo con la sugerencia del juego, hay (al menos) dos secuencias repetidas de 3 letras: DUF y LUE. Contando los lapsos de aparición de cada uno de ellos, y luego tomando su máximo común divisor, obtenemos la longitud más probable de la clave: 3.

Una vez que se identifica la longitud de la clave, podemos utilizar la técnica de análisis de frecuencia como en los cifrados de sustitución monoalfabéticos. El siguiente código divide el texto cifrado en "fases" según la letra de la clave que se aplique y cuenta la frecuencia de las letras dentro de la fase.

from collections import Counter

text = """\
LAFLUIWOYWPADUFHSNBVSWVNDZQDUF
RBPLUYQPLWLPHZRLUEDUBSYMIPRDIJ
HTYQUCUZYLKFRSKHZBUHULUEKPQFOY
LYSSAMWOCWHZOLGDTDDPPOFDDTGOPY
UDGWOYOSDRYKVVDVLAULRZYGWPLJZY
QKYPTWVLJIAFHHSWOMUVDDAPLMJLUE
PVLRNPDWFXWMQAFHZSEQCFAGQDFLJF
LHLDSWCLMQLFXUBULBDUBVPVWFQHWY
UHRHJGSOCUZZXAGFVLILQVAFDARKPQ
LZCQAGULJBUCZAMPL\
""".replace("\n", "")

phase1 = "".join(text[x] for x in range(len(text)) if x % 3 == 0)
phase2 = "".join(text[x] for x in range(len(text)) if x % 3 == 1)
phase3 = "".join(text[x] for x in range(len(text)) if x % 3 == 2)

triplets = (text[x - 2] + text[x - 1] + text[x] for x in range(2, len(text), 3))

print(Counter(phase1))
print(Counter(phase2))
print(Counter(phase3))
print(Counter(triplets))


El código también intenta encontrar secuencias repetidas de 3 letras como DUF y LUE. Pensé que no era necesario encontrar las secuencias en las fases que no sean 1-2-3. Desafortunadamente, eso no fue suficiente para encontrar qué es "EL" en el texto sin formato.

4-3: Ataque de fuerza bruta

Después de probar algunos cambios de acuerdo con el análisis de frecuencia, decidí hacer que mi computadora probara.

El código genera varios candidatos para la solución, por lo que debemos seleccionar el correcto manualmente.

from string import ascii_uppercase as alphabets
from itertools import product

text = """\
LAFLUIWOYWPADUFHSNBVSWVNDZQDUF
RBPLUYQPLWLPHZRLUEDUBSYMIPRDIJ
HTYQUCUZYLKFRSKHZBUHULUEKPQFOY
LYSSAMWOCWHZOLGDTDDPPOFDDTGOPY
UDGWOYOSDRYKVVDVLAULRZYGWPLJZY
QKYPTWVLJIAFHHSWOMUVDDAPLMJLUE
PVLRNPDWFXWMQAFHZSEQCFAGQDFLJF
LHLDSWCLMQLFXUBULBDUBVPVWFQHWY
UHRHJGSOCUZZXAGFVLILQVAFDARKPQ
LZCQAGULJBUCZAMPL\
""".replace("\n", "")

def char_to_num(char):
    return alphabets.index(char) + 1

def vigenere_sub(char1, char2):
    return alphabets[(char_to_num(char1) - char_to_num(char2) - 1) % 26]

for key_chars in product(alphabets, repeat=3):
    decryption = ""
    for x in range(len(text)):
        decryption += vigenere_sub(text[x], key_chars[x % 3])
    if "THE" in decryption and "AND" in decryption:
        print(key_chars)
        print(decryption)
        print()


Como puede sugerir la parte superior derecha de la imagen, como resultado del ataque de fuerza bruta, supe que casi había resuelto el rompecabezas mediante el análisis de frecuencia, pero renuncié a no tener la confianza suficiente para continuar. (No hagas zoom en la imagen si no quieres que te echen a perder).

5-1, 5-2, 5-3

Para los acertijos de criptografía mecanizados, podemos escribir un código para emular las funcionalidades de Enigma. Podemos usar el mismo código para resolverlos todos excepto:

  • Para el segundo acertijo, necesitamos algo de prueba y error, o un poco de fuerza bruta, para determinar la rotación inicial del disco.
  • Para el último rompecabezas, necesitamos la función para intercambiar algunos pares de letras.
from string import ascii_uppercase as keyboard

scrambler1 = (keyboard, "UWYGADFPVZBECKMTHXSLRINQOJ")
scrambler2 = (keyboard, "AJPCZWRLFBDKOTYUQGENHXMIVS")
scrambler3 = (keyboard, "TAGBPCSDQEUFVNZHYIXJWLRKOM")
reflector = (keyboard, "YRUHQSLDPXNGOKMIEBFZCWVJAT")

def scramble(position, disk, backwards=False):
    row1, row2 = disk[::-1] if backwards else disk
    char = row1[position]
    return row2.index(char)

def rotate(disk, n):
    return (disk[0][n:] + disk[0][:n], disk[1][n:] + disk[1][:n])

def decrypt(char, *disks):
    x = keyboard.index(char)
    for disk in disks:
        x = scramble(x, disk)
    for disk in disks[-2::-1]:
        x = scramble(x, disk, True)
    return keyboard[x]

def decrypt_text(text, *disks):
    decryption = ""
    disk1 = disks[0]
    for char in text:
        disk1 = rotate(disk1, 1)
        decryption += decrypt(char, disk1, *disks[1:])
    return decryption

def swap(text, key):
    result = ""
    for char in text:
        result += key.replace(char, "") if char in key else char
    return result

def swap_keys(text, keys):
    for key in keys:
        text = swap(text, key)
    return text

def puzzle_5_1():
    print(decrypt_text("ZYDNI", scrambler1, reflector))

def puzzle_5_2():
    for i in range(26):
        scrambler = rotate(scrambler1, i)
        text = decrypt_text("QHSGUWIG", scrambler, reflector)
        if text[:2] == "XV":
            print(text)

def puzzle_5_3():
    text = "GYHRVFLRXY"
    keys = ("AB", "SZ", "UY", "GH", "LQ", "EN")
    text = swap_keys(text, keys)
    d1 = rotate(scrambler2, scrambler2[0].index("A"))
    d2 = rotate(scrambler1, scrambler1[0].index("E"))
    d3 = rotate(scrambler3, scrambler3[0].index("B"))
    text = decrypt_text(text, d1, d2, d3, reflector)
    text = swap_keys(text, keys)
    print(text)

puzzle_5_1()
puzzle_5_2()
puzzle_5_3()


6 - 5

El rompecabezas es simple: solo ejecuta el algoritmo al revés. Sin embargo, al resolver esto a mano, un pequeño error podría estropearlo todo. Es por eso que usé (o tuve que usar, lo confieso) Python para resolver esto.

Tenga en cuenta que reinterpreté el algoritmo: en lugar de aplicar el intercambio de filas tanto al texto como a la clave y luego XOR-ing ellos, interpreté la instrucción como primero XOR-ing ellos y luego intercambiar las filas, los resultados son lo mismo.

def block(bitstr):
    return (bitstr[:4], bitstr[4:8], bitstr[8:12], bitstr[12:])

def swap(rows):
    return tuple(row[1] + row[0] + row[3] + row[2] for row in rows)

def xor(rows, key):
    keybits = "".join(bin(ord(char))[2:].zfill(8) for char in key)
    if len(keybits) != 16: raise AssertionError
    textbits = "".join(rows)
    val = "".join("0" if keybits[x] == textbits[x] else "1" for x in range(16))
    return block(val)

def shift(rows):
    result = []
    for n in range(len(rows)):
        row = rows[n]
        result.append(row[n:] + row[:n])
    return tuple(result)

def decrypt(cipher, key):
    return xor(swap(shift(cipher)), key)

ciphertext = block("1001011110110101")
first = decrypt(ciphertext, "BX")
second = decrypt(first, "YS")
print(chr(int(second[0] + second[1], 2)))
print(chr(int(second[2] + second[3], 2)))

By unvollstaendigkeitssatz

Deja un comentario

ArabicEnglishFrenchGermanItalianJapaneseKoreanPortugueseSpanish