Enigma Machine Emulator in Python

For now, I am not going to translate this article into English because there are many web pages that explain the details about the Enigma machines in English.

The source code below might be uploaded to GitHub.

はじめに

第二次世界大戦中, ナチスドイツ軍が使用していた Enigma 暗号機のエミュレータPython で書いた (書き直した). 以前のやつは継承とか使っていたが, バラバラにした. Bombeを書こうと思い始めたので, リファクタリングになっているかわからないがリファクタした.

記事の最後にソースコードを置いてある.

オリジナルのリフレクタとロータの追加機能はまだテストしていない. オリジナルのリフレクタとロータを動かせるようになった(たぶん).

Enigma に関する英語記事はたくさんあるが, 日本語ではほとんどない. ここで軽い解説を書こうと思う.

後で.

動作

この記事の最後にあるコードを実行すると次の結果が得られる.

test.py

動作確認.

$ python ./test.py
Enigma machine emulator version 1.4.8

Registered reflectors:
{
    "A": "EJMZALYXVBWFCRQUONTSPIKHGD",
    "B": "YRUHQSLDPXNGOKMIEBFZCWVJAT",
    "C": "FVPJIAOYEDRZXWGCTKUQSBNMHL",
    "B Thin": "ENKQAUYWJICOPBLMDXZVFTHRGS",
    "C Thin": "RDOBJNTKVEHMLFCWZAXGYIPSUQ"
}
Registered rotors:
{
    "I": "EKMFLGDQVZNTOWYHXUSPAIBRCJ",
    "II": "AJDKSIRUXBLHWTMCQGZNPYFVOE",
    "III": "BDFHJLCPRTXVZNYEIWGAKMUSQO",
    "IV": "ESOVPZJAYQUIRHXLNFTGKDCMWB",
    "V": "VZBRGITYUPSDNHLXAWMJQOFECK",
    "VI": "JPGVOUMFYQBENHZRDKASXLICTW",
    "VII": "NZJHGRCXMYSWBOUFAIVLPEKQDT",
    "VIII": "FKQHTLXOCBJSPDZRAMEWNIUYGV",
    "Beta": "LEYJVCNIXWPBQMDRTAKZGFUHOS",
    "Gamma": "FSOKANUERHMBTIYCWLQPZXVGJD"
}
Registered plugboards:
{
    "Plugs": "KHYDVGFBQRALPNSMIJOTZEWXCU"
}

wirings:
{
    "reflector": {
        "name": "C Thin",
        "wiring": "RDOBJNTKVEHMLFCWZAXGYIPSUQ",
        "pairs": {
            "A": "R",
            "B": "D",
            "C": "O",
            "E": "J",
            "F": "N",
            "G": "T",
            "H": "K",
            "I": "V",
            "L": "M",
            "P": "W",
            "Q": "Z",
            "S": "X",
            "U": "Y"
        }
    },
    "rotors": [
        {
            "name": "Gamma",
            "wiring": "FSOKANUERHMBTIYCWLQPZXVGJD",
            "turnover_position": [],
            "initial_position": "T",
            "ring_position": "M"
        },
        {
            "name": "III",
            "wiring": "BDFHJLCPRTXVZNYEIWGAKMUSQO",
            "turnover_position": [
                "V"
            ],
            "initial_position": "P",
            "ring_position": "S"
        },
        {
            "name": "VIII",
            "wiring": "FKQHTLXOCBJSPDZRAMEWNIUYGV",
            "turnover_position": [
                "Z",
                "M"
            ],
            "initial_position": "X",
            "ring_position": "F"
        },
        {
            "name": "II",
            "wiring": "AJDKSIRUXBLHWTMCQGZNPYFVOE",
            "turnover_position": [
                "E"
            ],
            "initial_position": "J",
            "ring_position": "Z"
        }
    ],
    "plugboard": {
        "name": "Plugs",
        "wiring": "KHYDVGFBQRALPNSMIJOTZEWXCU",
        "pairs": {
            "A": "K",
            "B": "H",
            "C": "Y",
            "E": "V",
            "F": "G",
            "I": "Q",
            "J": "R",
            "M": "P",
            "O": "S",
            "U": "Z"
        }
    },
    "keeps_initial_state": false
}

