Cypher: codici Python per risolvere enigmi avanzati (3-4, 4-3, 5-*, 6-5)

Come ho risolto alcuni enigmi avanzati (3-4, 4-3, 5-1, 5-2, 5-3, 6-5) usando Python

 

Introduzione

Questa guida mostra come ho risolto alcuni enigmi avanzati con l'aiuto di Python. Inizialmente risolvevo i puzzle con una matita e stampavo degli screenshot (come l'immagine qui sotto per 3-3; non fare clic per ingrandire se non vuoi essere viziato), ma per problemi successivi mi sono stancato di prove ed errori. Quindi ho chiesto a Python di farlo.

Nota: per pubblicare i miei codici nella Comunità di Steam, ho dovuto rinominare le variabili per gli indici degli elenchi da i a x per evitare che venissero analizzati come tag di markup (secondo me i markup dovrebbero essere ignorati all'interno di un elemento di codice).

3-4: Analisi della frequenza

Il codice seguente conta la frequenza di ogni lettera. Una volta fatto, assegna ogni lettera in base all'output e ETAOIN SHRDLU e identificherai i THE. Quindi il problema si risolve facilmente con carta e matita.
from collections import Counter

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

print(Counter(text))


Il risultato (non zoomare se non vuoi essere viziato):

4-3: Analisi della frequenza

Per risolvere un cifrario di sostituzione polialfabetico (cifrario di Vigenère), la prima cosa da fare è calcolare la lunghezza della chiave. Secondo il suggerimento in-game, ci sono (almeno) due sequenze ripetute di 3 lettere: DUF e LUE. Contando gli intervalli di apparizione per ciascuno di essi, e quindi prendendo il loro massimo comun divisore, otteniamo la lunghezza più probabile della chiave: 3.

Una volta identificata la lunghezza della chiave, possiamo utilizzare la tecnica dell'analisi della frequenza come nei cifrari a sostituzione monoalfabetica. Il codice seguente divide il testo cifrato in "fasi" a seconda della lettera della chiave applicata e conta la frequenza delle lettere all'interno della 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))


Il codice cerca anche di trovare sequenze ripetute di 3 lettere come DUF e LUE. Ho pensato che non fosse necessario trovare le sequenze nelle fasi diverse da 1-2-3. Sfortunatamente ciò non è stato sufficiente per trovare ciò che è "THE" nel testo in chiaro.

4-3: Attacco di forza bruta

Dopo aver provato alcuni turni in base all'analisi della frequenza, ho deciso di far provare il mio computer.

Il codice restituisce diversi candidati per la soluzione, quindi dobbiamo raccogliere quello corretto 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()


Come potrebbe suggerire la parte in alto a destra dell'immagine, a causa dell'attacco di forza bruta, sapevo di aver quasi risolto l'enigma mediante l'analisi della frequenza, ma ho rinunciato a non essere abbastanza sicuro per procedere. (Non ingrandire l'immagine se non vuoi essere rovinato.)

5-1, 5-2, 5-3

Per i puzzle di crittografia meccanizzata, possiamo scrivere un codice per emulare le funzionalità di Enigma. Possiamo usare lo stesso codice per risolverli tutti tranne:

  • Per il secondo puzzle, abbiamo bisogno di tentativi ed errori, o un po' di forza bruta, per determinare la rotazione iniziale del disco.
  • Per l'ultimo puzzle, abbiamo bisogno della funzione per scambiare alcune coppie di lettere.
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

Il puzzle è semplice: basta eseguire l'algoritmo all'indietro. Tuttavia, risolvendolo a mano, un piccolo errore potrebbe rovinare tutto. Ecco perché ho usato (o dovuto usare, lo confesso) Python per risolvere questo problema.

Si noti che ho reinterpretato l'algoritmo: invece di applicare lo scambio di riga sia al testo che alla chiave e quindi XOR-ing loro, ho interpretato l'istruzione come prima XOR-ing e poi scambiare le righe, i risultati sono lo stesso.

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

Lascia un tuo commento

ArabicEnglishFrenchGermanItalianJapaneseKoreanPortugueseSpanish