changeset 15:2e2692fb7de6

Created an encrypt procedure and a first test for it.
author Brian Neal <bgneal@gmail.com>
date Fri, 07 Jun 2013 22:30:33 -0500 (2013-06-08)
parents da830ef4ba52
children 1448d698f9e4
files m209/converter.py m209/key_wheel.py m209/keylist/__init__.py m209/procedure.py m209/tests/test_converter.py m209/tests/test_procedure.py
diffstat 6 files changed, 238 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/m209/converter.py	Thu Jun 06 20:32:37 2013 -0500
+++ b/m209/converter.py	Fri Jun 07 22:30:33 2013 -0500
@@ -11,10 +11,11 @@
 
 from . import M209Error
 from .data import KEY_WHEEL_DATA
-from .key_wheel import KeyWheel
+from .key_wheel import KeyWheel, KeyWheelError
 from .drum import Drum
 
-M209_ALPHABET = set(string.ascii_uppercase)
+M209_ALPHABET_LIST = string.ascii_uppercase
+M209_ALPHABET_SET = set(string.ascii_uppercase)
 CIPHER_TABLE = list(reversed(string.ascii_uppercase))
 
 
@@ -110,6 +111,18 @@
             drum = Drum(lug_list)
         self.drum = drum
 
+    def set_key_wheel(self, n, c):
+        """Set key wheel n to the letter c, where n is 0-5, inclusive.
+
+        Key wheel 0 is the leftmost key wheel, and 5 is the rightmost.
+
+        May raise KeyWheelError if c is not valid for key wheel n.
+
+        """
+        if not (0 <= n < len(self.key_wheels)):
+            raise M209Error("set_key_wheel(): invalid key wheel index {}".format(n))
+        self.key_wheels[n].set_pos(c)
+
     def set_key_wheels(self, s):
         """Set the key wheels from left to right to the six letter string s."""
 
@@ -117,7 +130,18 @@
             raise M209Error("Invalid key wheels setting length")
 
         for n in range(6):
-            self.key_wheels[n].set_pos(s[n])
+            try:
+                self.key_wheels[n].set_pos(s[n])
+            except KeyWheelError as ex:
+                raise KeyWheelError('wheel #{}: {}'.format(n, ex))
+
+    def set_random_key_wheels(self):
+        """Sets the 6 key wheels to random letters and returns the letters as
+        a string.
+
+        """
+        letters = [kw.set_random() for kw in self.key_wheels]
+        return ''.join(letters)
 
     def get_settings(self):
         """Returns the current settings as a M209Settings named tuple."""
@@ -188,7 +212,7 @@
         the internal substitution table.
 
         """
-        if c not in M209_ALPHABET:
+        if c not in M209_ALPHABET_SET:
             raise M209Error("Illegal char: {}".format(c))
 
         pins = [kw.is_effective() for kw in self.key_wheels]
--- a/m209/key_wheel.py	Thu Jun 06 20:32:37 2013 -0500
+++ b/m209/key_wheel.py	Fri Jun 07 22:30:33 2013 -0500
@@ -6,8 +6,11 @@
 simulation.
 
 """
+import random
+
 from . import M209Error
 
+
 class KeyWheelError(M209Error):
     """Exception class for all key wheel errors"""
     pass
@@ -120,3 +123,13 @@
             self.pos = self.letter_offsets[c]
         except KeyError:
             raise KeyWheelError("Invalid position {}".format(c))