initial positions of the rotors:
[
    "T",
    "P",
    "X",
    "J"
]

plain: I really don't know why it works.
formatted: IREALLYDONTKNOWWHYITWORKSX
encoded:   NHTFFMGYHDEMIQUTGXQSMKBDHG
decoded:   IREALLYDONTKNOWWHYITWORKSX

current positions of the rotors:
[
    "T",
    "P",
    "Y",
    "J"
]

original.py

オリジナルの設定ができる. 今回は3ロータで配線はランダムにしたので実行毎に結果が変わる.

$ ./original.py 3
wirings:
{
    "reflector": {
        "name": "original reflector",
        "wiring": "PDTBJRNXOEYVWGIAUFZCQLMHKS",
        "pairs": {
            "E": "J",
            "K": "Y",
            "C": "T",
            "Q": "U",
            "M": "W",
            "S": "Z",
            "A": "P",
            "F": "R",
            "L": "V",
            "G": "N",
            "H": "X",
            "I": "O",
            "B": "D"
        }
    },
    "rotors": [
        {
            "name": "original rotor 1",
            "wiring": "IXROUDJVGEHKATFPZMBQSNWLCY",
            "turnover_position": [
                "A",
                "X"
            ],
            "initial_position": "A",
            "ring_position": "E"
        },
        {
            "name": "original rotor 2",
            "wiring": "DQPWVZOYCBTHIMJSUFAERKNGLX",
            "turnover_position": [
                "D",
                "W"
            ],
            "initial_position": "Q",
            "ring_position": "J"
        },
        {
            "name": "original rotor 3",
            "wiring": "HRCNFKAPYTQJSZUOXMGVDWBEIL",
            "turnover_position": [
                "Z",
                "A"
            ],
            "initial_position": "U",
            "ring_position": "T"
        }
    ],
    "plugboard": {
        "name": "original plugboard",
        "wiring": "DYGAJRCIHETVWSXQPFNKZLMOBU",
        "pairs": {
            "F": "R",
            "P": "Q",
            "C": "G",
            "K": "T",
            "B": "Y",
            "A": "D",
            "E": "J",
            "O": "X",
            "M": "W",
            "U": "Z",
            "H": "I",
            "L": "V",
            "N": "S"
        }
    },
    "keeps_initial_state": true
}

initial positions of the rotors:
[
    "A",
    "U",
    "K"
]

plain: Ich weiss wirklich nicht, warum sie funktioniert.
formatted: ICHWEISSWIRKLICHNICHTWARUMSIEFUNKTIONIERTX
encoded:   QNAIBHJYPTWZNUEPUYYIOKFUKTBYAUBIZMCNJMDDKV
decoded:   ICHWEISSWIRKLICHNICHTWARUMSIEFUNKTIONIERTX

Source code

enigma/__init__.py

# -*- coding: utf-8 -*-

"""
Enigma machine emulator

Copyright (C) 2017, 2018 Vanaestea

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

from . import component
from . import emulator

PROG_NAME = 'Enigma machine emulator'
VERSION = '1.4.8'
AUTHOR = 'Vanaestea'
LICENSE = 'MIT'

enigma/component.py

# -*- coding: utf-8 -*-

"""
Enigma machine emulator

Copyright (C) 2017, 2018 Vanaestea

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

from abc import ABC, abstractmethod
from collections import Counter
from operator import itemgetter
import string

_MOD_BASE = len(string.ascii_uppercase)

class BaseComponent(ABC):
    _shifts_in = {}
    _shifts_out = {}
    _wirings = {}

    def __init__(self):
        self._name = None
        self._wiring = None
        self._shift_in = None
        self._shift_out = None

    @abstractmethod
    def _set_shifts(self):
        pass

    @abstractmethod
    def set(self):
        pass

    @abstractmethod
    def add(self):
        pass

    @abstractmethod
    def encode(self):
        pass

    @abstractmethod
    def decode(self):
        pass

    @property
    def wiring(self):
        return self._wiring

    @property
    def name(self):
        return self._name

    @classmethod
    def get_list(cls):
        return cls._wirings

