# HG changeset patch # User Brian Neal # Date 1340581145 18000 # Node ID 2792ca4ffa845531683c5dbe91ea862a511ed08c # Parent f4e25e6b76c3ba1d01c17da9eb5d6e2303d1345a Created enigma_machine class and tests. diff -r f4e25e6b76c3 -r 2792ca4ffa84 enigma/SConscript --- 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) diff -r f4e25e6b76c3 -r 2792ca4ffa84 enigma/enigma_utils.cpp --- /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; +} diff -r f4e25e6b76c3 -r 2792ca4ffa84 enigma/enigma_utils.h --- 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 + 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 diff -r f4e25e6b76c3 -r 2792ca4ffa84 enigma/machine.cpp --- /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 +#include "machine.h" +#include "rotor.h" +#include "rotor_factory.h" + +using namespace enigma; + +//////////////////////////////////////////////////////////////////////////////// + +enigma_machine::enigma_machine( + rotor_vector rv, + std::unique_ptr 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 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& rotor_types, + const std::vector& 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(); + } +} diff -r f4e25e6b76c3 -r 2792ca4ffa84 enigma/machine.h --- /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 +#include +#include +#include +#include "enigma_types.h" +#include "rotor.h" +#include "plugboard.h" + +namespace enigma +{ + typedef std::vector> 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 reflector, + const plugboard& pb); + + // construct an Enigma machine with a default plugboard (no cables connected): + enigma_machine(rotor_vector rv, + std::unique_ptr reflector); + + // key-sheet style constructors: + enigma_machine(const std::vector& rotor_types, + const std::vector& 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 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 diff -r f4e25e6b76c3 -r 2792ca4ffa84 enigma/tests/SConscript --- 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', ] ) diff -r f4e25e6b76c3 -r 2792ca4ffa84 enigma/tests/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 +#include +#include +#include +#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 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 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 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 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 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); + } + +};