On utilise le module Py-Enigma pour simuler la machine Enigma.
Le module itertools est commode pour énumérer les permutations d'objets : il évite les boucles imbriquées.
from enigma.machine import EnigmaMachine
import itertools
import time
Une fonction de score permet au programme de savoir si une chaine de caractères a une bonne chance d'être en "bon français".
Pour les détails, voir le début de substitution_mono.
Le fichier brut4g_fr.txt contient les effectifs des quadrigrammes obtenus à partir d'un corpus de textes français.
from math import log10
f4g ={}
f = open('brut4g_fr.txt') # statistiques des quadrigrammes français
total = 0 # effectif total
for line in f:
(w, c) = line.split(sep= ' ')
f4g[w] = int(c)
total += int(c)
for w in f4g:
f4g[w] = -log10(f4g[w]/total) # calcul des -log fréquences des 4-grammes
f.close()
def logscore(s):
logsum = 0
min_freq = 100 # logscore d'un 4gramme inexistant
for i in range(len(s)-3):
logsum += f4g.get(s[i:i+4], min_freq)
return logsum
logscore('TOUTVABIEN')
logscore('NEIBAVTUOT')
On est ramené à un problème d'optimisation : il faut obtenir la clé qui donne le logscore minimum.
------------------------------------------------------------------------
Echauffement 1 : Le message ci-dessous a été chiffré avec une machine
ENIGMA qui utilise le réflecteur B et les rotors I-II-III réglés sur
la position par défaut 1-1-1. Aucune fiche de connectée au plugboard.
------------------------------------------------------------------------
/i B-I-II-III 01-01-01
/e ???
------------------------------------------------------------------------
TVDOT JZXGW YPFFQ KTAUN YJLEQ NDXKZ ALYSH WHHMI MOWBW ZHHBY KUJDD URRGO
IPUHN VFJZE PAGVM JBYYA NGHCV DIJDL MGCIP FJODL ZJFNH VGPGU RFZLV ZOWMT
ZBNXM UZZRB NSVIN FLUXZ GTZZM FPYPE RKHJG ZYYQO SLZGM CQUNS VHXDM KNLRJ
QLSJK KZNNO YBKBX FMWKB FONTM HZGTQ WJ
------------------------------------------------------------------------
k1 = 'TVDOT JZXGW YPFFQ KTAUN YJLEQ NDXKZ ALYSH WHHMI MOWBW ZHHBY KUJDD URRGO\
IPUHN VFJZE PAGVM JBYYA NGHCV DIJDL MGCIP FJODL ZJFNH VGPGU RFZLV ZOWMT\
ZBNXM UZZRB NSVIN FLUXZ GTZZM FPYPE RKHJG ZYYQO SLZGM CQUNS VHXDM KNLRJ\
QLSJK KZNNO YBKBX FMWKB FONTM HZGTQ WJ'.replace(' ', '')
Work factor : $26^3 = 17576$.
start = time.time()
best_score = float('inf')
machine = EnigmaMachine.from_key_sheet(
rotors = ['I', 'II', 'III'],
reflector = 'B',
ring_settings = 'A A A',
plugboard_settings = '')
for init in itertools.permutations('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 3):
machine.set_display(init) # position initiale des rotors
txt = machine.process_text(k1)
score = logscore(txt)
if score < best_score:
best_score = score
print(best_score, init)
print(txt)
delta = int(time.time()-start)
print('----- terminé en ',delta//60, 'min', delta%60, 'sec')
C'est du gâteau !
------------------------------------------------------------------------
Echauffement 2 : Le message ci-dessous a été chiffré dans les mêmes
conditions le précédent, à la différence que les rotors ne sont pas
dans le même ordre... à vous de le trouver.
------------------------------------------------------------------------
/i B-?-?-? 01-01-01
/e ???
------------------------------------------------------------------------
SQAJI BVZLB PYNOC CMNJX KMASC FKOPR BJTJB WXKHP JXJFK NVSQX RUDQW DDOAB
JIXFJ NBBTY WQQUF IDRGL WUBZX XWQJR DBAVB AWHVD JZTZW QBWQY TWNQA RZJVD
XECVE GVGFQ JVZRQ NOCTZ ERJDY AHVJD OKLBC WEODN UYBXO NZPSK DQKCZ IV
------------------------------------------------------------------------
k2 = 'SQAJI BVZLB PYNOC CMNJX KMASC FKOPR BJTJB WXKHP JXJFK NVSQX RUDQW DDOAB\
JIXFJ NBBTY WQQUF IDRGL WUBZX XWQJR DBAVB AWHVD JZTZW QBWQY TWNQA RZJVD\
XECVE GVGFQ JVZRQ NOCTZ ERJDY AHVJD OKLBC WEODN UYBXO NZPSK DQKCZ IV'.replace(' ', '')
Work factor : $3!\times 26^3 = 105456$.
start = time.time()
best_score = float('inf')
for r in itertools.permutations(['I', 'II', 'III'], 3):
print('Rotors = ', r)
machine = EnigmaMachine.from_key_sheet(
rotors = r,
reflector = 'B',
ring_settings = 'A A A',
plugboard_settings = '')
for init in itertools.permutations('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 3):
machine.set_display(init) # position initiale des rotors
txt = machine.process_text(k2)
score = logscore(txt)
if score < best_score:
best_score = score
print(best_score, init)
print(txt)
delta = int(time.time()-start)
print('----- terminé en ',delta//60, 'min', delta%60, 'sec')
Tester tous les cas possibles dans le cas général prend beaucoup trop de temps. Il faut raffiner.
Voir l'article original de Jim Gillogly Ciphertext-Only Cryptanalysis Of Enigma
Pour des méthodes plus récentes, lire l'article de O. Ostwald & F. Weierud Modern Breaking of Enigma Ciphertexts
Le 5/12/2018 - Contact : Rossignol@bribes.org