class Rotor(BaseComponent):
    _wirings = {
        'I':     'EKMFLGDQVZNTOWYHXUSPAIBRCJ',
        'II':    'AJDKSIRUXBLHWTMCQGZNPYFVOE',
        'III':   'BDFHJLCPRTXVZNYEIWGAKMUSQO',
        'IV':    'ESOVPZJAYQUIRHXLNFTGKDCMWB',
        'V':     'VZBRGITYUPSDNHLXAWMJQOFECK',
        'VI':    'JPGVOUMFYQBENHZRDKASXLICTW',
        'VII':   'NZJHGRCXMYSWBOUFAIVLPEKQDT',
        'VIII':  'FKQHTLXOCBJSPDZRAMEWNIUYGV',
        'Beta':  'LEYJVCNIXWPBQMDRTAKZGFUHOS',
        'Gamma': 'FSOKANUERHMBTIYCWLQPZXVGJD',
    }
    # A turnover position is one character ahead of the real one
    _turnovers = {
        'I':     (17,),   # R
        'II':    (5,),    # E
        'III':   (22,),   # V
        'IV':    (10,),   # J
        'V':     (0,),    # Z
        'VI':    (0, 13), # Z+M
        'VII':   (0, 13), # Z+M
        'VIII':  (0, 13), # Z+M
        'Beta':  (),      # nothing
        'Gamma': (),      # nothing
    }

    def __init__(self):
        super().__init__()
        self._turnover = None
        self._initial_shift = 0
        self._shift = 0
        self._ring = 0
        self._fixed = False

    def _set_shifts(self, name):
        self._shifts_in[name] = tuple(ord(ch) - ord('A') for ch in self._wirings[self._name])
        self._shifts_out[name] = tuple(i for i, _ in sorted(enumerate(self._shifts_in[name]), key=itemgetter(1)))

    def set_with_shift(self, name, initial_shift, ring):
        if name not in self._wirings or\
           initial_shift < 0 or _MOD_BASE <= initial_shift or\
           ring < 0 or _MOD_BASE <= ring:
            return False
        self._name = name
        if name not in self._shifts_in:
            self._set_shifts(name)
        self._shift_in = self._shifts_in[name]
        self._shift_out = self._shifts_out[name]
        self._wiring = self._wirings[name]
        self._turnover = self._turnovers[name]
        self._ring = ring
        self._shift = self._initial_shift = initial_shift
        self._fixed = True if len(self._turnover) == 0 else False
        return True

    def set(self, name, initial_position, ring_position):
        return self.set_with_shift(name, ord(initial_position) - ord('A'), ord(ring_position) - ord('A'))

    def add_with_shift(self, name, wiring, turnover):
        if name not in self._wirings and\
           Counter(wiring) == Counter(string.ascii_uppercase):
            for idx in turnover:
                if idx < 0 or _MOD_BASE <= idx:
                    break
            else:
                self._wirings[name] = wiring
                self._turnovers[name] = tuple(sorted((i + 1) % _MOD_BASE for i in turnover))
                return True
        return False

    def add(self, name, wiring, turnover_position):
        return self.add_with_shift(name, wiring, tuple(ord(pos) - ord('A') for pos in turnover_position))

    def encode(self, idx, shift, ring=0):
        return self._shift_in[(idx + self._shift - self._ring - shift + ring) % _MOD_BASE]

    def decode(self, idx, shift, ring=0):
        return self._shift_out[(idx + self._shift - self._ring - shift + ring) % _MOD_BASE]

    def reset(self):
        self._shift = self._initial_shift

    def step(self):
        if not self._fixed:
            self._shift = (self._shift + 1) % _MOD_BASE

    def on_turnover(self, next_to_fast=False):
        for idx in self._turnover:
            if next_to_fast:
                if (self._shift + 1) % _MOD_BASE == idx:
                    break
            elif self._shift == idx:
                break
        else:
            return False
        return True

    @property
    def initial_shift(self):
        return self._initial_shift

    @initial_shift.setter
    def initial_shift(self, initial_shift):
        if 0 <= initial_shift < _MOD_BASE:
            self._initial_shift = initial_shift
        else:
            raise ValueError()

    @property
    def initial_position(self):
        return chr(self.initial_shift + ord('A'))

    @initial_position.setter
    def initial_position(self, initial_position):
        self.initial_shift = ord(initial_position) - ord('A')

    @property
    def shift(self):
        return self._shift

    @shift.setter
    def shift(self, shift):
        if 0 <= shift < _MOD_BASE:
            self._shift = shift
        else:
            raise ValueError()

    @property
    def position(self):
        return chr(self.shift + ord('A'))

    @position.setter
    def position(self, position):
        self.shift = ord(position) - ord('A')

    @property
    def ring(self):
        return self._ring

    @ring.setter
    def ring(self, ring):
        if 0 <= ring < _MOD_BASE:
            self._ring = ring
        else:
            raise ValueError()

    @property
    def ring_position(self):
        return chr(self.ring + ord('A'))

    @ring_position.setter
    def ring_position(self, ring_position):
        self.ring = ord(ring_position) - ord('A')

    # return the real turnover shift
    @property
    def turnover(self):
        return (self._turnover + _MOD_BASE - 1) % _MOD_BASE

    # return the real turnover position
    @property
    def turnover_position(self):
        return tuple(chr(ord('A') + (idx + _MOD_BASE - 1) % _MOD_BASE) for idx in self._turnover)

    @property
    def is_fixed(self):
        return self._fixed

