"""
.. _CrypTool: https://www.cryptool.org/en/documentation/ctbook/
.. _Decrypted Secrets: https://link.springer.com/book/10.1007/978-3-540-48121-8
The cryptoanalysis methods implemented here include:
.. csv-table::
:header: "Author", "Requirement", "See ..."
:widths: 15, 30, 30
*Rejewski*, encoded Spruchschluessel, `Decrypted Secrets`_
*Turing*, unencoded crib, `Decrypted Secrets`_
*Gillogly*, language statistics, `CrypTool`_
None of these methods always succeed. As a rule of thumb, the
Turing and Gillogly attacks require at least 30 characters
to get sufficient analysis results.
"""
from __future__ import annotations
from typing import Optional, Callable, Dict, List, Tuple
import copy
import random
import itertools
import pickle
import os.path
import networkx
import MzEnigma
[docs]class TagesschluesselRange(object):
"""Represents a range of Tagesschluessel and analysis methods working on them
:param enigma: model of the Enigma (required)
:param umkehrwalzenList: 'Umkehrwalze' (if undefined, randomly selected)
:param walzenList: List of type 'Walze' (if undefined, randomly selecte(if undefined, randomly selected)
:param tagesWalzenStellungenList: List of Tageswalzenstellungen (if undefined, all)
:param spruchScoring: 'SpruchScoring' (required by Gillolgy attack)
:param zusatzwalzenList: 'Zusatzwalze' (if undefined, randomly selected)
:param blank: replacement character for a blank
:param notify: notification function (e.g. print)
"""
def __init__(self,
enigma : Optional[MzEnigma.Enigma] = None,
umkehrwalzenList : Optional[List[MzEnigma.Umkehrwalze]] = None,
walzenList : Optional[List[MzEnigma.Walze]] = None,
tagesWalzenStellungenList : Optional[List[str]] = None,
zusatzwalzenList : Optional[List[MzEnigma.Zusatzwalze]] = None,
spruchScoring : Optional[MzEnigma.SpruchScoring] = None,
blank : str = '',
notify : Optional[Callable[[str], None]] = None) -> None:
self.notify = notify
assert enigma, '{}: Enigma required'.format(self.__class__.__name__)
self.enigma = enigma
self.blank = blank
alphabet = self.enigma.alphabet
if enigma.steckerbrett:
self.steckerbrett = copy.deepcopy(enigma.steckerbrett)
self.steckerbrett.notify = self.notify
else:
self.steckerbrett = None
assert walzenList is not None and len(walzenList) > 0, '{}: WalzenRange is required'.format(self.__class__.__name__)
if walzenList is None or len(walzenList) == 0:
walzenList = itertools.permutations(self.enigma.walzenList, self.enigma.numberOfWalzen)
self.walzenList = list()
for _walzen in walzenList:
walzen = list()
for _walze in _walzen:
walze = copy.deepcopy(_walze)
walze.notify = self.notify
walzen.append(_walze)
self.walzenList.append(walzen)
if tagesWalzenStellungenList:
for tagesWalzenStellungen in tagesWalzenStellungenList:
assert len(tagesWalzenStellungen) == enigma.numberOfWalzen, '{}: Walzenstellung, number of characters must match the number of walzen'.format(self.__class__.__name__)
assert all(v in alphabet for v in tagesWalzenStellungen), '{}: Walzenstellung, all characters must be in alphabet'.format(self.__class__.__name__)
else:
tagesWalzenStellungenList = list(itertools.product(self.enigma.alphabet, repeat = self.enigma.numberOfWalzen))
self.tagesWalzenStellungenList = copy.deepcopy(tagesWalzenStellungenList )
self.zusatzwalzenList = list()
if enigma.zusatzwalzen is not None:
assert zusatzwalzenList is not None, '{}: zusatzwalzenList is required'.format(self.__class__.__name__)
for _walze in zusatzwalzenList:
walze = copy.deepcopy(_walze)
walze.notify = self.notify
self.zusatzwalzenList.append(walze)
assert umkehrwalzenList is not None or len(self.enigma.umkehrwalzen) == 1, '{}: umkehrwalzenList is required'.format(self.__class__.__name__)
if umkehrwalzenList is None:
umkehrwalzenList = [ self.enigma.umkehrwalzen ]
self.umkehrwalzenList = list()
for _walze in umkehrwalzenList:
walze = copy.deepcopy(_walze)
walze.notify = self.notify
self.umkehrwalzenList.append(walze)
self.spruchScoring = spruchScoring
[docs] @classmethod
def rejewskiAttack(cls, catalog : List[Tuple[MzEnigma.Tagesschluessel, List[List[str]]]], encodedSpruchSchluessel : str) -> List[Tuple[MzEnigma.Tagesschluessel, str]]:
"""Find candidate values for the unencoded SpruchSchluessel from a catalog
:param catalog: catalog of encoded spruchschluessels (required)
:param encodedSpruchSchluessel: encoded spruchschluessels to be found (required)
:returns: List[Tuple[Tagesschluessel, unencodedSpruchSchluessel]]
"""
cls.checkRejewskiCatalog(catalog)
firstKey, firstItem = catalog[0]
alphabet = firstKey.alphabet
encodedPosKeys = list()
for first, second in zip(encodedSpruchSchluessel[:len(firstItem)], encodedSpruchSchluessel[len(firstItem):]):
encodedPosKeys.append(first+second)
validCandidates = list()
for tagesschluessel, listOfDoublets in catalog:
unencodedPosKeys = list()
for pos, doublets in enumerate(listOfDoublets):
if encodedPosKeys[pos] not in doublets:
break
unencodedPosKeys.append(alphabet[doublets.index(encodedPosKeys[pos])])
if len(unencodedPosKeys) == len(listOfDoublets):
validCandidates.append((tagesschluessel,''.join(unencodedPosKeys)))
return validCandidates
[docs] def createRejewskiCatalog(self, pickleFile : Optional[str] = None) -> List[Tuple[MzEnigma.Tagesschluessel, List[List[str]]]]:
"""Build up a catalog of encoded spruchschluessels. Works only without a steckerbrett.
:param pickleFile: pickle file to be created (optional)
:returns: List[Tuple[Tagesschluessel, List[List[Doublet]]]
"""
assert self.enigma.steckerbrett is None, '{}.createRejewskiCatalog: engines with steckerbrett are not supported'.format(self.__class__.__name__)
if pickleFile is not None:
pickleFile = os.path.normpath(pickleFile)
assert not os.path.exists(pickleFile), '{}.createRejewskiCatalog: pickle file {} is already exising'.format(self.__class__.__name__, pickleFile)
gTagesschluessel = MzEnigma.Tagesschluessel(enigma = self.enigma, notify = None)
rejewskiList = list()
for umkehrwalze in self.umkehrwalzenList:
if self.notify:
self.notify('{}.createRejewskiCatalog: - examining umkehrwalze = {}'.format(self.__class__.__name__, umkehrwalze.name))
for walzen in self.walzenList:
if self.notify:
nameList = list()
for walze in walzen:
nameList.append(walze.name)
self.notify('{}.createRejewskiCatalog: - examining walzen = {}'.format(self.__class__.__name__, nameList))
for tagesWalzenStellungenID, tagesWalzenStellungenTuple in enumerate(self.tagesWalzenStellungenList):
tagesWalzenStellungen = ''.join(tagesWalzenStellungenTuple)
if self.notify:
self.notify('{}.createRejewskiCatalog: + examining tagesWalzenStellungen = {} ({} of {})'.format(self.__class__.__name__, tagesWalzenStellungen, tagesWalzenStellungenID, len(self.tagesWalzenStellungenList)))
if len(self.zusatzwalzenList) > 0:
for zusatzwalze in self.zusatzwalzenList:
if self.notify:
self.notify('{}.createRejewskiCatalog: - examining zusatzwalze = {}'.format(self.__class__.__name__, zusatzwalze.name))
tagesschluessel = MzEnigma.Tagesschluessel.changeWalzen(
gTagesschluessel, walzen = walzen, tagesWalzenStellungen = tagesWalzenStellungen, umkehrwalze = umkehrwalze, zusatzwalze = zusatzwalze)
rejewskiList.append((tagesschluessel, tagesschluessel.findDoublets(first = 0, second = self.enigma.numberOfWalzen, all = True)))
else:
tagesschluessel = MzEnigma.Tagesschluessel.changeWalzen(
gTagesschluessel, walzen = walzen, tagesWalzenStellungen = tagesWalzenStellungen, umkehrwalze = umkehrwalze)
rejewskiList.append((tagesschluessel, tagesschluessel.findDoublets(first = 0, second = self.enigma.numberOfWalzen, all = True)))
if pickleFile is not None:
pickle.dump( rejewskiList, open( pickleFile, "wb" ) )
return rejewskiList
[docs] @classmethod
def checkRejewskiCatalog(cls, catalog : List[Tuple[MzEnigma.Tagesschluessel, List[List[str]]]], expectedSpruchschluesselLength : Optional[int] = None) -> None:
"""check a catalog of encoded spruchschluessels
:param expectedSpruchschluesselLength: number of characters in unencoded spruchschluessel (optional)
"""
assert len(catalog) > 0, '{}.checkRejewskiCatalog: empty catalog detected'.format(cls.__class__.__name__)
firstKey, firstItem = catalog[0]
assert isinstance(firstKey, MzEnigma.Tagesschluessel), '{}.checkRejewskiCatalog: improper catalog key, MzEnigma.Tagesschluessel espected'.format(cls.__class__.__name__)
assert isinstance(firstItem, list), '{}.checkRejewskiCatalogk: improper catalog item, list espected'.format(cls.__class__.__name__)
alphabet = firstKey.alphabet
if expectedSpruchschluesselLength is not None:
assert len(firstItem) == expectedSpruchschluesselLength, '{}.checkRejewskiCatalog: number of positions in catalog item ({}) != number of positions in spruchschluessel ({})'.format(cls.__class__.__name__, len(firstItem[0]), expectedSpruchschluesselLength)
assert len(firstItem[0]) == len(alphabet), '{}.checkRejewskiCatalog: number of positions in catalog item.item({}) != number of characters ({})'.format(cls.__class__.__name__, len(firstItem), len(alphabet))
assert isinstance(firstItem[0][0], str), '{}.checkRejewskiCatalog: improper catalog item.item, str espected'.format(cls.__class__.__name__)
[docs] @classmethod
def loadRejewskiCatalog(cls, pickleFile : str) -> List[Tuple[MzEnigma.Tagesschluessel, List[List[str]]]]:
"""Build up a catalog of encoded spruchschluessels
:returns: List[Tuple[Tagesschluessel, List[List[Doublet]]]
"""
pickleFile = os.path.normpath(pickleFile)
assert os.path.isfile(pickleFile), '{}.loadRejewskiCatalog: pickle file {} is not or not a file'.format(cls.__class__.__name__, pickleFile)
rejewskiList = pickle.load( open( pickleFile, "rb" ) )
cls.checkRejewskiCatalog(rejewskiList)
return rejewskiList
[docs] def turingAttack(self, encodedSpruch : str = '', crib : str = '', startingPosition : int = 0) -> List[Tuple[MzEnigma.Tagesschluessel, List[Tuple[str, str]]]]:
"""Turing attack to derive candidate settings for Umkehrwalze, Walzen, Zusatzwalze, Tageswalzenstellungen and several settings of the Steckerbrett
An engine with Steckerbrett is required.
The rate of correct Tagesschluessels increases with increasing length of the crib.
:param encodedSpruch: encoded message to be attacked (required)
:param crib: unencoded crib to be found in the message (required)
:param startingPosition: starting position of the crib in the message
:return: List[Tuple[Tagesschluessel, List[Tuple[Walzenstellungen, Steckerbrett.wiring]]]]
"""
def connectBus(id : int, v : str) -> bool:
'''Heart of the turing bomb - the bus is restricted to the ONE active line with a character
:param id: bus to be used
:param v: input value
:return: boolean indicating success
'''
nonlocal graph, ecList, buses
if buses[id] is None:
buses[id] = v
vID = self.enigma.alphabet.index(v)
for tgtID in graph.neighbors(id):
for _, posDict in graph[id][tgtID].items():
pos = posDict['pos']
tgtV = ecList[vID][pos]
if not connectBus(tgtID, tgtV):
return False
return True
else:
return buses[id] == v
def getValidWalzenStellungen(tagesschluessel : MzEnigma.Tagesschluessel) -> List[Tuple[str, List[Dict[str, str]]]]:
nonlocal ecList, buses, firstBus, spruchLen, graph
if self.notify:
self.notify('{}.turingAttack: \n{}'.format(self.__class__.__name__, tagesschluessel))
oldTagesWalzenStellungen = tagesschluessel.tagesWalzenStellungen
candidateSteckerbrettList = list()
for tagesWalzenStellungenID, tagesWalzenStellungenTuple in enumerate(self.tagesWalzenStellungenList):
knownWirings = list()
tagesschluessel.tagesWalzenStellungen = ''.join(tagesWalzenStellungenTuple)
if self.notify:
self.notify('{}.turingAttack: - examining tagesWalzenStellungen = {} ({} of {})'.format(self.__class__.__name__, tagesschluessel.tagesWalzenStellungen, tagesWalzenStellungenID, len(self.tagesWalzenStellungenList)))
ecList = list()
for nc, c in enumerate(self.enigma.alphabet):
ecList.append(list())
for cs in tagesschluessel.encode(spruchLen * c):
ecList[-1].append(cs)
for v in self.enigma.alphabet:
if self.notify:
self.notify('{}.turingAttack: + examining "{}"'.format(self.__class__.__name__, v))
buses = len(self.enigma.alphabet) * [ None ]
if connectBus(firstBus, v):
knownWiring = dict()
for n, id in enumerate(buses):
if id is not None:
knownWiring[self.enigma.alphabet[n]] = id
keys = list(knownWiring.keys())
for key in keys:
value = knownWiring[key]
if value not in keys:
knownWiring[value] = key
knownWirings.append(knownWiring)
if self.notify:
self.notify('{}.turingAttack: wiring {} found'.format(self.__class__.__name__, knownWiring))
candidateSteckerbrettList.append((tagesschluessel.tagesWalzenStellungen, knownWirings))
tagesschluessel.tagesWalzenStellungen = oldTagesWalzenStellungen
return candidateSteckerbrettList
assert self.enigma.steckerbrett is not None, '{}.turingAttack: engines without steckerbrett are not supported'.format(self.__class__.__name__)
posList = MzEnigma.Enigma.validCribPositions(encodedSpruch, crib)
assert startingPosition in posList, '{}.turingAttack: {} is not a valid starting position, use in {}'.format(self.__class__.__name__, startingPosition, posList)
encodedSpruch = encodedSpruch[startingPosition:]
ecList = list()
buses = list()
# We need to use a MultiGraph, since the menu may contain duplicate edges (with different positions)
graph = networkx.MultiGraph()
for pos, cc in enumerate(crib):
icc = self.enigma.alphabet.index(cc)
iec = self.enigma.alphabet.index(encodedSpruch[pos])
graph.add_edge(icc, iec, pos = pos)
firstBus = sorted(graph.adjacency(), key = lambda item: len(item[1]), reverse = True)[0][0]
spruchLen = len(crib)
encodedSpruch = encodedSpruch[:spruchLen]
gTagesschluessel = MzEnigma.Tagesschluessel(enigma = self.enigma,
steckerbrett = copy.deepcopy(MzEnigma.UnconnectedSteckerbrett),
tagesWalzenStellungen = self.enigma.numberOfWalzen * 'A',
blank = self.blank, notify = None)
validCandidates = list()
for umkehrwalze in self.umkehrwalzenList:
if self.notify:
self.notify('{}.turingAttack: - examining umkehrwalze = {}'.format(self.__class__.__name__, umkehrwalze.name))
for walzen in self.walzenList:
if self.notify:
nameList = list()
for walze in walzen:
nameList.append(walze.name)
self.notify('{}.turingAttack: - examining walzen = {}'.format(self.__class__.__name__, nameList))
if len(self.zusatzwalzenList) > 0:
for zusatzwalze in self.zusatzwalzenList:
if self.notify:
self.notify('{}.turingAttack: - examining zusatzwalze = {}'.format(self.__class__.__name__, zusatzwalze.name))
tagesschluessel = MzEnigma.Tagesschluessel.changeWalzen(
gTagesschluessel, walzen = walzen, tagesWalzenStellungen = gTagesschluessel.tagesWalzenStellungen,
umkehrwalze = umkehrwalze, zusatzwalze = zusatzwalze)
candidateSteckerbrettList = getValidWalzenStellungen(tagesschluessel)
if len(candidateSteckerbrettList) > 0:
validCandidates.append((copy.deepcopy(tagesschluessel), candidateSteckerbrettList))
else:
tagesschluessel = MzEnigma.Tagesschluessel.changeWalzen(
gTagesschluessel, walzen = walzen, tagesWalzenStellungen = gTagesschluessel.tagesWalzenStellungen, umkehrwalze = umkehrwalze)
candidateSteckerbrettList = getValidWalzenStellungen(tagesschluessel)
if len(candidateSteckerbrettList) > 0:
validCandidates.append((copy.deepcopy(tagesschluessel), candidateSteckerbrettList))
return validCandidates
[docs] def gilloglyAttackPhase1(self, encodedSpruch : str) -> MzEnigma.Tagesschluessel:
"""Brute force attack to derive settings for Umkehrwalze, Walzen, Zusatzwalze, and Tageswalzenstellungen.
Uses the index of coincidence for scoring.
The rate of correct Tagesschluessels increases with increasing length of the encodedSpruch.
:param encodedSpruch: message to be attacked (required)
:return: Tagesschlüssel
"""
def doScoring():
nonlocal encodedSpruch, bestScore, tagesschluessel, bestTagesschluessel, lastDecryptedSpruch
decryptedSpruch = tagesschluessel.decode(encodedSpruch)
actScore = MzEnigma.SpruchScoring.indexOfCoincidence(decryptedSpruch)
# decryptedSpruchMD5 = hashlib.md5(str(decryptedSpruch).encode("utf-8")).hexdigest()
if self.notify:
self.notify('{}.gilloglyAttackPhase1: + examining tagesWalzenStellungen = {}'.format(self.__class__.__name__, tagesschluessel.tagesWalzenStellungen))
self.notify('{} score = {:.3f} (best: {:.3f})'.format(60*' ', actScore, bestScore))
lastDecryptedSpruch = decryptedSpruch
if actScore > bestScore:
bestScore = actScore
bestTagesschluessel = copy.deepcopy(tagesschluessel)
lastDecryptedSpruch = ''
gTagesschluessel = MzEnigma.Tagesschluessel(enigma = self.enigma, steckerbrett = self.steckerbrett, blank = self.blank, notify = None)
bestTagesschluessel = None
bestScore = 0
for umkehrwalze in self.umkehrwalzenList:
if self.notify:
self.notify('{}.gilloglyAttackPhase1: - examining umkehrwalze = {}'.format(self.__class__.__name__, umkehrwalze.name))
for walzen in self.walzenList:
if self.notify:
nameList = list()
for walze in walzen:
nameList.append(walze.name)
self.notify('{}.gilloglyAttackPhase1: - examining walzen = {}'.format(self.__class__.__name__, nameList))
for tagesWalzenStellungenTuple in self.tagesWalzenStellungenList:
tagesWalzenStellungen = ''.join(tagesWalzenStellungenTuple)
if len(self.zusatzwalzenList) > 0:
for zusatzwalze in self.zusatzwalzenList:
if self.notify:
self.notify('{}.gilloglyAttackPhase1: - examining zusatzwalze = {}'.format(self.__class__.__name__, zusatzwalze.name))
tagesschluessel = MzEnigma.Tagesschluessel.changeWalzen(
gTagesschluessel, walzen = walzen, tagesWalzenStellungen = tagesWalzenStellungen, umkehrwalze = umkehrwalze, zusatzwalze = zusatzwalze)
doScoring()
else:
tagesschluessel = MzEnigma.Tagesschluessel.changeWalzen(
gTagesschluessel, walzen = walzen, tagesWalzenStellungen = tagesWalzenStellungen, umkehrwalze = umkehrwalze)
doScoring()
if self.notify:
self.notify('{}.gilloglyAttackPhase1: bestScore = {:.3f}\n{}'.format(self.__class__.__name__, bestScore, bestTagesschluessel))
return bestTagesschluessel
[docs] def shotgunPhase2(self,
phase1Tagesschluessel : MzEnigma.Tagesschluessel, encodedSpruch : str = '', noImprovement : int = 10) -> MzEnigma.Tagesschluessel:
"""Shotgun hill climbing or simulated annealing attack to derive settings for Steckerbrett
Uses the 'newNgramScore' for scoring.
:param phase1Tagesschluessel: tagesschluessel with correct settings for Umkehrwalze, Walzen, Zusatzwalze, and Tageswalzenstellungen(required)
:param encodedSpruch: encrypted message to be attacked (required)
:param noImprovement: stop condition after *noImprovement* number of attempts in a cycle
:return: Tagesschlüssel
"""
def fillWired(tagesschluessel):
wired = set()
unwired = set()
for n, c in enumerate(tagesschluessel.alphabet):
if tagesschluessel.steckerbrett.wiring[n] != c:
wired.add(c)
else:
unwired.add(c)
return wired, unwired
assert self.spruchScoring is not None, '{}.shotgunPhase2: self.spruchScoring is required'.format(self.__class__.__name__)
assert phase1Tagesschluessel.steckerbrett is not None, '{}.gilloglyAttackPhase2: steckerbrett is required'.format(self.__class__.__name__)
wired, unwired = fillWired(phase1Tagesschluessel)
if len(wired) <= 2:
return phase1Tagesschluessel
assert len(unwired) > 2, '{}.shotgunPhase2: steckerbrett with > 0 unconnected pins required'.format(self.__class__.__name__)
tagesschluessel = copy.deepcopy(phase1Tagesschluessel)
notImproved = 0
bestScore = 0
while notImproved < noImprovement:
actScore = self.spruchScoring.newNgramScore(tagesschluessel.decode(encodedSpruch), bestScore, tagesschluessel.alphabet)
if actScore >= bestScore:
if actScore > bestScore:
notImproved = -1
bestScore = actScore
bestWired = copy.deepcopy(wired)
bestUnwired = copy.deepcopy(unwired)
bestWiring = tagesschluessel.steckerbrett.wiring
if self.notify:
self.notify('{}.shotgunPhase2: bestScore = {}\n{}\n'.format(
self.__class__.__name__, bestScore, tagesschluessel))
else:
tagesschluessel.steckerbrett.wiring = bestWiring
wired = copy.deepcopy(bestWired)
unwired = copy.deepcopy(bestUnwired)
notImproved += 1
unconnectSrc = random.choice(list(wired))
connectSrc = random.choice(list(unwired))
unwired.remove(connectSrc)
unwired.add(unconnectSrc)
connectTgt = random.choice(list(unwired))
tagesschluessel.steckerbrett.clearMark3Setting(unconnectSrc)
tagesschluessel.steckerbrett.addMark3Setting(connectSrc, connectTgt)
wired, unwired = fillWired(tagesschluessel)
return tagesschluessel
[docs] def exchangePhase2(self,
phase1Tagesschluessel : MzEnigma.Tagesschluessel, encodedSpruch : str = '', cycles : int = 5) -> MzEnigma.Tagesschluessel:
"""Hill climbing based on exchange of x-connected ports of a steckerbrett
:param phase1Tagesschluessel: tagesschluessel with correct settings for Umkehrwalze, Walzen, Zusatzwalze, and Tageswalzenstellungen(required)
:param encodedSpruch: encrypted message to be attacked (required)
:param cycles: number of exchange cycles
:return: Tagesschlüssel
"""
assert self.spruchScoring is not None, '{}.exchangePhase2: self.spruchScoring is required'.format(self.__class__.__name__)
assert phase1Tagesschluessel.steckerbrett is not None, '{}.gilloglyAttackPhase2: steckerbrett is required'.format(self.__class__.__name__)
tagesschluessel = copy.deepcopy(phase1Tagesschluessel)
wired = set()
for n, c in enumerate(tagesschluessel.alphabet):
if tagesschluessel.steckerbrett.wiring[n] != c:
wired.add(c)
if len(wired) <= 2:
return phase1Tagesschluessel
bestWiring = tagesschluessel.steckerbrett.wiring
bestScore = self.spruchScoring.ngramScore(tagesschluessel.decode(encodedSpruch), validChars = tagesschluessel.alphabet)
for cycle in range(cycles):
frequencyDict = tagesschluessel.frequencyDict(encodedSpruch, decode = True)
for freq in sorted(frequencyDict.keys(), reverse = True):
for c1, _ in frequencyDict[freq]:
if c1 in wired:
c1Index = tagesschluessel.steckerbrett.wiring.index(c1)
if c1 != tagesschluessel.alphabet[c1Index]:
for c2 in wired:
c2Index = tagesschluessel.steckerbrett.wiring.index(c2)
if c2 != c1 and c2 != tagesschluessel.alphabet[c2Index]:
actWiring = tagesschluessel.steckerbrett.wiring
tagesschluessel.steckerbrett.exchangeMark3Setting(c1, c2)
actScore = self.spruchScoring.ngramScore(tagesschluessel.decode(encodedSpruch), validChars = tagesschluessel.alphabet)
if actScore > bestScore:
bestScore = actScore
bestWiring = tagesschluessel.steckerbrett.wiring
if self.notify:
self.notify('{}.exchangePhase2: cycle {}, exchanging {} <-> {}, score = {:.3f}'.format(self.__class__.__name__, cycle + 1, c1, c2, bestScore))
tagesschluessel.steckerbrett.wiring = actWiring
tagesschluessel.steckerbrett.wiring = bestWiring
tagesschluessel.steckerbrett.wiring = bestWiring
return tagesschluessel
[docs] def gilloglyAttackPhase2(self,
phase1Tagesschluessel : MzEnigma.Tagesschluessel, encodedSpruch : str = '',
noImprovement : int = 10, cycles : int = 1000, useSimulatedAnnealing : bool = False) -> MzEnigma.Tagesschluessel:
"""Shotgun hill climbing or simulated annealing attack to derive settings for Steckerbrett
:param phase1Tagesschluessel: tagesschluessel with correct settings for Umkehrwalze, Walzen, Zusatzwalze, and Tageswalzenstellungen(required)
:param encodedSpruch: encrypted message to be attacked (required)
:param noImprovement: stop condition after *noImprovement* number of attempts in a cycle
:param cycles: number of cycles with a random steckerbrett configuration
:param useSimulatedAnnealing: use simulated annealing instead of conventional hill climbing
:return: Tagesschlüssel
"""
if useSimulatedAnnealing:
self.spruchScoring.setSATemperature(encodedSpruch)
else:
self.spruchScoring.setSATemperature('')
bestScore = 0
tagesschluessel = copy.deepcopy(phase1Tagesschluessel)
initialScore = self.spruchScoring.ngramScore(tagesschluessel.decode(encodedSpruch), 1, validChars = tagesschluessel.alphabet)
bestWiring = tagesschluessel.steckerbrett.wiring
for cycle in range(cycles):
tagesschluessel = self.shotgunPhase2(self, tagesschluessel, encodedSpruch, noImprovement)
actScore = self.spruchScoring.newNgramScore(tagesschluessel.decode(encodedSpruch), bestScore, tagesschluessel.alphabet)
if self.notify:
self.notify('{}.gilloglyAttackPhase2/cycle {}: score = {:.3f} (best: {:.3f})'.format(self.__class__.__name__, cycle, actScore, bestScore))
if actScore > bestScore:
bestScore = actScore
bestWiring = tagesschluessel.steckerbrett.wiring
newSteckerbrett = MzEnigma.Steckerbrett.Mark_3(alphabet = tagesschluessel.alphabet)
tagesschluessel.steckerbrett.wiring = newSteckerbrett.wiring
tagesschluessel.steckerbrett.wiring = bestWiring
if self.notify:
finalScore = self.spruchScoring.ngramScore(tagesschluessel.decode(encodedSpruch), 1, validChars = tagesschluessel.alphabet)
self.notify('{}.gilloglyAttackPhase2: score {:.3f} -> {:.3f}'.format(self.__class__.__name__, initialScore, finalScore))
return tagesschluessel
[docs] def mzAttackPhase2(self, phase1Tagesschluessel : MzEnigma.Tagesschluessel, encodedSpruch : str = '') -> MzEnigma.Tagesschluessel:
"""Systematic hill climbing attack to derive settings for Steckerbrett
Uses the 'ngramScore' for scoring.
:param phase1Tagesschluessel: tagesschluessel with correct settings for Umkehrwalze, Walzen, Zusatzwalze, and Tageswalzenstellungen(required)
:param encodedSpruch: encrypted message to be attacked (required)
:return: Tagesschlüssel
"""
assert self.spruchScoring is not None, '{}.mzAttackPhase2: self.spruchScoring is required'.format(self.__class__.__name__)
assert phase1Tagesschluessel.steckerbrett is not None, '{}.mzAttackPhase2: steckerbrett is required'.format(self.__class__.__name__)
tagesschluessel = copy.deepcopy(phase1Tagesschluessel)
nConnections = 0
for n, c in enumerate(tagesschluessel.alphabet):
if tagesschluessel.steckerbrett.wiring[n] != c:
nConnections += 1
nConnections = nConnections // 2
assert nConnections > 0, '{}.mzAttackPhase2: at least one connection in steckerbrett required'.format(self.__class__.__name__)
tagesschluessel.steckerbrett.wiring = tagesschluessel.alphabet
monograms = sorted(self.spruchScoring.ngramDict[1].items(), key = lambda c2s: c2s[1], reverse = False)
assert len(monograms) > 2*nConnections, '{}.mzAttackPhase2: len(monograms) = {} > 2*nConnections = {}'.format(self.__class__.__name__, len(monograms), 2*nConnections)
initialScore = self.spruchScoring.ngramScore(tagesschluessel.decode(encodedSpruch), 1, validChars = tagesschluessel.alphabet)
unwired = list(tagesschluessel.alphabet)
self.spruchScoring.numberOfChars = 1
connectedChars = ''
connection = 0
while connection < nConnections and len(unwired) > 1:
connectTgt = monograms.pop()[0]
connectedChars += connectTgt
if connectTgt not in unwired:
continue
maxScore = 0
maxN = -1
wiring = tagesschluessel.steckerbrett.wiring
for n, connectSrc in enumerate(unwired):
tagesschluessel.steckerbrett.wiring = wiring
if connectSrc != connectTgt:
tagesschluessel.steckerbrett.addMark3Setting(connectSrc, connectTgt)
decodedSpruch = tagesschluessel.decode(encodedSpruch)
if True:
actScore = self.spruchScoring.ngramScore(decodedSpruch, 1, validChars = connectedChars)
else:
actScore = MzEnigma.SpruchScoring.indexOfCoincidence(decodedSpruch)
if actScore > maxScore:
maxScore = actScore
maxN = n
tagesschluessel.steckerbrett.wiring = wiring
connectSrc = unwired[maxN]
unwired.pop(unwired.index(connectSrc))
if self.notify:
self.notify('{}.mzAttackPhase2: connecting {} -> {}, maxScore = {:.3f}'.format(self.__class__.__name__, connectSrc, connectTgt, maxScore))
if connectSrc != connectTgt:
tagesschluessel.steckerbrett.addMark3Setting(connectSrc, connectTgt)
unwired.pop(unwired.index(connectTgt))
connection += 1
if self.notify:
finalScore = self.spruchScoring.ngramScore(tagesschluessel.decode(encodedSpruch), 1, validChars = tagesschluessel.alphabet)
self.notify('{}.mzAttackPhase2: score {:.3f} -> {:.3f}'.format(self.__class__.__name__, initialScore, finalScore))
return tagesschluessel