changeset 4:2792ca4ffa84

Created enigma_machine class and tests.
author Brian Neal <bgneal@gmail.com>
date Sun, 24 Jun 2012 18:39:05 -0500
parents f4e25e6b76c3
children 80debdaa4f65
files enigma/SConscript enigma/enigma_utils.cpp enigma/enigma_utils.h enigma/machine.cpp enigma/machine.h enigma/tests/SConscript enigma/tests/test_machine.t.h
diffstat 7 files changed, 524 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/enigma/SConscript	Sat Jun 23 23:28:17 2012 -0500
+++ b/enigma/SConscript	Sun Jun 24 18:39:05 2012 -0500
@@ -1,9 +1,11 @@
 Import('env')
 
 sources = Split("""
+   enigma_utils.cpp
    rotor.cpp
    rotor_factory.cpp
    plugboard.cpp
+   machine.cpp
    """)
 
 env.StaticLibrary('enigma', sources)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enigma/enigma_utils.cpp	Sun Jun 24 18:39:05 2012 -0500
@@ -0,0 +1,22 @@
+// Copyright (C) 2012 by Brian Neal.
+// This file is part of Cpp-Enigma, the Enigma Machine simulation.
+// Cpp-Enigma is released under the MIT License (see License.txt).
+//
+// enigma_utils.cpp - Implementation file for enigma_utils.h.
+
+#include "enigma_utils.h"
+
+std::string enigma::remove_spaces(const std::string& s)
+{
+   std::string result;
+   result.reserve(s.size());
+
+   for (const auto& c : s)
+   {
+      if (c != ' ')
+      {
+         result += c;
+      }
+   }
+   return result;
+}
--- a/enigma/enigma_utils.h	Sat Jun 23 23:28:17 2012 -0500
+++ b/enigma/enigma_utils.h	Sun Jun 24 18:39:05 2012 -0500
@@ -6,6 +6,8 @@
 //
 // enigma_utils.h - This file contains common functions used throughout Cpp-Enigma.
 
+#include <string>
+
 namespace enigma
 {
    // This version of mod acts like Python's with respect to negative dividends.
@@ -17,6 +19,9 @@
       }
       return dividend % 26;
    }