class PairwiseComponent(BaseComponent):
    _wirings_pairs = {}

    def __init__(self):
        super().__init__()
        self._pairs = None

    def _is_pairwise(self, pairs):
        s = set()
        t = set(string.ascii_uppercase)
        for k, v in pairs.items():
            if k in s or k not in t:
                break
            s.add(k)
            if v in s or v not in t:
                break
            s.add(v)
        else:
            return True
        return False

    def _set_pairs(self, name):
        shifts = self._shifts_in[name]
        pairs = {}
        s = set()
        for i, e in enumerate(shifts):
            if i == e or i in s or e in s:
                continue
            pairs[chr(i + ord('A'))] = chr(e + ord('A'))
            s.add(i)
            s.add(e)
        self._wirings_pairs[name] = pairs

    def set(self, name):
        if name not in self._wirings:
            return False
        self._name = name
        self._wiring = self._wirings[name]
        if name not in self._shifts_in:
            self._set_shifts(name)
        self._shift_in = self._shifts_in[name]
        self._shift_out = self._shifts_out[name]
        if name not in self._wirings_pairs:
            self._set_pairs(name)
        self._pairs = self._wirings_pairs[name]
        return True

    def add(self, name, pairs):
        if name in self._wirings or not self._is_pairwise(pairs):
            return False
        self._wirings_pairs[name] = dict((k, v) if k < v else (v, k) for k, v in tuple(pairs.items()))
        a = list(string.ascii_uppercase)
        for k, v in self._wirings_pairs[name].items():
            i = ord(k) - ord('A')
            j = ord(v) - ord('A')
            a[i], a[j] = a[j], a[i]
        self._wirings[name] = ''.join(a)
        return True

    @property
    def pairs(self):
        return self._pairs

    @classmethod
    def get_pairs_list(cls):
        return cls.pairs

class Reflector(PairwiseComponent):
    _wirings = {
        'A':      'EJMZALYXVBWFCRQUONTSPIKHGD',
        'B':      'YRUHQSLDPXNGOKMIEBFZCWVJAT',
        'C':      'FVPJIAOYEDRZXWGCTKUQSBNMHL',
        'B Thin': 'ENKQAUYWJICOPBLMDXZVFTHRGS',
        'C Thin': 'RDOBJNTKVEHMLFCWZAXGYIPSUQ',
    }

    def _set_shifts(self, name):
        self._shifts_in[name] = tuple(ord(ch) - ord('A') for ch in self._wirings[self._name])
        self._shifts_out[name] = None

    def encode(self, idx, shift, ring=0):
        return self._shift_in[(idx - shift + ring) % _MOD_BASE]

    def decode(self, idx, shift, ring=0):
        return encode(idx, shift, ring)

