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
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):
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)))