# HG changeset patch # User Brian Neal # Date 1370662233 18000 # Node ID 2e2692fb7de6cdb0b2bc48d1dfad71d3105d59dd # Parent da830ef4ba52a479589d2c709d8aab0123183615 Created an encrypt procedure and a first test for it. diff -r da830ef4ba52 -r 2e2692fb7de6 m209/converter.py --- 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] diff -r da830ef4ba52 -r 2e2692fb7de6 m209/key_wheel.py --- 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 diff -r da830ef4ba52 -r 2e2692fb7de6 m209/keylist/__init__.py --- 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 diff -r da830ef4ba52 -r 2e2692fb7de6 m209/procedure.py --- /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) diff -r da830ef4ba52 -r 2e2692fb7de6 m209/tests/test_converter.py --- 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) diff -r da830ef4ba52 -r 2e2692fb7de6 m209/tests/test_procedure.py --- /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)