class Plugboard(PairwiseComponent):
    def _set_shifts(self, name):
        self._shifts_in[name] = tuple(ord(ch) - ord('A') for ch in self._wirings[name])
        self._shifts_out[name] = tuple(i for i, _ in sorted(enumerate(self._shifts_in[name]), key=itemgetter(1)))

    def encode(self, idx):
        return self._shift_in[idx]

    def decode(self, idx, shift, ring=0):
        return self._shift_out[(idx - shift + ring) % _MOD_BASE]

enigma/emulator.py

# -*- coding: utf-8 -*-

"""
Enigma machine emulator

Copyright (C) 2017, 2018 Vanaestea

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

from . import component
import string

class Enigma(object):
    def __init__(self, keeps_initial_state=True):
        self._reflector = component.Reflector()
        self._rotors = []
        self._plugboard = component.Plugboard()
        self._keeps_initial_state = keeps_initial_state

    def set_reflector(self, name):
        return self._reflector.set(name)

    def add_reflector(self, name, wiring):
        return self._reflector.add(name, wiring)

    def append_rotor(self, name, initial_position='A', ring_position='A'):
        rotor = component.Rotor()
        if rotor.set(name, initial_position, ring_position):
            self._rotors.append(rotor)
            return True
        return False

    # deprecated
    def push_rotor(self, name, initial_position='A', ring_position='A'):
        self.append_rotor(name, initial_position, ring_position)

    def add_rotor(self, name, wiring, turnover=('A',)):
        rotor = component.Rotor()
        return rotor.add(name, wiring, turnover)

    def replace_rotor(self, index, name, initial_position='A', ring_position='A'):
        rotor = component.Rotor()
        if rotor.set(name, initial_position, ring_position):
            self._rotors[index] = rotor
            return True
        return False

    def insert_rotor(self, index, name, initial_position='A', ring_position='A'):
        rotor = component.Rotor()
        if rotor.set(name, initial_position, ring_position):
            self._rotors.insert(index, rotor)
            return True
        return False

    def clear_rotors(self):
        self._rotors.clear()

    def add_plugboard(self, name, pairs):
            return self._plugboard.add(name, pairs)

    def set_plugboard(self, name):
            return self._plugboard.set(name)

    def add_set_plugboard(self, name, pairs):
        if self._plugboard.add(name, pairs):
            return self._plugboard.set(name)
        return False

    def reset(self):
        for rotor in self._rotors:
            rotor.reset()

    def set_initial_positions(self, positions):
        positions = positions.upper()
        try:
            for rotor, pos in zip(self._rotors, positions):
                rotor.initial_position = pos
        except:
            return False
        self.reset()
        return True

    def is_text_valid(self, text):
        if set(text.upper()) <= set(string.ascii_uppercase):
                return True
        return False

    def encode(self, text):
        if self._keeps_initial_state:
            self.reset()

        text = text.upper()

        if not self.is_text_valid(text):
            raise ValueError('The text includes invalid characters')

        o = []
        for ch in text:
            idx = ord(ch) - ord('A')

            # Step
            i = len(self._rotors) - 1
            self._rotors[i].step()
            i -= 1
            if i > 0 and (self._rotors[i + 1].on_turnover() or\
                          self._rotors[i].on_turnover(True)):
                self._rotors[i].step()
                while i > 0:
                    if self._rotors[i].on_turnover():
                        i -= 1
                        self._rotors[i].step()
                    else:
                        break

            # Encode
            # Incoming
            e = self._plugboard.encode(idx)
            shift = 0
            ring = 0
            for rotor in reversed(self._rotors):
                e = rotor.encode(e, shift, ring)
                shift = rotor.shift
                ring = rotor.ring

            e = self._reflector.encode(e, shift, ring)

            # Outgoing
            shift = 0
            ring = 0
            for rotor in self._rotors:
                e = rotor.decode(e, shift, ring)
                shift = rotor.shift
                ring = rotor.ring
            e = self._plugboard.decode(e, shift, ring)
            o.append(chr(e + ord('A')))

        return ''.join(o)

    def decode(self, text):
        return self.encode(text)

    def get_setting(self):
        return {
            'reflector': {
                'name': self._reflector.name,
                'wiring': self._reflector.wiring,
                'pairs': self._reflector.pairs
            },
            'rotors': tuple(
                {
                    'name': rotor.name,
                    'wiring': rotor.wiring,
                    'turnover_position': tuple(e for e in rotor.turnover_position),
                    'initial_position': rotor.initial_position,
                    'ring_position': rotor.ring_position,
                }
                for rotor in self._rotors
            ),
            'plugboard': {
                'name': self._plugboard.name,
                'wiring': self._plugboard.wiring,
                'pairs': self._plugboard.pairs,
            },
            'keeps_initial_state': self._keeps_initial_state,
        }

    def get_positions(self):
        return [rotor.position for rotor in self._rotors]

test.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import enigma
from enigma import emulator
from enigma import component
import json

def main():
    print('{} version {}'.format(enigma.PROG_NAME, enigma.VERSION))
    print()

    e = emulator.Enigma(False)
    e.set_reflector('C Thin')
    e.append_rotor('Gamma', 'T', 'M')
    e.append_rotor('III', 'P', 'S')
    e.append_rotor('VIII', 'X', 'F')
    e.append_rotor('II', 'J', 'Z')
    e.add_set_plugboard('Plugs', {'A':'K', 'B':'H', 'C':'Y', 'E':'V', 'F':'G', 'I':'Q', 'J':'R', 'M':'P', 'O':'S', 'U':'Z'})

    print('Registered reflectors:')
    print(json.dumps(component.Reflector.get_list(), indent=4))
    print('Registered rotors:')
    print(json.dumps(component.Rotor.get_list(), indent=4))
    print('Registered plugboards:')
    print(json.dumps(component.Plugboard.get_list(), indent=4))
    print()

    print('wirings:')
    print(json.dumps(e.get_setting(), indent=4))
    print()

    print('initial positions of the rotors:')
    print(json.dumps(e.get_positions(), indent=4))
    print()

    plain = "I really don't know why it works."
    formatted = plain.replace(' ', '').replace("'", '').replace('.', 'X').upper()
    encoded = e.encode(formatted)
    e.reset()
    decoded = e.decode(encoded)
    print('plain: {}'.format(plain))
    print('formatted: {}'.format(formatted))
    print('encoded:   {}'.format(encoded))
    print('decoded:   {}'.format(decoded))
    print()

    print('current positions of the rotors:')
    print(json.dumps(e.get_positions(), indent=4))

if __name__ == '__main__':
    main()

original.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import json
from random import choice, shuffle
from string import ascii_uppercase
from enigma import emulator

def main():
    if len(sys.argv) != 2:
        sys.exit()
    n = int(sys.argv[1])
    e = emulator.Enigma()
    a = list(ascii_uppercase)
    shuffle(a)
    e.add_reflector('original reflector', {a:b for a, b in zip(a[0::2], a[1::2])})
    e.set_reflector('original reflector')
    shuffle(a)
    e.add_set_plugboard('original plugboard', {a:b for a, b in zip(a[0::2], a[1::2])})
    for i in range(n):
        name = 'original rotor {}'.format(i + 1)
        shuffle(a)
        e.add_rotor(name, ''.join(a), tuple(set((choice(a), choice(a)))))
        e.append_rotor(name, choice(a), choice(a))
    plain = 'Ich weiss wirklich nicht, warum sie funktioniert.'
    formatted = plain.replace(' ', '').replace(',', '').replace('.', 'X').upper()
    enc = e.encode(formatted)
    dec = e.decode(enc)

    print('wirings:')
    print(json.dumps(e.get_setting(), indent=4))
    print()

    print('initial positions of the rotors:')
    print(json.dumps(e.get_positions(), indent=4))
    print()

    print('plain: {}'.format(plain))
    print('formatted: {}'.format(formatted))
    print('encoded:   {}'.format(enc))
    print('decoded:   {}'.format(dec))

if __name__ == '__main__':
    main()