1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
# Copyright (C) 2012 by Brian Neal.
# This file is part of Py-Enigma, the Enigma Machine simulation.
# Py-Enigma is released under the MIT License (see License.txt).
"""rotor.py - this module contains the Rotor class for the Enigma simulation."""
import string
import collections
from . import RotorError
ALPHA_LABELS = string.ascii_uppercase
# In specifying a wiring for a rotor, every letter must be included exactly
# once. This variable helps us enforce that:
WIRING_FREQ_SET = set((letter, 1) for letter in ALPHA_LABELS)
class Rotor:
"""The Rotor class represents the Enigma Machine rotors (Walzen).
A rotor has 26 circularly arranged pins on the right (entry) side and 26
contacts on the left side. Each pin is connected to a single contact by
internal wiring, thus establishing a substitution cipher. We represent this
wiring by establishing a mapping from a pin to a contact (and vice versa for
the return path). Internally we number the pins and contacts from 0-25 in a
clockwise manner with 0 being the "top".
An alphabetic or numeric ring is fastened to the rotor by the operator. The
labels of this ring are displayed to the operator through a small window on
the top panel. The ring can be fixed to the rotor in one of 26 different
positions; this is called the ring setting (Ringstellung). We will number
the ring settings from 0-25 where 0 means no offset (e.g. the letter "A" is
mapped to pin 0 on an alphabetic ring). A ring setting of 1 means the letter
"B" is mapped to pin 0.
Each rotor can be in one of 26 positions on the spindle, with position 0
where pin/contact 0 is being indicated in the operator window. The rotor
rotates towards the operator by mechanical means during normal operation as
keys are being pressed during data entry. Position 1 is thus defined to be
one step from position 0. Likewise, position 25 is the last position before
another step returns it to position 0, completing 1 trip around the spindle.
Finally, a rotor has a "stepping" or "turnover" parameter. Physically this
is implemented by putting a notch on the alphabet ring and it controls when
the rotor will "kick" the rotor to its left, causing the neighbor rotor to
rotate. Most rotors had one notch, but some Kriegsmarine rotors had 2
notches and thus rotated twice as fast.
Note that due to the system of ratchets and pawls, the middle rotor (in a 3
rotor Enigma) can "double-step". The middle rotor will advance on the next
step of the first rotor a second time in a row, if the middle rotor is in
its own turnover position.
Note that we allow the stepping parameter to be None. This indicates the
rotor does not rotate. This allows us to model the entry wheel and
reflectors as stationary rotors.
"""
def __init__(self, model_name, wiring, ring_setting=0, stepping=None):
"""Establish rotor characteristics:
model_name - e.g. "I", "II", "III", "Beta", "Gamma"
wiring - this should be a string of 26 alphabetic characters that
represents the internal wiring transformation of the signal as it enters
from the right side. This is the format used in various online
resources. For example, for the Wehrmacht Enigma type I rotor the
mapping is "EKMFLGDQVZNTOWYHXUSPAIBRCJ".
ring_setting - this should be an integer from 0-25, inclusive, which
indicates the Ringstellung. A value of 0 means there is no offset; e.g.
the letter "A" is fixed to pin 0. A value of 1 means "B" is mapped to
pin 0.
stepping - this is the stepping or turnover parameter. It should be an
iterable, for example a string such as "Q". This will indicate that when
the rotor transitions from "Q" to "R" (by observing the operator
window), the rotor will "kick" the rotor to its left, causing it to
rotate. If the rotor has more than one notch, a string of length 2 could
be used, e.g. "ZM". Another way to think of this parameter is that when
a character in the stepping string is visible in the operator window, a
notch is lined up with the pawl on the left side of the rotor. This
will allow the pawl to push up on the rotor *and* the rotor to the left
when the next key is depressed.
Note that for purposes of simulation, our rotors will always use
alphabetic labels A-Z. In reality, the Heer & Luftwaffe devices used
numbers 01-26, and Kriegsmarine devices used A-Z. Our usage of A-Z is
simply for simulation convenience. In the future we may allow either
display.
"""
self.name = model_name
self.wiring_str = wiring.upper()
self.ring_setting = ring_setting
self.pos = 0
self.rotations = 0
# check wiring length
if len(self.wiring_str) != 26:
raise RotorError("invalid wiring length")
# check wiring format; must contain A-Z
for c in self.wiring_str:
if c not in ALPHA_LABELS:
raise RotorError("invalid wiring: %s" % wiring)
# check wiring format; ensure every letter appears exactly once
input_set = set(collections.Counter(self.wiring_str).items())
if input_set != WIRING_FREQ_SET:
raise RotorError("invalid wiring frequency")
if not isinstance(ring_setting, int) or not (0 <= ring_setting < 26):
raise RotorError("invalid ring_setting")
# Create two lists to describe the internal wiring. Two lists are used
# to do fast lookup from both entry (from the right) and exit (from the
# left).
self.entry_map = [ord(pin) - ord('A') for pin in self.wiring_str]
self.exit_map = [0] * 26
for i, v in enumerate(self.entry_map):
self.exit_map[v] = i
# build a map of display values to positions
self.display_map = {}
for n in range(26):
self.display_map[chr(ord('A') + n)] = (n - self.ring_setting) % 26
# build a reverse map of position mapped to display values
self.pos_map = {v : k for k, v in self.display_map.items()}
# build step set; this is a set of positions where our notches are in
# place to allow the pawls to move
self.step_set = set()
if stepping is not None:
for pos in stepping:
if pos in self.display_map:
self.step_set.add(pos)
else:
raise RotorError("stepping: %s" % pos)
# initialize our position and display value:
self.set_display('A')
def set_display(self, val):
"""Spin the rotor such that the string val appears in the operator
window.
This sets the internal position of the rotor on the axle and thus
rotates the pins and contacts accordingly.
A value of 'A' for example puts the rotor in position 0, assuming an
internal ring setting of 0.
The parameter val must be a string in 'A' - 'Z'.
Setting the display resets the internal rotation counter to 0.
"""
s = val.upper()
if s not in self.display_map:
raise RotorError("bad display value %s" % val)
self.pos = self.display_map[s]
self.display_val = s
self.rotations = 0
def get_display(self):
"""Returns what is currently being displayed in the operator window."""
return self.display_val
def signal_in(self, n):
"""Simulate a signal entering the rotor from the right at a given pin
position n.
n must be an integer between 0 and 25.
Returns the contact number of the output signal (0-25).
"""
# determine what pin we have at that position due to rotation
pin = (n + self.pos) % 26
# run it through the internal wiring
contact = self.entry_map[pin]
# turn back into a position due to rotation
return (contact - self.pos) % 26
def signal_out(self, n):
"""Simulate a signal entering the rotor from the left at a given
contact position n.
n must be an integer between 0 and 25.
Returns the pin number of the output signal (0-25).
"""
# determine what contact we have at that position due to rotation
contact = (n + self.pos) % 26
# run it through the internal wiring
pin = self.exit_map[contact]
# turn back into a position due to rotation
return (pin - self.pos) % 26
def notch_over_pawl(self):
"""Return True if this rotor has a notch in the stepping position and
False otherwise.
"""
return self.display_val in self.step_set
def rotate(self):
"""Rotate the rotor forward due to mechanical stepping action."""
self.pos = (self.pos + 1) % 26
self.display_val = self.pos_map[self.pos]
self.rotations += 1
|