+
+   // Removes spaces from a string and returns the resulting string:
+   std::string remove_spaces(const std::string& s);
 }
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enigma/machine.cpp	Sun Jun 24 18:39:05 2012 -0500
@@ -0,0 +1,101 @@
+// Copyright (C) 2012 by Brian Neal.
+// This file is part of Cpp-Enigma, the Enigma Machine simulation.
+// Cpp-Enigma is released under the MIT License (see License.txt).
+//
+// machine.cpp - The implementation file for the main Enigma machine class.
+
+#include <cstddef>
+#include "machine.h"
+#include "rotor.h"
+#include "rotor_factory.h"
+
+using namespace enigma;
+
+////////////////////////////////////////////////////////////////////////////////
+
+enigma_machine::enigma_machine(
+      rotor_vector rv,
+      std::unique_ptr<rotor> reflector,
+      const plugboard& pb)
+ : rotors(std::move(rv)),
+   reflector(std::move(reflector)),
+   pb(pb),
+   r_rotor(0),
+   m_rotor(0),
+   l_rotor(0)
+{
+   rotor_count_check();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+enigma_machine::enigma_machine(
+      rotor_vector rv,
+      std::unique_ptr<rotor> reflector)
+ : rotors(std::move(rv)),
+   reflector(std::move(reflector)),
+   pb(),
+   r_rotor(0),
+   m_rotor(0),
+   l_rotor(0)
+{
+   rotor_count_check();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+enigma_machine::enigma_machine(
+      const std::vector<std::string>& rotor_types,
+      const std::vector<int>& ring_settings,
+      const std::string& reflector_name,
+      const std::string& plugboard_settings)
+ : rotors(),
+   reflector(create_reflector(reflector_name.c_str())),
+   pb(plugboard_settings),
+   r_rotor(0),
+   m_rotor(0),
+   l_rotor(0)
+{
+   for (const auto& name : rotor_types)
+   {
+      rotors.push_back(create_rotor(name.c_str()));
+   }
+   rotor_count_check();
+
+   // if ring settings are supplied, there has to be one for each rotor
+   if (!ring_settings.empty())
+   {
+      if (rotors.size() != ring_settings.size())
+      {
+         throw enigma_machine_error("rotor/ring setting count mismatch");
+      }
+
+      for (std::size_t i = 0; i < rotors.size(); ++i)
+      {
+         rotors[i]->set_ring_setting(ring_settings[i]);
+      }
+   }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void enigma_machine::rotor_count_check()
+{
+   if (rotors.size() != 3 && rotors.size() != 4)
+   {
+      throw enigma_machine_error("rotor count");
+   }
+
+   if (rotors.size() == 3)
+   {
+      r_rotor = rotors[2].get();
+      m_rotor = rotors[1].get();
+      l_rotor = rotors[0].get();
+   }
+   else
+   {
+      r_rotor = rotors[3].get();
+      m_rotor = rotors[2].get();
+      l_rotor = rotors[1].get();
+   }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enigma/machine.h	Sun Jun 24 18:39:05 2012 -0500
@@ -0,0 +1,175 @@
+#ifndef CPP_ENIGMA_MACHINE_H
+#define CPP_ENIGMA_MACHINE_H
+// Copyright (C) 2012 by Brian Neal.
+// This file is part of Cpp-Enigma, the Enigma Machine simulation.
+// Cpp-Enigma is released under the MIT License (see License.txt).
+//
+// machine.h - This file contains the main Enigma machine class.
+
+#include <memory>
+#include <string>
+#include <vector>
+#include <cassert>
+#include "enigma_types.h"
+#include "rotor.h"
+#include "plugboard.h"
+
+namespace enigma
+{
+   typedef std::vector<std::unique_ptr<rotor>> rotor_vector;
+
+   class enigma_machine_error : public enigma_error
+   {
+   public:
+      explicit enigma_machine_error(const std::string& what_arg)
+       : enigma_error(what_arg)
+      {}
+   };
+
+   class enigma_machine
+   {
+   public:
+      // construct an Enigma machine from component parts:
+      enigma_machine(rotor_vector rv,
+                     std::unique_ptr<rotor> reflector,
+                     const plugboard& pb);
+
+      // construct an Enigma machine with a default plugboard (no cables connected):
+      enigma_machine(rotor_vector rv,
+                     std::unique_ptr<rotor> reflector);
+
+      // key-sheet style constructors:
+      enigma_machine(const std::vector<std::string>& rotor_types,
+                     const std::vector<int>& ring_settings,
+                     const std::string& reflector_name = "B",
+                     const std::string& plugboard_settings = "");
+
+      // set the rotor display (starting position) - 3 rotor version
+      void set_display(char left, char mid, char right)
+      {
+         assert(rotors.size() == 3);
+
+         rotors[0]->set_display(left);
+         rotors[1]->set_display(mid);
+         rotors[2]->set_display(right);
+      }
+
+      // set the rotor display (starting position) - 4 rotor version
+      void set_display(char c0, char c1, char c2, char c3)
+      {
+         assert(rotors.size() == 4);
+
+         rotors[0]->set_display(c0);
+         rotors[1]->set_display(c1);
+         rotors[2]->set_display(c2);
+         rotors[3]->set_display(c3);
+      }
+
+      // return the rotor display (starting position) as a string
+      std::string get_display() const
+      {
+         std::string result;
+         for (const auto& r : rotors)
+         {
+            result += r->get_display();
+         }
+         return result;
+      }
+
+      // simulate front panel key press; returns the lamp character that is lit
+      char key_press(char c)
+      {
+         step_rotors();
+         return electric_signal(c - 'A') + 'A';
+      }
+
+      // Process a buffer of text of length n, placing the result in an output buffer.
+      void process_text(const char* input, char* output, std::size_t n)
+      {
+         for (std::size_t i = 0; i < n; ++i)
+         {
+            *output++ = key_press(*input++);
+         }
+      }
+
+      std::string process_text(const std::string& input)
+      {
+         std::string result;
+         result.reserve(input.size());
+
+         for (const auto& c : input)
+         {
+            result += key_press(c);
+         }
+         return result;
+      }
+
+      // for access to the plugboard for hill-climbing, etc
+      plugboard& get_plugboard() { return pb; }
+
+   private:
+      rotor_vector rotors;
+      std::unique_ptr<rotor> reflector;
+      plugboard pb;
+      rotor* r_rotor;      // rightmost rotor
+      rotor* m_rotor;      // 2nd to right rotor
+      rotor* l_rotor;      // 3rd to right rotor
+
+      void rotor_count_check();
+
+      void step_rotors()
+      {
+         // The right-most rotor's right-side ratchet is always over a pawl, and
+         // it has no neighbor to the right, so it always rotates.
+         //
+         // The middle rotor will rotate if either:
+         //   1) The right-most rotor's left side notch is over the 2nd pawl
+         //       or
+         //   2) It has a left-side notch over the 3rd pawl
+         //
+         // The third rotor (from the right) will rotate only if the middle rotor
+         // has a left-side notch over the 3rd pawl.
+         //
+         // Kriegsmarine model M4 has 4 rotors, but the 4th rotor (the leftmost)
+         // does not rotate (they did not add a 4th pawl to the mechanism).
+
+         const bool l_rotate = m_rotor->notch_over_pawl();
+         const bool m_rotate = l_rotate || r_rotor->notch_over_pawl();
+
+         r_rotor->rotate();
+         if (m_rotate)
+         {
+            m_rotor->rotate();
+         }
+         if (l_rotate)
+         {
+            l_rotor->rotate();
+         }
+      }
+
+      // Simulate running an electric signal through the machine in order to
+      // perform an encrypt or decrypt operation
+      // signal_num - the wire (0-25) that the simulated current occurs on
+      // Returns a lamp number to light (an integer 0-25).
+      int electric_signal(int signal_num)
+      {
+         int pos = pb.signal(signal_num);
+
+         for (auto r = rotors.rbegin(); r != rotors.rend(); ++r)
+         {
+            pos = (*r)->signal_in(pos);
+         }
+
+         pos = reflector->signal_in(pos);
+
+         for (const auto& r : rotors)
+         {
+            pos = r->signal_out(pos);
+         }
+
+         return pb.signal(pos);
+      }
+   };
+}
+
+#endif
--- a/enigma/tests/SConscript	Sat Jun 23 23:28:17 2012 -0500
+++ b/enigma/tests/SConscript	Sun Jun 24 18:39:05 2012 -0500
@@ -3,6 +3,7 @@
 env.CxxTest('test_suite', [
       'test_rotor.t.h',
       'test_plugboard.t.h',
+      'test_machine.t.h',
    ]
 )
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enigma/tests/test_machine.t.h	Sun Jun 24 18:39:05 2012 -0500
@@ -0,0 +1,218 @@
+// Copyright (C) 2012 by Brian Neal.
+// This file is part of Cpp-Enigma, the Enigma Machine simulation.
+// Cpp-Enigma is released under the MIT License (see License.txt).
+//
+// test_machine.t.h - Unit tests for the enigma_machine class.
+
+#include <memory>
+#include <string>
+#include <vector>
+#include <cxxtest/TestSuite.h>
+#include "machine.h"
+#include "enigma_utils.h"
+
+using namespace enigma;
+
+
+class stepping_test_suite : public CxxTest::TestSuite
+{
+public:
+
+   void test_double_stepping()
+   {
+      // Ensure the rotors step realistically by testing for a "double-step"
+      // This example taken from
+      // http://users.telenet.be/d.rijmenants/en/enigmatech.htm
+      // in the section on "The Stepping Mechanism."
+
+      enigma_machine m({"III", "II", "I"}, {});
+      m.set_display('K', 'D', 'O');
+
+      const std::vector<std::string> truth_data = {
+               "KDP", "KDQ", "KER", "LFS", "LFT", "LFU",
+      };
+
+      for (const auto& expected : truth_data)
+      {
+         m.key_press('A');
+         TS_ASSERT_EQUALS(m.get_display(), expected);
+      }
+   }
+};
+
+
+class simple_cipher_test_suite : public CxxTest::TestSuite
+{
+public:
+
+   void setUp()
+   {
+      m.reset(new enigma_machine({"I", "II", "III"}, {}));
+      m->set_display('A', 'A', 'A');
+      plaintext = "AAAAA";
+      ciphertext = "BDZGO";
+   }
+
+   void test_simple_encrypt()
+   {
+      std::vector<char> buffer(plaintext.size());
+      m->process_text(plaintext.c_str(), buffer.data(), plaintext.size());
+      TS_ASSERT_EQUALS(ciphertext, std::string(buffer.begin(), buffer.end()));
+   }
+
+   void test_simple_decrypt()
+   {
+      std::vector<char> buffer(plaintext.size());
+      m->process_text(ciphertext.c_str(), buffer.data(), ciphertext.size());
+      TS_ASSERT_EQUALS(plaintext, std::string(buffer.begin(), buffer.end()));
+   }
+
+private:
+   std::unique_ptr<enigma_machine> m;
+   std::string plaintext;
+   std::string ciphertext;
+};
+
+
+// This example taken from Dirk Rijmenants' simulator manual.
+//
+// It is credited to Frode Weierud and Geoff Sullivan.
+// http://cryptocellar.com
+//
+class actual_decrypt_test_suite : public CxxTest::TestSuite
+{
+public:
+   void setUp()
+   {
+      m.reset(new enigma_machine({"II", "IV", "V"}, {1, 20, 11}, "B",
+               "AV BS CG DL FU HZ IN KM OW RX"));
+   }
+
+   void decrypt(const std::string& start,
+                const std::string& enc_key,
+                const std::string& ciphertext,
+                const std::string& truth_data)
+   {
+      // remove spaces & Kenngruppen from ciphertext
+      std::string ctext(ciphertext.begin() + 5, ciphertext.end());
+      ctext = remove_spaces(ctext);
+
+      // remove spaces from truth_data
+      const std::string expected(remove_spaces(truth_data));
+
+      // decrypt message key to get start position
+      m->set_display(start[0], start[1], start[2]);
+      std::string key = m->process_text(enc_key);
+
+      // decrypt the message with the key
+      m->set_display(key[0], key[1], key[2]);
+
+      const std::string plaintext = m->process_text(ctext);
+
+      TS_ASSERT_EQUALS(plaintext, expected);
+   }
+
+   void test_decrpyt_1()
+   {
+      const std::string ciphertext =
+                  "RFUGZ EDPUD NRGYS ZRCXN"
+                  "UYTPO MRMBO FKTBZ REZKM"
+                  "LXLVE FGUEY SIOZV EQMIK"
+                  "UBPMM YLKLT TDEIS MDICA"
+                  "GYKUA CTCDO MOHWX MUUIA"
+                  "UBSTS LRNBZ SZWNR FXWFY"
+                  "SSXJZ VIJHI DISHP RKLKA"
+                  "YUPAD TXQSP INQMA TLPIF"
+                  "SVKDA SCTAC DPBOP VHJK";
+
+      const std::string truth_data =
+                  "AUFKL XABTE ILUNG XVONX"
+                  "KURTI NOWAX KURTI NOWAX"
+                  "NORDW ESTLX SEBEZ XSEBE"
+                  "ZXUAF FLIEG ERSTR ASZER"
+                  "IQTUN GXDUB ROWKI XDUBR"
+                  "OWKIX OPOTS CHKAX OPOTS"
+                  "CHKAX UMXEI NSAQT DREIN"
+                  "ULLXU HRANG ETRET ENXAN"
+                  "GRIFF XINFX RGTX";
+
+      decrypt("WXC", "KCH", ciphertext, truth_data);
+   }
+
+   void test_decrpyt_2()
+   {
+      const std::string ciphertext =
+          "FNJAU SFBWD NJUSE GQOBH"
+          "KRTAR EEZMW KPPRB XOHDR"
+          "OEQGB BGTQV PGVKB VVGBI"
+          "MHUSZ YDAJQ IROAX SSSNR"
+          "EHYGG RPISE ZBOVM QIEMM"
+          "ZCYSG QDGRE RVBIL EKXYQ"
+          "IRGIR QNRDN VRXCY YTNJR";
+
+      const std::string truth_data =
+          "DREIG EHTLA NGSAM ABERS"
+          "IQERV ORWAE RTSXE INSSI"
+          "EBENN ULLSE QSXUH RXROE"
+          "MXEIN SXINF RGTXD REIXA"
+          "UFFLI EGERS TRASZ EMITA"
+          "NFANG XEINS SEQSX KMXKM"
+          "XOSTW XKAME NECXK";
+
+      decrypt("CRS", "YPJ", ciphertext, truth_data);
+   }
+
+private:
+   std::unique_ptr<enigma_machine> m;
+};
+
+
+// This is the Kriegsmarine example from Dirk Rijmenants' simulator manual.
+//
+// It is credited to Stefan Krah and the M4 project:
+// http://www.bytereef.org/m4_project.html
+//
+class kriegsmarine_test_suite : public CxxTest::TestSuite
+{
+public:
+
+   void test_decrypt()
+   {
+      const std::string stecker = "1/20 2/12 4/6 7/10 8/13 14/23 15/16 17/25 18/26 22/24";
+
+      enigma_machine machine({"Beta", "II", "IV", "I"}, {0, 0, 0, 21}, "B-Thin", stecker);
+
+      std::string ciphertext = remove_spaces(
+        "FCLC QRKN NCZW VUSX PNYM INHZ XMQX SFWX WLKJ AHSH NMCO CCAK UQPM KCSM"
+        "HKSE INJU SBLK IOSX CKUB HMLL XCSJ USRR DVKO HULX WCCB GVLI YXEO AHXR"
+        "HKKF VDRE WEZL XOBA FGYU JQUK GRTV UKAM EURB VEKS UHHV OYHA BCJW MAKL"
+        "FKLM YFVN RIZR VVRT KOFD ANJM OLBG FFLE OPRG TFLV RHOW OPBE KVWM UQFM"
+        "PWPA RMFH AGKX IIBG FCLC QRKM VA");
+
+      // remove the message indicators from the message (the first and last 2
+      // groups of the message -- it appears the last partial group 'VA' should
+      // be removed also)
+
+      ciphertext = std::string(ciphertext.begin() + 8, ciphertext.end() - 10);
+
+      machine.set_display('V', 'J', 'N', 'A');
+      const std::string plaintext = machine.process_text(ciphertext);
+
+      const std::string truth_data = remove_spaces(
+         "VONV ONJL OOKS JHFF TTTE"
+         "INSE INSD REIZ WOYY QNNS"
+         "NEUN INHA LTXX BEIA NGRI"
+         "FFUN TERW ASSE RGED RUEC"
+         "KTYW ABOS XLET ZTER GEGN"
+         "ERST ANDN ULAC HTDR EINU"
+         "LUHR MARQ UANT ONJO TANE"
+         "UNAC HTSE YHSD REIY ZWOZ"
+         "WONU LGRA DYAC HTSM YSTO"
+         "SSEN ACHX EKNS VIER MBFA"
+         "ELLT YNNN NNNO OOVI ERYS"
+         "ICHT EINS NULL");
+
+      TS_ASSERT_EQUALS(plaintext, truth_data);
+   }
+
+};