Cypher : codes Python pour résoudre des énigmes avancées (3-4, 4-3, 5-*, 6-5)

Comment j'ai résolu des puzzles avancés (3-4, 4-3, 5-1, 5-2, 5-3, 6-5) en utilisant Python

 

Introduction

Ce guide montre comment j'ai résolu certaines des énigmes avancées à l'aide de Python. À l'origine, je résolvais les énigmes avec un crayon et des captures d'écran imprimées (comme l'image ci-dessous pour 3-3 ; ne cliquez pas pour zoomer si vous ne voulez pas être gâté), mais pour les problèmes ultérieurs, je me suis lassé des essais et des erreurs. J'ai donc demandé à Python de le faire.

Remarque : pour publier mes codes sur la communauté Steam, j'ai dû renommer les variables des index de liste de i à x afin d'éviter qu'elles ne soient analysées comme des balises de balisage (à mon avis, les balisages doivent être ignorés dans un élément de code).

3-4 : Analyse des fréquences

Le code ci-dessous compte la fréquence de chaque lettre. Une fois que c'est fait, assignez chaque lettre selon la sortie et ETAOIN SHRDLU, et vous identifierez les THE. Ensuite, le problème est facilement résolu avec un crayon et du papier.
from collections import Counter

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

print(Counter(text))


Le résultat (ne zoomez pas si vous ne voulez pas être spoilé) :

4-3 : Analyse des fréquences

Pour résoudre un chiffre par substitution polyalphabétique (chiffre de Vigenère), la première chose à faire est de déterminer la longueur de la clé. Selon l'indice du jeu, il y a (au moins) deux séquences répétées de 3 lettres : DUF et LUE. En comptant les étendues d'apparition pour chacun d'eux, puis en prenant leur plus grand diviseur commun, nous obtenons la longueur la plus probable de la clé : 3.

Une fois la longueur de la clé identifiée, on peut utiliser la technique d'analyse fréquentielle comme dans les chiffrements par substitution monoalphabétique. Le code ci-dessous divise le texte chiffré en « phases » en fonction de la lettre de la clé qui est appliquée, et compte la fréquence des lettres dans la phase.

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


Le code essaie également de trouver des séquences répétées de 3 lettres comme DUF et LUE. Je pensais qu'il n'était pas nécessaire de retrouver les séquences dans les phases autres que 1-2-3. Malheureusement, cela n'a pas suffi pour trouver ce qui est "LE" dans le texte en clair.

4-3 : Attaque par force brute

Après avoir essayé quelques décalages en fonction de l'analyse des fréquences, j'ai décidé de faire essayer mon ordinateur.

Le code génère plusieurs candidats pour la solution, nous devons donc choisir le bon manuellement.

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


Comme la partie supérieure droite de l'image peut le suggérer, à la suite de l'attaque par force brute, je savais que j'avais presque résolu le puzzle par analyse de fréquence, mais j'avais renoncé à ne pas être suffisamment confiant pour continuer. (Ne zoomez pas l'image si vous ne voulez pas être spoilé.)

5-1, 5-2, 5-3

Pour les puzzles de cryptographie mécanisés, nous pouvons écrire un code pour émuler les fonctionnalités d'Enigma. Nous pouvons utiliser le même code pour les résoudre tous sauf :

  • Pour le deuxième casse-tête, nous avons besoin d'essais et d'erreurs, ou d'un peu de force brute, pour déterminer la rotation initiale du disque.
  • Pour le dernier puzzle, nous avons besoin de la fonction pour échanger quelques paires de lettres.
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

Le casse-tête est simple : il suffit d'exécuter l'algorithme à l'envers. Cependant, en résolvant cela à la main, une petite erreur pourrait tout gâcher. C'est pourquoi j'ai utilisé (ou dû utiliser, je l'avoue) Python pour résoudre ce problème.

Notez que j'ai réinterprété l'algorithme : au lieu d'appliquer l'échange de lignes à la fois au texte et à la clé, puis de les associer par XOR, j'ai interprété l'instruction comme étant d'abord les associer par XOR, puis d'échanger les lignes, les résultats étant le même.

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

Laisser un commentaire

ArabicEnglishFrenchGermanItalianJapaneseKoreanPortugueseSpanish