# HG changeset patch # User Brian Neal # Date 1370721808 18000 # Node ID 766d8da90e48f88e5f89eb5165fb3ed3cf69b4ba # Parent c8027f88443fbc02aec4d513ab0a255d6817c694 Implemented the decrypt procedure. diff -r c8027f88443f -r 766d8da90e48 m209/converter.py --- a/m209/converter.py Sat Jun 08 12:56:25 2013 -0500 +++ b/m209/converter.py Sat Jun 08 15:03:28 2013 -0500 @@ -13,6 +13,7 @@ from .data import KEY_WHEEL_DATA from .key_wheel import KeyWheel, KeyWheelError from .drum import Drum +from .utils import group as group_text M209_ALPHABET_LIST = string.ascii_uppercase M209_ALPHABET_SET = set(string.ascii_uppercase) @@ -168,8 +169,7 @@ ciphertext.append(self._cipher(p)) if group: - s = ' '.join(''.join(ciphertext[i:i+5]) for i in range(0, - len(ciphertext), 5)) + s = group_text(ciphertext) else: s = ''.join(ciphertext) return s diff -r c8027f88443f -r 766d8da90e48 m209/procedure.py --- a/m209/procedure.py Sat Jun 08 12:56:25 2013 -0500 +++ b/m209/procedure.py Sat Jun 08 15:03:28 2013 -0500 @@ -18,20 +18,30 @@ procedure known to the author. """ +from collections import namedtuple import random +import re from . import M209Error from .converter import M209, M209_ALPHABET_SET, M209_ALPHABET_LIST from .key_wheel import KeyWheelError +from .utils import group as group_text class ProcedureError(M209Error): pass -class StdEncryptProcedure: - """This class encapsulates the "standard" encrypt procedure for the M-209 as - found in the training film T.F. 11 - 1400. +DecryptParams = namedtuple('DecryptParams', + ['sys_ind', 'ext_msg_ind', 'key_list_ind', 'ciphertext']) + + +MSG_RE = re.compile(r'^([A-Z]{5}) ([A-Z]{5}) ((?:[A-Z]{5} )+)\1 \2$') + + +class StdProcedure: + """This class encapsulates the "standard" encrypt/decrypt procedure for the + M-209 as found in the training film T.F. 11 - 1400. The procedure can be configured with an optional M-209, and optional key list to be used for the day. If the M-209 is not supplied, one will be @@ -39,15 +49,36 @@ list must be supplied. This can be done at construction time or via the set_key_list() method. + To perform a decrypt operation, a 3-step process must be used: + 1) Call set_decrypt_message(msg) - this passes the message to be + decrypted to the procedure and establishes the parameters to be used + for the actual decrypt() operation. These decrypt parameters are + returned to the caller. + 2) The caller can examine the decrypt parameters to determine which key + list must be installed before a successful decrypt() operation can be + carried out. The caller may call get_key_list() to examine the + current key list. It is up to the caller to obtain the required key + list and install it with set_key_list(), if necessary. This is done + by ensuring the installed key list indicator matches the key_list_ind + field of the decrypt parameters. + 3) Finally decrypt() can be called. If the procedure does not have the + key list necessary to decrypt the message, a ProcedureError is + raised. + """ def __init__(self, m_209=None, key_list=None): self.m_209 = m_209 if m_209 else M209() + self.decrypt_params = None if key_list: self.set_key_list(key_list) + def get_key_list(self): + """Returns the currently installed key list object.""" + return self.key_list + def set_key_list(self, key_list): - """Use the supplied key list for all future encrypt operations. + """Use the supplied key list for all future encrypt/decrypt operations. Configure the M209 with the key list parameters. @@ -109,19 +140,10 @@ int_msg_ind = self.m_209.encrypt(sys_ind * 12, group=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: - self.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 + self.m_209.letter_counter = 0 + + # Set the key wheels to the internal message indicator + self._set_int_message_indicator(int_msg_ind) # Now encipher the message on the M209 ciphertext = self.m_209.encrypt(plaintext, group=group, spaces=spaces) @@ -146,3 +168,102 @@ # Assemble the final message; group if requested sep = ' ' if group else '' return sep.join(msg_parts) + + def set_decrypt_message(self, msg): + """Prepare to decrypt the supplied message. + + The messsage can be grouped into 5-letter groups separated by spaces or + accepted without spaces. + + Returns a DecryptParams tuple to the caller. The caller should ensure + the procedure instance has the required key list before calling decrypt. + + """ + # See if we need to group the message. + if not ' ' in msg: + msg = group_text(msg) + + # Perform some basic checks on the message to see if it looks like an + # M209 message. + + # Ensure the message passes a regex format check + m = MSG_RE.match(msg) + if not m: + raise ProcedureError("invalid decrypt message format") + + group1 = m.group(1) + group2 = m.group(2) + + # Check system indicator is repeated twice + if group1[0] != group1[1]: + raise ProcedureError("missing system indicator") + + sys_ind = group1[0] + ext_msg_ind = group1[2:] + group2[:3] + key_list_ind = group2[3:] + ciphertext = m.group(3).rstrip() + + self.decrypt_params = DecryptParams(sys_ind=sys_ind, + ext_msg_ind=ext_msg_ind, + key_list_ind=key_list_ind, + ciphertext=ciphertext) + + return self.decrypt_params + + def decrypt(self, z_sub=True): + """Decrypt the message set in a previous set_decrypt_message() call. The + resulting plaintext is returned as a string. + + If z_sub is True, 'Z' characters in the output plaintext will be + replaced by space characters, just like an actual M-209. If z_sub is + False, no such substitution will occur. + + A ProcedureError will be raised if the procedure instance has not been + configured with the required key list. + + """ + if not self.decrypt_params: + raise ProcedureError("no prior call to set_decrypt_message") + + if not self.key_list or ( + self.key_list.indicator != self.decrypt_params.key_list_ind): + raise ProcedureError("key list '{}' required".format( + self.decrypt_params.key_list_ind)) + + # We assume the caller called set_key_list() if necessary, so the M209 + # has been keyed. + + self.m_209.letter_counter = 0 + self.m_209.set_key_wheels(self.decrypt_params.ext_msg_ind) + + # Generate internal message indicator + + int_msg_ind = self.m_209.encrypt(self.decrypt_params.sys_ind * 12, + group=False) + + # set key wheels to internal message indicator + self._set_int_message_indicator(int_msg_ind) + + self.m_209.letter_counter = 0 + plaintext = self.m_209.decrypt(self.decrypt_params.ciphertext, + spaces=True, z_sub=z_sub) + return plaintext + + def _set_int_message_indicator(self, indicator): + """Sets the key wheels to the given internal message indicator as per + the standard procedure. + + """ + # 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(indicator) + n = 0 + while n != 6: + try: + self.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 diff -r c8027f88443f -r 766d8da90e48 m209/tests/test_procedure.py --- a/m209/tests/test_procedure.py Sat Jun 08 12:56:25 2013 -0500 +++ b/m209/tests/test_procedure.py Sat Jun 08 15:03:28 2013 -0500 @@ -7,7 +7,7 @@ import unittest from ..keylist import KeyList -from ..procedure import StdEncryptProcedure +from ..procedure import StdProcedure PLAINTEXT = 'ATTACK AT DAWN' @@ -28,8 +28,19 @@ 'ACDFHIMN' ], letter_check='TNMYS CRMKK UHLKW LDQHM RQOLW R') - self.proc = StdEncryptProcedure(key_list=self.fm) + self.proc = StdProcedure(key_list=self.fm) def test_encrypt(self): result = self.proc.encrypt(PLAINTEXT, ext_msg_ind='ABCDEF', sys_ind='G') self.assertEqual(result, CIPHERTEXT) + + def test_decrypt(self): + result = self.proc.set_decrypt_message(CIPHERTEXT) + + self.assertEqual(result.sys_ind, 'G') + self.assertEqual(result.ext_msg_ind, 'ABCDEF') + self.assertEqual(result.key_list_ind, 'FM') + self.assertEqual(result.ciphertext, CIPHERTEXT[12:29]) + + plaintext = self.proc.decrypt() + self.assertEqual(plaintext[:len(PLAINTEXT)], PLAINTEXT) diff -r c8027f88443f -r 766d8da90e48 m209/utils.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/m209/utils.py Sat Jun 08 15:03:28 2013 -0500 @@ -0,0 +1,11 @@ +# 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 various utility functions.""" + + +def group(text, n=5): + """Groups the given text into n-letter groups separated by spaces.""" + + return ' '.join(''.join(text[i:i+n]) for i in range(0, len(text), n))