annotate m209/procedure.py @ 18:766d8da90e48

Implemented the decrypt procedure.
author Brian Neal <bgneal@gmail.com>
date Sat, 08 Jun 2013 15:03:28 -0500
parents 1448d698f9e4
children 467295d7807f
rev   line source
bgneal@15 1 # Copyright (C) 2013 by Brian Neal.
bgneal@15 2 # This file is part of m209, the M-209 simulation.
bgneal@15 3 # m209 is released under the MIT License (see LICENSE.txt).
bgneal@15 4
bgneal@15 5 """This module contains the encrypt & decrypt procedures as described in "War
bgneal@15 6 Department, Official Training Film, T.F. 11 - 1400, Army, Service Forces."
bgneal@15 7 A YouTube playlist of this film can be found here:
bgneal@15 8
bgneal@15 9 http://www.youtube.com/playlist?list=PLCPgncK_sTnEny2-uoTV-1_GC72zo-vKq
bgneal@15 10
bgneal@15 11 This procedure is also described on Mark J. Blair's pages:
bgneal@15 12
bgneal@15 13 http://www.nf6x.net/2013/04/practical-use-of-the-m-209-cipher-machine-chapter-4/
bgneal@15 14 http://www.nf6x.net/2013/04/practical-use-of-the-m-209-cipher-machine-chapter-5/
bgneal@15 15
bgneal@15 16 If other procedures are discovered, this module can be expanded to a package and
bgneal@15 17 the new procedures can be added as different modules. For now, this is the only
bgneal@15 18 procedure known to the author.
bgneal@15 19
bgneal@15 20 """
bgneal@18 21 from collections import namedtuple
bgneal@15 22 import random
bgneal@18 23 import re
bgneal@15 24
bgneal@15 25 from . import M209Error
bgneal@16 26 from .converter import M209, M209_ALPHABET_SET, M209_ALPHABET_LIST
bgneal@15 27 from .key_wheel import KeyWheelError
bgneal@18 28 from .utils import group as group_text
bgneal@15 29
bgneal@15 30
bgneal@15 31 class ProcedureError(M209Error):
bgneal@15 32 pass
bgneal@15 33
bgneal@15 34
bgneal@18 35 DecryptParams = namedtuple('DecryptParams',
bgneal@18 36 ['sys_ind', 'ext_msg_ind', 'key_list_ind', 'ciphertext'])
bgneal@18 37
bgneal@18 38
bgneal@18 39 MSG_RE = re.compile(r'^([A-Z]{5}) ([A-Z]{5}) ((?:[A-Z]{5} )+)\1 \2$')
bgneal@18 40
bgneal@18 41
bgneal@18 42 class StdProcedure:
bgneal@18 43 """This class encapsulates the "standard" encrypt/decrypt procedure for the
bgneal@18 44 M-209 as found in the training film T.F. 11 - 1400.
bgneal@15 45
bgneal@16 46 The procedure can be configured with an optional M-209, and optional key
bgneal@16 47 list to be used for the day. If the M-209 is not supplied, one will be
bgneal@16 48 created internally. Before an encrypt() operation can be performed, a key
bgneal@16 49 list must be supplied. This can be done at construction time or via the
bgneal@16 50 set_key_list() method.
bgneal@15 51
bgneal@18 52 To perform a decrypt operation, a 3-step process must be used:
bgneal@18 53 1) Call set_decrypt_message(msg) - this passes the message to be
bgneal@18 54 decrypted to the procedure and establishes the parameters to be used
bgneal@18 55 for the actual decrypt() operation. These decrypt parameters are
bgneal@18 56 returned to the caller.
bgneal@18 57 2) The caller can examine the decrypt parameters to determine which key
bgneal@18 58 list must be installed before a successful decrypt() operation can be
bgneal@18 59 carried out. The caller may call get_key_list() to examine the
bgneal@18 60 current key list. It is up to the caller to obtain the required key
bgneal@18 61 list and install it with set_key_list(), if necessary. This is done
bgneal@18 62 by ensuring the installed key list indicator matches the key_list_ind
bgneal@18 63 field of the decrypt parameters.
bgneal@18 64 3) Finally decrypt() can be called. If the procedure does not have the
bgneal@18 65 key list necessary to decrypt the message, a ProcedureError is
bgneal@18 66 raised.
bgneal@18 67
bgneal@15 68 """
bgneal@16 69 def __init__(self, m_209=None, key_list=None):
bgneal@16 70 self.m_209 = m_209 if m_209 else M209()
bgneal@18 71 self.decrypt_params = None
bgneal@15 72
bgneal@16 73 if key_list:
bgneal@16 74 self.set_key_list(key_list)
bgneal@15 75
bgneal@18 76 def get_key_list(self):
bgneal@18 77 """Returns the currently installed key list object."""
bgneal@18 78 return self.key_list
bgneal@18 79
bgneal@16 80 def set_key_list(self, key_list):
bgneal@18 81 """Use the supplied key list for all future encrypt/decrypt operations.
bgneal@15 82
bgneal@16 83 Configure the M209 with the key list parameters.
bgneal@15 84
bgneal@16 85 """
bgneal@16 86 if len(key_list.indicator) != 2:
bgneal@16 87 raise ProcedureError("invalid key list indicator")
bgneal@15 88
bgneal@16 89 self.key_list = key_list
bgneal@16 90 self.m_209.set_drum_lugs(key_list.lugs)
bgneal@16 91 self.m_209.set_all_pins(key_list.pin_list)
bgneal@15 92
bgneal@16 93 def encrypt(self, plaintext, group=True, spaces=True, ext_msg_ind=None, sys_ind=None):
bgneal@16 94 """Encrypts a plaintext message using standard procedure. The encrypted text
bgneal@16 95 with the required message indicators are returned as one string.
bgneal@15 96
bgneal@16 97 The encrypt function accepts these parameters:
bgneal@15 98
bgneal@16 99 plaintext - Input string of text to be encrypted
bgneal@16 100 group - If True, the resulting encrypted text will be grouped into 5-letter
bgneal@16 101 groups with a space between each group. If False, no spaces will be
bgneal@16 102 present in the output.
bgneal@16 103 spaces - If True, space characters in the input plaintext will automatically
bgneal@16 104 be replaced with 'Z' characters before encrypting.
bgneal@16 105 ext_msg_ind - This is the external message indicator, which, if supplied,
bgneal@16 106 must be a valid 6 letter string of key wheel settings. If not supplied,
bgneal@16 107 one will be generated randomly.
bgneal@16 108 sys_ind - This is the system indicator, which must be a string of length
bgneal@16 109 1 in the range 'A'-'Z', inclusive. If None, one is chosen at random.
bgneal@15 110
bgneal@16 111 A ProcedureError will be raised if the procedure does not have a key
bgneal@16 112 list to work with.
bgneal@15 113
bgneal@16 114 """
bgneal@16 115 # Ensure we have a key list indicator and there is no ambiguity:
bgneal@16 116
bgneal@16 117 if not self.key_list:
bgneal@16 118 raise ProcedureError("encrypt requires a key list")
bgneal@16 119
bgneal@16 120 self.m_209.letter_counter = 0
bgneal@16 121
bgneal@16 122 # Set key wheels to external message indicator
bgneal@16 123 if ext_msg_ind:
bgneal@16 124 try:
bgneal@16 125 self.m_209.set_key_wheels(ext_msg_ind)
bgneal@16 126 except M209Error as ex:
bgneal@16 127 raise M209Error("invalid external message indicator {} - {}".format(
bgneal@16 128 ext_msg_ind, ex))
bgneal@15 129 else:
bgneal@16 130 ext_msg_ind = self.m_209.set_random_key_wheels()
bgneal@15 131
bgneal@16 132 # Ensure we have a valid system indicator
bgneal@16 133 if sys_ind:
bgneal@16 134 if sys_ind not in M209_ALPHABET_SET:
bgneal@16 135 raise ProcedureError("invalid system indicator {}".format(sys_ind))
bgneal@16 136 else:
bgneal@16 137 sys_ind = random.choice(M209_ALPHABET_LIST)
bgneal@15 138
bgneal@16 139 # Generate internal message indicator
bgneal@15 140
bgneal@16 141 int_msg_ind = self.m_209.encrypt(sys_ind * 12, group=False)
bgneal@15 142
bgneal@18 143 self.m_209.letter_counter = 0
bgneal@18 144
bgneal@18 145 # Set the key wheels to the internal message indicator
bgneal@18 146 self._set_int_message_indicator(int_msg_ind)
bgneal@15 147
bgneal@16 148 # Now encipher the message on the M209
bgneal@16 149 ciphertext = self.m_209.encrypt(plaintext, group=group, spaces=spaces)
bgneal@15 150
bgneal@16 151 # If we are grouping, and the final group in the ciphertext has less than
bgneal@16 152 # 5 letters, pad with X's to make a complete group:
bgneal@16 153 if group:
bgneal@16 154 total_len = len(ciphertext)
bgneal@16 155 num_groups = total_len // 5
bgneal@16 156 num_spaces = num_groups - 1 if num_groups >= 2 else 0
bgneal@16 157 x_count = 5 - (total_len - num_spaces) % 5
bgneal@16 158 if 0 < x_count < 5:
bgneal@16 159 ciphertext = ciphertext + 'X' * x_count
bgneal@16 160
bgneal@16 161 # Add the message indicators to pad each end of the message
bgneal@16 162
bgneal@16 163 pad1 = sys_ind * 2 + ext_msg_ind[:3]
bgneal@16 164 pad2 = ext_msg_ind[3:] + self.key_list.indicator
bgneal@16 165
bgneal@16 166 msg_parts = [pad1, pad2, ciphertext, pad1, pad2]
bgneal@16 167
bgneal@16 168 # Assemble the final message; group if requested
bgneal@16 169 sep = ' ' if group else ''
bgneal@16 170 return sep.join(msg_parts)
bgneal@18 171
bgneal@18 172 def set_decrypt_message(self, msg):
bgneal@18 173 """Prepare to decrypt the supplied message.
bgneal@18 174
bgneal@18 175 The messsage can be grouped into 5-letter groups separated by spaces or
bgneal@18 176 accepted without spaces.
bgneal@18 177
bgneal@18 178 Returns a DecryptParams tuple to the caller. The caller should ensure
bgneal@18 179 the procedure instance has the required key list before calling decrypt.
bgneal@18 180
bgneal@18 181 """
bgneal@18 182 # See if we need to group the message.
bgneal@18 183 if not ' ' in msg:
bgneal@18 184 msg = group_text(msg)
bgneal@18 185
bgneal@18 186 # Perform some basic checks on the message to see if it looks like an
bgneal@18 187 # M209 message.
bgneal@18 188
bgneal@18 189 # Ensure the message passes a regex format check
bgneal@18 190 m = MSG_RE.match(msg)
bgneal@18 191 if not m:
bgneal@18 192 raise ProcedureError("invalid decrypt message format")
bgneal@18 193
bgneal@18 194 group1 = m.group(1)
bgneal@18 195 group2 = m.group(2)
bgneal@18 196
bgneal@18 197 # Check system indicator is repeated twice
bgneal@18 198 if group1[0] != group1[1]:
bgneal@18 199 raise ProcedureError("missing system indicator")
bgneal@18 200
bgneal@18 201 sys_ind = group1[0]
bgneal@18 202 ext_msg_ind = group1[2:] + group2[:3]
bgneal@18 203 key_list_ind = group2[3:]
bgneal@18 204 ciphertext = m.group(3).rstrip()
bgneal@18 205
bgneal@18 206 self.decrypt_params = DecryptParams(sys_ind=sys_ind,
bgneal@18 207 ext_msg_ind=ext_msg_ind,
bgneal@18 208 key_list_ind=key_list_ind,
bgneal@18 209 ciphertext=ciphertext)
bgneal@18 210
bgneal@18 211 return self.decrypt_params
bgneal@18 212
bgneal@18 213 def decrypt(self, z_sub=True):
bgneal@18 214 """Decrypt the message set in a previous set_decrypt_message() call. The
bgneal@18 215 resulting plaintext is returned as a string.
bgneal@18 216
bgneal@18 217 If z_sub is True, 'Z' characters in the output plaintext will be
bgneal@18 218 replaced by space characters, just like an actual M-209. If z_sub is
bgneal@18 219 False, no such substitution will occur.
bgneal@18 220
bgneal@18 221 A ProcedureError will be raised if the procedure instance has not been
bgneal@18 222 configured with the required key list.
bgneal@18 223
bgneal@18 224 """
bgneal@18 225 if not self.decrypt_params:
bgneal@18 226 raise ProcedureError("no prior call to set_decrypt_message")
bgneal@18 227
bgneal@18 228 if not self.key_list or (
bgneal@18 229 self.key_list.indicator != self.decrypt_params.key_list_ind):
bgneal@18 230 raise ProcedureError("key list '{}' required".format(
bgneal@18 231 self.decrypt_params.key_list_ind))
bgneal@18 232
bgneal@18 233 # We assume the caller called set_key_list() if necessary, so the M209
bgneal@18 234 # has been keyed.
bgneal@18 235
bgneal@18 236 self.m_209.letter_counter = 0
bgneal@18 237 self.m_209.set_key_wheels(self.decrypt_params.ext_msg_ind)
bgneal@18 238
bgneal@18 239 # Generate internal message indicator
bgneal@18 240
bgneal@18 241 int_msg_ind = self.m_209.encrypt(self.decrypt_params.sys_ind * 12,
bgneal@18 242 group=False)
bgneal@18 243
bgneal@18 244 # set key wheels to internal message indicator
bgneal@18 245 self._set_int_message_indicator(int_msg_ind)
bgneal@18 246
bgneal@18 247 self.m_209.letter_counter = 0
bgneal@18 248 plaintext = self.m_209.decrypt(self.decrypt_params.ciphertext,
bgneal@18 249 spaces=True, z_sub=z_sub)
bgneal@18 250 return plaintext
bgneal@18 251
bgneal@18 252 def _set_int_message_indicator(self, indicator):
bgneal@18 253 """Sets the key wheels to the given internal message indicator as per
bgneal@18 254 the standard procedure.
bgneal@18 255
bgneal@18 256 """
bgneal@18 257 # Set wheels to internal message indicator from left to right. We must skip
bgneal@18 258 # letters that aren't valid for a given key wheel.
bgneal@18 259 it = iter(indicator)
bgneal@18 260 n = 0
bgneal@18 261 while n != 6:
bgneal@18 262 try:
bgneal@18 263 self.m_209.set_key_wheel(n, next(it))
bgneal@18 264 except KeyWheelError:
bgneal@18 265 pass
bgneal@18 266 except StopIteration:
bgneal@18 267 assert False, "Ran out of letters building internal message indicator"
bgneal@18 268 else:
bgneal@18 269 n += 1