# HG changeset patch # User Brian Neal # Date 1370805504 18000 # Node ID eeda81e07800eb24fc53996c9183448697ded9b6 # Parent 8b9c6a6e7669308b3e620193259686caf7335f19 Added more procedure tests. Decided to remove group option from encrypt and z_sub option from decrypt since this procedure always groups and always performs Z substitution. diff -r 8b9c6a6e7669 -r eeda81e07800 m209/procedure.py --- a/m209/procedure.py Sun Jun 09 13:11:23 2013 -0500 +++ b/m209/procedure.py Sun Jun 09 14:18:24 2013 -0500 @@ -72,9 +72,14 @@ if key_list: self.set_key_list(key_list) + else: + self.key_list = None def get_key_list(self): - """Returns the currently installed key list object.""" + """Returns the currently installed key list object or None if one has + not been set. + + """ return self.key_list def set_key_list(self, key_list): @@ -90,16 +95,13 @@ self.m_209.set_drum_lugs(key_list.lugs) self.m_209.set_all_pins(key_list.pin_list) - def encrypt(self, plaintext, group=True, spaces=True, ext_msg_ind=None, sys_ind=None): + def encrypt(self, plaintext, spaces=True, 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: 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. ext_msg_ind - This is the external message indicator, which, if supplied, @@ -124,8 +126,8 @@ try: self.m_209.set_key_wheels(ext_msg_ind) except M209Error as ex: - raise M209Error("invalid external message indicator {} - {}".format( - ext_msg_ind, ex)) + raise ProcedureError( + "invalid external message indicator {} - {}".format(ext_msg_ind, ex)) else: ext_msg_ind = self.m_209.set_random_key_wheels() @@ -146,17 +148,17 @@ 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) + ciphertext = self.m_209.encrypt(plaintext, group=True, 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 + # If the final group in the ciphertext has less than 5 letters, pad with + # X's to make a complete 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 @@ -165,9 +167,8 @@ msg_parts = [pad1, pad2, ciphertext, pad1, pad2] - # Assemble the final message; group if requested - sep = ' ' if group else '' - return sep.join(msg_parts) + # Assemble the final message + return ' '.join(msg_parts) def set_decrypt_message(self, msg): """Prepare to decrypt the supplied message. @@ -210,14 +211,10 @@ return self.decrypt_params - def decrypt(self, z_sub=True): + def decrypt(self): """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. @@ -246,7 +243,7 @@ self.m_209.letter_counter = 0 plaintext = self.m_209.decrypt(self.decrypt_params.ciphertext, - spaces=True, z_sub=z_sub) + spaces=True, z_sub=True) return plaintext def _set_int_message_indicator(self, indicator): diff -r 8b9c6a6e7669 -r eeda81e07800 m209/tests/test_procedure.py --- a/m209/tests/test_procedure.py Sun Jun 09 13:11:23 2013 -0500 +++ b/m209/tests/test_procedure.py Sun Jun 09 14:18:24 2013 -0500 @@ -7,7 +7,7 @@ import unittest from ..keylist import KeyList -from ..procedure import StdProcedure +from ..procedure import StdProcedure, ProcedureError PLAINTEXT = 'ATTACK AT DAWN' @@ -44,3 +44,82 @@ plaintext = self.proc.decrypt() self.assertEqual(plaintext[:len(PLAINTEXT)], PLAINTEXT) + + def test_blair_decrypt(self): + + ciphertext = ( + 'DDGPD UCOFM JSCPS XZTGR HHWJG ' + 'BDKKK SHISC IMDFK RLUVH TWGAW ' + 'SUYMM VZBQP OEBJE KPMBW GPGNI ' + 'OFGAL VRYJC LSPLJ GRFYE UQVZT ' + 'PSNDT OAPYG SKGKM CKQTD JCPBE ' + 'NHYRX DDGPD UCOFM') + plaintext = ('MISSION ACCOMPLISHED X ALL ENEMY FORCES NEUTRALI ED X' + ' ERO CASUALTIES X EIGHT PRISONERS TAKEN X' + ' AWAITING FURTHER ORDERSO') + + result = self.proc.set_decrypt_message(ciphertext) + + self.assertEqual(result.sys_ind, 'D') + self.assertEqual(result.ext_msg_ind, 'GPDUCO') + self.assertEqual(result.key_list_ind, 'FM') + self.assertEqual(result.ciphertext, ciphertext[12:-12]) + + result = self.proc.decrypt() + self.assertEqual(result, plaintext) + + +class ProcedureErrorsTestCase(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') + + def test_encrypt_no_key_list(self): + proc = StdProcedure() + self.assertRaises(ProcedureError, proc.encrypt, 'TEST') + + def test_encrypt_bad_ext_msg_ind(self): + proc = StdProcedure(key_list=self.fm) + self.assertRaises(ProcedureError, proc.encrypt, 'TEST', ext_msg_ind='WWWWWW') + + def test_encrypt_bad_sys_ind(self): + proc = StdProcedure(key_list=self.fm) + self.assertRaises(ProcedureError, proc.encrypt, 'TEST', sys_ind='!') + + def test_decrypt_invalid_msg(self): + proc = StdProcedure(key_list=self.fm) + self.assertRaisesRegex(ProcedureError, 'message format', + proc.set_decrypt_message, 'TEST') + + def test_decrypt_invalid_sys_ind(self): + proc = StdProcedure(key_list=self.fm) + + ciphertext = 'GZABC DEFFM NQHNL CAARZ OLTVX GZABC DEFFM' + self.assertRaisesRegex(ProcedureError, 'system indicator', + proc.set_decrypt_message, ciphertext) + + def test_decrypt_no_params(self): + proc = StdProcedure(key_list=self.fm) + self.assertRaises(ProcedureError, proc.decrypt) + + def test_decrypt_no_key_list(self): + proc = StdProcedure() + proc.set_decrypt_message(CIPHERTEXT) + self.assertRaisesRegex(ProcedureError, "key list 'FM' required", proc.decrypt) + + def test_decrypt_wrong_key_list(self): + proc = StdProcedure(self.fm) + ciphertext = 'GGABC DEFYL NQHNL CAARZ OLTVX GGABC DEFYL' + proc.set_decrypt_message(ciphertext) + self.assertRaisesRegex(ProcedureError, "key list 'YL' required", proc.decrypt) +