changeset 2:c292c6b5e7ae

First cut at the M209 class. This is a WIP. Encryption works.
author Brian Neal <bgneal@gmail.com>
date Wed, 29 May 2013 21:32:12 -0500 (2013-05-30)
parents 8b51044f9c94
children 0a2a066fb18c
files m209/converter.py m209/data.py m209/key_wheel.py
diffstat 3 files changed, 160 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/converter.py	Wed May 29 21:32:12 2013 -0500
@@ -0,0 +1,124 @@
+# Copyright (C) 2013 by Brian Neal.
+# This file is part of m209, the M-209 simulation.
+# m209 is released under the MIT License (see LICENSE.txt).
+
+"""This module contains the M209 class, which puts together all the parts to
+assemble a complete M-209 converter.
+
+"""
+import string
+
+from . import M209Error
+from .data import KEY_WHEEL_DATA
+from .key_wheel import KeyWheel
+from .drum import Drum
+
+ALLOWED_PLAINTEXT = set(string.ascii_uppercase)
+CIPHER = list(reversed(string.ascii_uppercase))
+
+
+class M209:
+    """The M209 class is the top-level class in the M-209 simulation. It
+    aggregates key wheels and a drum and orchestrates their movements to provide
+    encrypt and decrypt functions for the operator.
+
+    """
+    def __init__(self):
+        """Build a M209 instance with all pins in the ineffective state and all
+        drum lugs in neutral positions.
+
+        """
+        self.key_wheels = [KeyWheel(*args) for args in KEY_WHEEL_DATA]
+        self.drum = Drum()
+        self.letter_counter = 0
+
+    def set_pins(self, n, effective_pins):
+        """Sets the pin settings on the key wheel specified by n, where n is
+        between 0-5, inclusive. Key wheel 0 is the left-most wheel and wheel
+        5 is the right-most.
+
+        effective_pins - must be an iterable of letters whose pins are slid to
+        the "effective" position (to the right). Letters not appearing in this
+        sequence are considered to be in the "ineffective" position (to the
+        left). If None or empty, all pins are set to be ineffective.
+
+        """
+        if not (0 <= n < len(self.key_wheels)):
+            raise M209Error("set_pins(): invalid key wheel index {}".format(n))
+        self.key_wheels[n].set_pins(effective_pins)
+
+    def set_drum_lugs(self, lug_list):
+        """Sets the drum lugs according to the given lug_list parameter.
+
+        If lug_list is None or empty, all lugs will be placed in neutral
+        positions.
+
+        Otherwise, the lug_list can either be a list or a string.
+
+        If lug_list is passed a list, it must be a list of 1 or 2-tuple integers,
+        where each integer is between 0-5, inclusive, and represents a 0-based
+        key wheel position. The list can not be longer than 27 items.
+
+        If lug_list is passed as a string, it is assumed to be in key list
+        format. That is, it must consist of at most 27 whitespace separated pairs
+        of integers separated by dashes. For example:
+                '1-0 2-0 2-0 0-3 0-5 0-5 0-6 2-4 3-6'
+
+        Each integer pair must be in the form 'm-n' where m & n are integers
+        between 0 and 6, inclusive. Each integer represents a lug position where
+        0 is a neutral position, and 1-6 correspond to key wheel positions. If
+        m & n are both non-zero, they cannot be equal.
+
+        If a string or list has less than 27 items, it is assumed all remaining
+        bars have both lugs in the neutral (0) positions.
+
+        Order in a list or string doesn't matter.
+
+        """
+        if isinstance(lug_list, str):
+            drum = Drum.from_key_list(lug_list)
+        else:
+            drum = Drum(lug_list)
+        self.drum = drum
+
+    def encrypt(self, plaintext, group=True):
+
+        ciphertext = []
+        for p in plaintext:
+            if p not in ALLOWED_PLAINTEXT:
+                raise M209Error("Illegal input to encrypt(): {}".format(p))
+
+            pins = [kw.is_effective() for kw in self.key_wheels]
+            count = self.drum.rotate(pins)
+            c = CIPHER[(ord(p) - ord('A') - count) % 26]
+            ciphertext.append(c)
+
+            for kw in self.key_wheels:
+                kw.rotate()
+
+            self.letter_count += 1
+
+        if group:
+            s = ' '.join(''.join(ciphertext[i:i+5]) for i in range(0,
+                len(ciphertext), 5))
+        else:
+            s = ''.join(ciphertext)
+
+        return s
+
+
+if __name__ == '__main__':
+
+    m209 = M209()
+    m209.set_drum_lugs('1-0 2-0 2-0 2-0 2-0 0-3 0-4 0-4 0-4 0-5 0-5 0-5 0-6 0-6 0-6 0-6 0-6 0-6 0-6 0-6 0-6 0-6 0-6 2-5 2-6 3-4 4-5')
+    m209.set_pins(0, 'BFJKLOSTUWXZ')
+    m209.set_pins(1, 'ABDJKLMORTUV')
+    m209.set_pins(2, 'EHJKNPQRSX')
+    m209.set_pins(3, 'ABCHIJLMPQR')
+    m209.set_pins(4, 'BCDGJLNOPQS')
+    m209.set_pins(5, 'AEFHIJP')
+    pt = 'A' * 26
+    print(m209.encrypt(pt))
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/data.py	Wed May 29 21:32:12 2013 -0500
@@ -0,0 +1,35 @@
+# Copyright (C) 2013 by Brian Neal.
+# This file is part of m209, the M-209 simulation.
+# m209 is released under the MIT License (see LICENSE.txt).
+
+"""This module contains important key wheel data that makes our simulation
+historically accurate and thus interoperable with actual M-209 units.
+
+"""
+
+# This list contains a 2-tuple for each key wheel in an M-209, in order from
+# left to right as an operator faces the machine.
+# The first element of each tuple is an iterable of letters for that wheel.
+# The second element is the letter whose pin interacts with the guide arm when
+# the letter "A" is being displayed to the operator.
+#
+# This information was taken from Wikipedia [1] and is understood to be
+# unclassified at this point in time. :-)
+#
+# [1]: http://en.wikipedia.org/wiki/M-209
+
+KEY_WHEEL_DATA = [
+    ("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "P"),
+    ("ABCDEFGHIJKLMNOPQRSTUVXYZ", "O"),
+    ("ABCDEFGHIJKLMNOPQRSTUVX", "N"),
+    ("ABCDEFGHIJKLMNOPQRSTU", "M"),
+    ("ABCDEFGHIJKLMNOPQRS", "L"),
+    ("ABCDEFGHIJKLMNOPQ", "K"),
+]
+
+assert(len(KEY_WHEEL_DATA[0][0]) == 26)
+assert(len(KEY_WHEEL_DATA[1][0]) == 25)
+assert(len(KEY_WHEEL_DATA[2][0]) == 23)
+assert(len(KEY_WHEEL_DATA[3][0]) == 21)
+assert(len(KEY_WHEEL_DATA[4][0]) == 19)
+assert(len(KEY_WHEEL_DATA[5][0]) == 17)
--- a/m209/key_wheel.py	Wed May 29 19:45:52 2013 -0500
+++ b/m209/key_wheel.py	Wed May 29 21:32:12 2013 -0500
@@ -57,7 +57,7 @@
     def set_pins(self, effective_pins):
         """Sets which pins are effective.
 
-        effective_pins - must be an iterable of letters whose pins are slide to
+        effective_pins - must be an iterable of letters whose pins are slid to
         the "effective" position (to the right). Letters not appearing in this
         sequence are considered to be in the "ineffective" position (to the
         left). If None or empty, all pins are set to be ineffective.