/usr/lib/python3/dist-packages/enigma/machine.py is in python3-enigma 0.1-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
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 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | # 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).
"""This module contains the top-level EnigmaMachine class for the Enigma Machine
simulation.
"""
import string
from .rotors.factory import create_rotor, create_reflector
from .plugboard import Plugboard
from .keyfile import get_daily_settings
class EnigmaError(Exception):
pass
# The Enigma keyboard consists of the 26 letters of the alphabet, uppercase
# only:
KEYBOARD_CHARS = string.ascii_uppercase
KEYBOARD_SET = set(KEYBOARD_CHARS)
class EnigmaMachine:
"""Top-level class for the Enigma Machine simulation."""
def __init__(self, rotors, reflector, plugboard):
"""Configures the Enigma Machine. Parameters are as follows:
rotors - a list containing 3 or 4 (for the Kriegsmarine M4 version)
Rotor objects. The order of the list is important. The first rotor is
the left-most rotor, and the last rotor is the right-most (from the
operator's perspective sitting at the machine).
reflector - a rotor object to represent the reflector (UKW)
plugboard - a plugboard object to represent the state of the plugboard
Note that on the military Enigma machines we are simulating, the entry
wheel is a simple straight-pass through and is not simulated here. It
would not be too hard to add a parameter for the entry wheel and pass a
Rotor object for it if it is desired to simulate a non-military Enigma
machine.
"""
if len(rotors) not in [3, 4]:
raise EnigmaError("Must supply 3 or 4 rotors")
self.rotors = rotors
self.rotor_count = len(rotors)
self.reflector = reflector
self.plugboard = plugboard
@classmethod
def from_key_sheet(cls, rotors='I II III', ring_settings=None,
reflector='B', plugboard_settings=None):
"""Convenience function to build an EnigmaMachine from the data as you
might find it on a key sheet:
rotors: either a list of strings naming the rotors from left to right
or a single string:
e.g. ["I", "III", "IV"] or "I III IV"
ring_settings: either a list/tuple of integers, a string, or None to
represent the ring settings to be applied to the rotors in the rotors
list. The acceptable values are:
- A list/tuple of integers with values between 0-25
- A string; either space separated letters or numbers, e.g. 'B U L'
or '2 21 12'. If numbers are used, they are 1-based to match
historical key sheet data.
- None means all ring settings are 0.
reflector: a string that names the reflector to use
plugboard_settings: a string of plugboard settings as you might find
on a key sheet; e.g.
'PO ML IU KJ NH YT GB VF RE DC'
or
'18/26 17/4 21/6 3/16 19/14 22/7 8/1 12/25 5/9 10/15'
A value of None means no plugboard connections are made.
"""
# validate inputs
if isinstance(rotors, str):
rotors = rotors.split()
num_rotors = len(rotors)
if num_rotors not in (3, 4):
raise EnigmaError("invalid rotors list size")
if ring_settings is None:
ring_settings = [0] * num_rotors
elif isinstance(ring_settings, str):
strings = ring_settings.split()
ring_settings = []
for s in strings:
if s.isalpha():
ring_settings.append(ord(s.upper()) - ord('A'))
elif s.isdigit():
ring_settings.append(int(s) - 1)
else:
raise EnigmaError('invalid ring setting: %s' % s)
if num_rotors != len(ring_settings):
raise EnigmaError("# of rotors doesn't match # of ring settings")
# assemble the machine
rotor_list = [create_rotor(r[0], r[1]) for r in zip(rotors, ring_settings)]
return cls(rotor_list,
create_reflector(reflector),
Plugboard.from_key_sheet(plugboard_settings))
@classmethod
def from_key_file(cls, fp, day=None):
"""Convenience function to read key parameters from a file.
fp - a file-like object that contains daily key settings
day - the line labeled with the day number (1-31) will be used for the
settings. If day is None, the day number will be determined from today's
date.
For more information on the file format, see keyfile.py.
"""
args = get_daily_settings(fp, day)
return cls.from_key_sheet(**args)
def set_display(self, val):
"""Sets the rotor operator windows to 'val'.
'val' must be a string or iterable containing values for each window
from left to right.
"""
if len(val) != self.rotor_count:
raise EnigmaError("Incorrect length for display value")
for i, rotor in enumerate(reversed(self.rotors)):
rotor.set_display(val[-1 - i])
def get_display(self):
"""Returns the operator display as a string."""
return "{}{}{}".format(self.rotors[-3].get_display(),
self.rotors[-2].get_display(),
self.rotors[-1].get_display())
def key_press(self, key):
"""Simulate a front panel key press.
key - a string representing the letter pressed
The rotors are stepped by simulating the mechanical action of the
machine.
Next a simulated current is run through the machine.
The lamp that is lit by this key press is returned as a string.
"""
if key not in KEYBOARD_SET:
raise EnigmaError('illegal key press %s' % key)
# simulate the mechanical action of the machine
self._step_rotors()
# simulate the electrical operations:
signal_num = ord(key) - ord('A')
lamp_num = self._electric_signal(signal_num)
return KEYBOARD_CHARS[lamp_num]
def _step_rotors(self):
"""Simulate the mechanical action of pressing a key."""
# The right-most rotor's right-side ratchet is always over a pawl, and
# it has no neighbor to the right, so it always rotates.
#
# The middle rotor will rotate if either:
# 1) The right-most rotor's left side notch is over the 2nd pawl
# or
# 2) It has a left-side notch over the 3rd pawl
#
# The third rotor (from the right) will rotate only if the middle rotor
# has a left-side notch over the 3rd pawl.
#
# Kriegsmarine model M4 has 4 rotors, but the 4th rotor (the leftmost)
# does not rotate (they did not add a 4th pawl to the mechanism).
rotor1 = self.rotors[-1]
rotor2 = self.rotors[-2]
rotor3 = self.rotors[-3]
# decide which rotors can move
rotate2 = rotor1.notch_over_pawl() or rotor2.notch_over_pawl()
rotate3 = rotor2.notch_over_pawl()
# move rotors
rotor1.rotate()
if rotate2:
rotor2.rotate()
if rotate3:
rotor3.rotate()
def _electric_signal(self, signal_num):
"""Simulate running an electric signal through the machine in order to
perform an encrypt or decrypt operation
signal_num - the wire (0-25) that the simulated current occurs on
Returns a lamp number to light (an integer 0-25).
"""
pos = self.plugboard.signal(signal_num)
for rotor in reversed(self.rotors):
pos = rotor.signal_in(pos)
pos = self.reflector.signal_in(pos)
for rotor in self.rotors:
pos = rotor.signal_out(pos)
return self.plugboard.signal(pos)
def process_text(self, text, replace_char='X'):
"""Run the text through the machine, simulating a key press for each
letter in the text.
text - the text to process. Note that the text is converted to upper
case before processing.
replace_char - if text contains a character not on the keyboard, replace
it with replace_char; if replace_char is None the character is dropped
from the message
"""
result = []
for key in text:
c = key.upper()
if c not in KEYBOARD_SET:
if replace_char:
c = replace_char
else:
continue # ignore it
result.append(self.key_press(c))
return ''.join(result)
def get_rotor_counts(self):
"""Return the rotor rotation counts as a list of integers."""
return [r.rotations for r in self.rotors]
|