+
+    def set_random(self):
+        """Sets the position of the key wheel to a random letter.
+
+        The random letter is returned as a string.
+
+        """
+        c = random.choice(self.letters)
+        self.set_pos(c)
+        return c
--- a/m209/keylist/__init__.py	Thu Jun 06 20:32:37 2013 -0500
+++ b/m209/keylist/__init__.py	Fri Jun 07 22:30:33 2013 -0500
@@ -0,0 +1,5 @@
+# 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).
+
+from .key_list import KeyList
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/procedure.py	Fri Jun 07 22:30:33 2013 -0500
@@ -0,0 +1,136 @@
+# 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 encrypt & decrypt procedures as described in "War
+Department, Official Training Film, T.F. 11 - 1400, Army, Service Forces."
+A YouTube playlist of this film can be found here:
+
+http://www.youtube.com/playlist?list=PLCPgncK_sTnEny2-uoTV-1_GC72zo-vKq
+
+This procedure is also described on Mark J. Blair's pages:
+
+http://www.nf6x.net/2013/04/practical-use-of-the-m-209-cipher-machine-chapter-4/
+http://www.nf6x.net/2013/04/practical-use-of-the-m-209-cipher-machine-chapter-5/
+
+If other procedures are discovered, this module can be expanded to a package and
+the new procedures can be added as different modules. For now, this is the only
+procedure known to the author.
+
+"""
+import random
+
+from . import M209Error
+from .converter import M209_ALPHABET_SET, M209_ALPHABET_LIST
+from .key_wheel import KeyWheelError
+
+
+class ProcedureError(M209Error):
+    pass
+
+
+def encrypt(m_209, plaintext, group=True, spaces=True, key_list=None,
+            key_list_ind=None, ext_msg_ind=None, sys_ind=None):
+    """Encrypts a plaintext message using standard procedure. The encrypted text
+    with the required message indicators are returned as one string.
+
+    The encrypt function accepts these parameters:
+
+    m_209 - A M209 converter instance
+    plaintext - Input string of text to be encrypted
+    group - If True, the resulting encrypted text will be grouped into 5-letter
+        groups with a space between each group. If False, no spaces will be
+        present in the output.
+    spaces - If True, space characters in the input plaintext will automatically
+        be replaced with 'Z' characters before encrypting.
+    key_list - If not None, this must be a KeyList instance and it will be
+        used to setup the m_209 machine. If None, it is assumed the M209 is
+        already setup in the desired configuration.
+    key_list_ind - If key_list is None, then this parameter must be supplied,
+        and it is the key list indicator that was used to setup the M209.
+    ext_msg_ind - This is the external message indicator, which, if supplied,
+        must be a valid 6 letter string of key wheel settings. If not supplied,
+        one will be generated randomly.
+    sys_ind - This is the system indicator, which must be a string of length
+        1 in the range 'A'-'Z', inclusive. If None, one is chosen at random.
+
+    It is an error to supply both key_list and key_list_ind options. Supply one
+    or the other. A ProcedureError will be raised if both are supplied or if
+    neither are supplied.
+
+    """
+    # Ensure we have a key list indicator and there is no ambiguity:
+
+    if (key_list and key_list_ind) or (not key_list and not key_list_ind):
+        raise ProcedureError("encrypt requires either key_list or key_list_ind but not both")
+
+    # Setup M209 machine if necessary
+
+    if key_list:
+        m_209.set_drum_lugs(key_list.lugs)
+        m_209.set_all_pins(key_list.pin_list)
+        key_list_ind = key_list.indicator
+
+    if len(key_list_ind) != 2:
+        raise ProcedureError("key list indicator must be two letters")
+
+    m_209.letter_counter = 0
+
+    # Set key wheels to external message indicator
+    if ext_msg_ind:
+        try:
+            m_209.set_key_wheels(ext_msg_ind)
+        except M209Error as ex:
+            raise M209Error("invalid external message indicator {} - {}".format(
+                ext_msg_ind, ex))
+    else:
+        ext_msg_ind = m_209.set_random_key_wheels()
+
+    # Ensure we have a valid system indicator
+    if sys_ind:
+        if sys_ind not in M209_ALPHABET_SET:
+            raise ProcedureError("invalid system indicator {}".format(sys_ind))
+    else:
+        sys_ind = random.choice(M209_ALPHABET_LIST)
+
+    # Generate internal message indicator
+
+    int_msg_ind = m_209.encrypt(sys_ind * 12, group=False, spaces=False)
+
+    # Set wheels to internal message indicator from left to right. We must skip
+    # letters that aren't valid for a given key wheel.
+    it = iter(int_msg_ind)
+    n = 0
+    while n != 6:
+        try:
+            m_209.set_key_wheel(n, next(it))
+        except KeyWheelError:
+            pass
+        except StopIteration:
+            assert False, "Ran out of letters building internal message indicator"
+        else:
+            n += 1
+
+    # Now encipher the message on the M209
+    ciphertext = m_209.encrypt(plaintext, group=group, spaces=spaces)
+
+    # If we are grouping, and the final group in the ciphertext has less than
+    # 5 letters, pad with X's to make a complete group:
+    if group:
+        total_len = len(ciphertext)
+        num_groups = total_len // 5
+        num_spaces = num_groups - 1 if num_groups >= 2 else 0
+        x_count = 5 - (total_len - num_spaces) % 5
+        if 0 < x_count < 5:
+            ciphertext = ciphertext + 'X' * x_count
+
+    # Add the message indicators to pad each end of the message
+
+    pad1 = sys_ind * 2 + ext_msg_ind[:3]
+    pad2 = ext_msg_ind[3:] + key_list_ind
+
+    msg_parts = [pad1, pad2, ciphertext, pad1, pad2]
+
+    # Assemble the final message; group if requested
+    sep = ' ' if group else ''
+    return sep.join(msg_parts)
--- a/m209/tests/test_converter.py	Thu Jun 06 20:32:37 2013 -0500
+++ b/m209/tests/test_converter.py	Fri Jun 07 22:30:33 2013 -0500
@@ -87,6 +87,23 @@
         check = 'OZGPK AFVAJ JYRZW LRJEG MOVLU M'
         self.letter_check(lugs, pin_list, check)
 
+    def test_fm_letter_check(self):
+        """See if we can pass a letter check using Mark J. Blair's FM key list."""
+
+        lugs = '1-0 2-0*8 0-3*7 0-4*5 0-5*2 1-5 1-6 3-4 4-5'
+
+        pin_list = [
+            'BCEJOPSTUVXY',
+            'ACDHJLMNOQRUYZ',
+            'AEHJLOQRUV',
+            'DFGILMNPQS',
+            'CEHIJLNPS',
+            'ACDFHIMN'
+        ]
+
+        check = 'TNMYS CRMKK UHLKW LDQHM RQOLW R'
+        self.letter_check(lugs, pin_list, check)
+
     def test_no_group(self):
 
         m = M209(AA_LUGS, AA_PIN_LIST)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/tests/test_procedure.py	Fri Jun 07 22:30:33 2013 -0500
@@ -0,0 +1,39 @@
+# 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).
+
+"""Unit tests for the M209 encrypt & decrypt procedures."""
+
+import unittest
+
+from ..keylist import KeyList
+from ..procedure import encrypt
+from ..converter import M209
+
+
+PLAINTEXT = 'ATTACK AT DAWN'
+CIPHERTEXT = 'GGABC DEFFM NQHNL CAARZ OLTVX GGABC DEFFM'
+
+
+class ProcedureTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.fm = KeyList(indicator="FM",
+                    lugs = '1-0 2-0*8 0-3*7 0-4*5 0-5*2 1-5 1-6 3-4 4-5',
+                    pin_list = [
+                        'BCEJOPSTUVXY',
+                        'ACDHJLMNOQRUYZ',
+                        'AEHJLOQRUV',
+                        'DFGILMNPQS',
+                        'CEHIJLNPS',
+                        'ACDFHIMN'
+                    ],
+                    letter_check = 'TNMYS CRMKK UHLKW LDQHM RQOLW R')
+        self.m_209 = M209()
+
+    def test_encrypt(self):
+
+        result = encrypt(self.m_209, PLAINTEXT, key_list=self.fm,
+                ext_msg_ind='ABCDEF', sys_ind='G')
+
+        self.assertEqual(result, CIPHERTEXT)