changeset 35:dec8cd7da4d3

Wrote out most of the user's guide. Added plugboard info to the ref. manual.
author Brian Neal <bgneal@gmail.com>
date Sun, 03 Jun 2012 17:33:43 -0500
parents 4a9c4d3ced13
children 8347542c24a3
files enigma/docs/source/guide.rst enigma/docs/source/overview.rst enigma/docs/source/reference.rst enigma/machine.py
diffstat 4 files changed, 438 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/enigma/docs/source/guide.rst	Sat Jun 02 22:32:42 2012 -0500
+++ b/enigma/docs/source/guide.rst	Sun Jun 03 17:33:43 2012 -0500
@@ -1,2 +1,349 @@
 User's guide
 ============
+
+This short guide attempts to get you up and running with Py-Enigma quickly. For
+more detailed information, please see the :doc:`Reference manual <reference>`.
+
+
+If you are new to Enigma machines
+---------------------------------
+
+This guide assumes you know the basics of Enigma machines. Before proceeding
+with Py-Enigma please explore some of the links presented in the
+:ref:`references-label`. For the most complete and detailed description of how
+an Enigma machine works, please see Dirk Rijmenants' excellent `Technical
+Details of the Enigma Machine
+<http://users.telenet.be/d.rijmenants/en/enigmatech.htm>`_.
+
+Building your Enigma machine
+----------------------------
+
+If you are interested in working with historically accurate Enigma machines, the
+easiest way to build your first machine is to use the "key sheet" shortcut
+functions. If instead you wish to experiment with custom designed rotors or
+configurations, you can build a machine out of separate components by hand.
+These two approaches are demonstrated in the following sections.
+
+Using key sheet shortcuts
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+During the war, Enigma machine operators re-configured their machines every day
+according to a code book, or key sheet, to help increase security. Each key
+sheet contained daily Enigma settings for one month. Before transmitting the
+first message of the day, the operator looked up the current day on the key
+sheet for the given month and configured the machine accordingly. The key sheet
+specified:
+
+* *Walzenlage*: what rotors to use, and what order to put them into the machine
+* *Ringstellung*: the ring settings for each rotor
+* *Steckerverbindungen*: the plugboard connections
+* *Kenngruppen*: special text fragments that should be transmitted to identify
+  the transmitter's key settings to any receiver. This is also known as the
+  *message indicator*.
+
+The reflector setting was usually fixed and not changed once in the field. The
+choice of reflector seems to have been decided at the unit level to establish
+different networks. Of course our simulation is not hindered by these logistical
+concerns, and our simulated key sheets will also specify reflector type.
+
+When an Enigma machine operator received a message from a radio operator,
+probably his first task was to determine what key settings were used to transmit
+the message. For example, the message could have been transmitted the day
+before, and he was only handed the message just now. This was accomplished by
+transitting (in the clear) certain text fragments, the so-called *Kenngruppen*,
+at certain points in the message. By examining these text groups, the operator
+could scan the key sheet for today and perhaps the past few days and hopefully
+identify what day the message was sent. The operator would then reconfigure his
+Enigma machine accordingly and decode the message. The *Kenngruppen* was ignored
+when decrypting the actual message.
+
+The :class:`EnigmaMachine <enigma.machine.EnigmaMachine>` class has two
+class methods for constructing machines from key sheet data. The first class
+method is called :meth:`from_key_sheet
+<enigma.machine.EnigmaMachine.from_key_sheet>`::
+
+   from enigma.machine import EnigmaMachine
+
+   machine = EnigmaMachine.from_key_sheet(
+          rotors='IV V I', 
+          reflector='B',
+          ring_settings='21 15 16',
+          plugboard_settings='AC LS BQ WN MY UV FJ PZ TR OK')
+
+This is all well and good if you wish to simulate an army or air force Enigma
+machine. But what about navy (*Kriegsmarine*) models? Navy Enigma machines and
+key sheets have slightly different nomenclature. This is also no problem for
+Py-Enigma::
+   
+   machine = EnigmaMachine.from_key_sheet(
+          rotors='Beta VII IV V',
+          reflector='B-Thin',
+          ring_settings='G N O',
+          plugboard_settings='18/26 17/4 21/6 3/16 19/14 22/7 8/1 12/25 5/9 10/15')
+
+Some notes on the parameters:
+
+* ``rotors`` can either be a space separated list of rotor names, or a list of
+  rotor name strings. For a complete list of supported rotor names, see
+  :ref:`rotor-table-label`.
+* ``reflector`` is a string that names the reflector to use. For a complete list
+  of supported reflector names, see :ref:`reflector-table-label`.
+* ``ring_settings`` can be a space separated list of uppercase letters or
+  numbers, as would be found on a key sheet. An empty string or ``None`` means
+  ring settings of all 'A' or 1.
+* ``plugboard_settings`` can either be space separated uppercase letter pairs,
+  or slash separated numbers. Note that 'AB' is equivalent to '1/2', etc.
+
+.. warning::
+
+   ``ring_settings`` can also take a list of integers, but these integers are
+   **0-based**. Remember that when using a string of numbers they are
+   **1-based** to correspond to actual historical key sheet data. In other
+   words, these values produce identical ring settings: ``[0, 5, 15]``,
+   ``'A F P'``, and ``'1 6 16'``.
+
+
+Constructing by hand
+~~~~~~~~~~~~~~~~~~~~
+
+It is also possible to "build an Enigma machine by hand" by explicitly providing
+the component objects to the :class:`EnigmaMachine
+<enigma.machine.EnigmaMachine>` constructor. This makes it possible to invent
+different rotor and reflector types::
+
+   from enigma.rotors.rotor import Rotor
+   from enigma.plugboard import Plugboard
+   from enigma.machine import EnigmaMachine
+
+   r1 = Rotor('my rotor1', 'EKMFLGDQVZNTOWYHXUSPAIBRCJ', ring_setting=0, stepping='Q')
+   r2 = Rotor('my rotor2', 'AJDKSIRUXBLHWTMCQGZNPYFVOE', ring_setting=5, stepping='E')
+   r3 = Rotor('my rotor3', 'BDFHJLCPRTXVZNYEIWGAKMUSQO', ring_setting=15, stepping='V')
+
+   reflector = Rotor('my reflector', 'FVPJIAOYEDRZXWGCTKUQSBNMHL')
+
+   pb = Plugboard.from_key_sheet('PO ML IU KJ NH YT GB VF RE DC')
+
+   machine = EnigmaMachine([r1, r2, r3], reflector, pb)
+
+This example illustrates a few different things:
+
+* When calling the :class:`Rotor <enigma.rotors.rotor.Rotor>` constructor
+  directly, the internal wiring is specified as a 26-character long string which
+  specifies the cipher substitution. This notation is consistent with several
+  online sources of Enigma information.
+* :class:`Rotor <enigma.rotors.rotor.Rotor>` ``ring_setting`` arguments are
+  0-based integers (0-25).
+* :class:`Rotor <enigma.rotors.rotor.Rotor>` ``stepping`` arguments specify 
+  when rotors turn their neighbors. For more information see the 
+  :class:`Rotor <enigma.rotors.rotor.Rotor>` reference.
+* Reflectors are simulated as rotors that have no ring setting or stepping
+  capability.
+* :class:`Plugboard <enigma.plugboard.Plugboard>` objects have a convenient
+  :meth:`from_key_sheet <enigma.plugboard.Plugboard.from_key_sheet>` class method 
+  constructor that works in exactly the same way as the previous example.
+* When calling the :class:`EnigmaMachine <enigma.machine.EnigmaMachine>`
+  constructor directly, the rotor assignment is specified by a list of rotors
+  where order specifies the left-to-right order in the machine.
+
+.. note::
+
+   If you decide to create your own reflector, and you desire to maintain
+   reciprocal encryption & decryption (a fundamental characteristic of war-time
+   Enigma machines), your connections must be made in pairs. Thus if you wire
+   'A' to 'G', you must also wire 'G' to 'A', and so on.
+
+For more details on the various constructor arguments, please see the
+:doc:`reference`.
+   
+
+Encrypting & Decrypting
+-----------------------
+
+Now that you have built your Enigma machine, you probably want to start using it
+to encrypt and decrypt text! The first step is to set your initial rotor
+positions. This is critical if you want someone else to understand your message!
+
+::
+
+   machine.set_display('XYZ')       # set rotor positions
+
+The value given to :meth:`set_display
+<enigma.machine.EnigmaMachine.set_display>` is a simple string, which must have
+one uppercase letter per rotor in your machine. In this example, we are
+setting the leftmost rotor to 'X', the middle rotor to 'Y', and the rightmost
+rotor to 'Z'.
+
+If you ever need to obtain the current rotor positions, you can use the
+:meth:`get_display <enigma.machine.EnigmaMachine.get_display>` method::
+
+   position = machine.get_display()    # read rotor position
+
+.. note::
+
+   The :meth:`set_display <enigma.machine.EnigmaMachine.set_display>` method
+   always takes letters for simulation convenience. If you are simulating an
+   Enigma machine with numeric rotors, you'll have to translate the numbers to
+   the appropriate letters. On actual Enigma machines, a label on the inside box
+   lid had such a table to aid the operator.
+
+Next, you can simulate a single key press::
+
+   c = machine.key_press('A')
+
+The input to :meth:`key_press <enigma.machine.EnigmaMachine.key_press>` is a
+string that consists of a single uppercase letter. Invalid input will raise an
+``EnigmaError`` exception. The transformed text is returned.
+
+To process a whole string of text::
+
+   c = machine.process_text('This is a test!', replace_char='X')
+
+The :meth:`process_text <enigma.machine.EnigmaMachine.process_text>` method
+accepts an arbitrary string and performs some processing on it before internally
+calling :meth:`key_press <enigma.machine.EnigmaMachine.key_press>` on each element of
+the string. 
+
+First, all input is converted to uppercase. Next, any character not in the
+Enigma uppercase character set is either replaced or dropped from the input
+according to the ``replace_char`` parameter. If ``replace_char`` is a string of
+one character, it is used as the replacement character. If it is ``None``, the
+invalid input character is removed from the message. Thus the previous example
+is equivalent to::
+
+   c = machine.process_text('THISXISXAXTESTX')
+
+This is all you need to start creating encrypted and decrypted messages.
+
+
+Example communication procedure
+-------------------------------
+
+The Wehrmacht had various elaborate procedures for transmitting and receiving
+messages. These procedures varied by service branch and also changed during the
+course of the war.  In general, the Kriegsmarine procedures were more elaborate
+and involved not only key sheets but other auxiliary documents. On top of this,
+each branch of the military had its own conventions for encoding abbreviations,
+numbers, space characters, place names, etc. Important words or phrases may need
+to be repeated or stressed in some way. 
+
+We will now present a simplified scenario based on a procedure employed by the
+army (*Heer*) after 1940. This example is based upon one found in Dirk
+Rijmenants' simulator manual, which is based upon a real-life example from Frode
+Weierud's `Cryptocellar <http://cryptocellar.org>`_ website.
+
+Suppose a message needs to be transmitted. The operator of the transmitting
+machine consults his key sheet and configures his machine according to the daily
+settings found inside. Let's suppose the key sheet dictates the following
+initial parameters for the current day:
+
+* Rotor usage and order is *II IV V*
+* Ring settings for each rotor, in order, are: *B U L*
+* Plugboard settings are: *AV BS CG DL FU HZ IN KM OW RX*
+* One of the daily Kenngruppen possibilities is *UGZ*
+
+Let us also assume the reflector employed by this army unit is 'B'.
+
+The operator then configures his machine::
+
+   machine = EnigmaMachine.from_key_sheet(
+          rotors='II IV V',
+          reflector='B',
+          ring_settings='B U L',
+          plugboard_settings='AV BS CG DL FU HZ IN KM OW RX')
+
+Suppose the Enigma operator was handed a message for transmit by an officer
+which reads "The Russians are coming!". The operator would first randomly decide
+two things:
+
+* Initial rotor positions, say ``WXC``
+* A three letter *message key*, say ``BLA``
+
+The operator would then turn the rotor thumb wheels to set the initial rotor
+position and then type the three letter message key to produce an encrypted
+message key::
+
+   machine.set_display('WXC')    # set initial rotor positions
+   enc_key = machine.process_text('BLA')      # encrypt message key
+
+In this example, the encrypted key turns out to be ``KCH``. This is written down
+for later. 
+
+The operator then sets the rotors to the unencrypted message key ``BLA`` and
+then types in the officer's message, performing various substitutions and
+transformations according to training and current procedures. In our simple
+case, he performs the following::
+
+   machine.set_display('BLA')    # use message key BLA
+   ciphertext = machine.process_text('THEXRUSSIANSXAREXCOMINGX')
+   print(ciphertext)
+
+This produces the ciphertext ``NIBLFMYMLLUFWCASCSSNVHAZ``.
+
+Next, between the Enigma operator and the radio operator, a message is formed
+up. This message includes the following components:
+
+* The time of transmission
+* The station identification for transmitter and intended recipient(s)
+* The message length; in our case this is 24
+* The initial rotor positions in unencrypted form (``WXC``)
+* The encrypted message key value (``KCH``)
+* The unencrypted message indicator (*Kenngruppen*)
+* The encrypted message contents
+
+In our example, the message handed over to the radio operator to be transmitted
+by either Morse code or perhaps even voice would look something like this::
+
+   U6Z DE C 1500 = 24 = WXC KCH =
+
+   BNUGZ NIBLF MYMLL UFWCA 
+   SCSSN VHAZ=
+
+The top line indicates day 31, station C transmits to station U6Z, sent at 1500
+hours and contains 24 letters. The starting position is ``WXC`` and the
+encrypted message key is ``KCH``.
+
+Next we have the body of the message. The army transmitted messages in 5 letter
+groups. The first group contains the Kenngruppen, or indicator. Procedure
+required the operator pick one of the Kenngruppen possibilities from the key
+sheet, and then pad it out with two random letters. Here the operator chose to
+prepend ``BN`` to the Kenngruppen value of ``UGZ``. He could have also appended
+the two letters, or perhaps appended one and prepended the other.
+
+After the message indicator group, the encrypted text follows in 5 letter
+groups.
+
+Now at receiving station U6Z, the radio operator receives the over-the-air
+message and types or writes it up in the form shown and hands it to the Enigma
+operator.
+
+The Enigma operator first looks for the message indicator. He uses the group
+``BNUGZ`` and scans his key sheet for either ``BNU``, ``NUG``, or ``UGZ``. He
+could presumably also use the date information found in the message preamble to
+help his search of the key sheet. If everything checks out the operator now
+knows which entry in his monthly key sheet to use.  Thus, as was done at the
+transmitting station, he configures his Enigma according to the key sheet::
+
+   machine = EnigmaMachine.from_key_sheet(
+          rotors='II IV V',
+          reflector='B',
+          ring_settings='B U L',
+          plugboard_settings='AV BS CG DL FU HZ IN KM OW RX')
+
+The receiving operator then must decrypt the message key::
+
+   machine.set_display('WXC')
+   msg_key = machine.process_text('KCH')
+
+This should reveal that the message key is the original ``BLA``. The rotors are
+then set to this value and the message can be decrypted, taking care to ignore
+the Kenngruppen::
+
+   machine.set_display(msg_key)     # original message key is BLA
+   plaintext = machine.process_text('NIBLFMYMLLUFWCASCSSNVHAZ')
+   print(plaintext)
+
+The Enigma operator then decodes the message "THEXRUSSIANSXAREXCOMINGX". He then
+uses his training and procedures to further process the message. Finally, the
+somewhat troubling message "The Russians are coming" is handed to his commanding
+officer.
+
--- a/enigma/docs/source/overview.rst	Sat Jun 02 22:32:42 2012 -0500
+++ b/enigma/docs/source/overview.rst	Sun Jun 03 17:33:43 2012 -0500
@@ -83,6 +83,8 @@
    $ hg clone https://bitbucket.org/bgneal/enigma
 
 
+.. _references-label:
+
 Acknowledgements & References
 -----------------------------
 
--- a/enigma/docs/source/reference.rst	Sat Jun 02 22:32:42 2012 -0500
+++ b/enigma/docs/source/reference.rst	Sun Jun 03 17:33:43 2012 -0500
@@ -8,7 +8,9 @@
 --------------
 
 The ``EnigmaMachine`` class represents an assembled Enigma machine that consists
-of rotors, a plugboard, a keyboard, and indicator lamps.
+of rotors, a plugboard, a keyboard, and indicator lamps. The keyboard and lamps
+act as input and outputs. The other components are represented by Python
+classes.
 
 
 EnigmaMachine class reference
@@ -55,7 +57,8 @@
 
       * A list/tuple of integers with values between 0-25.
       * A string; either space separated letters or numbers, e.g.
-        ``'B U L'`` or ``'1 20 11'``.
+        ``'B U L'`` or ``'1 20 11'``. Note that if numbers are used, they
+        should be between 1-26 to match historical key sheet data.
       * ``None`` means all ring settings are 0.
 
       The ``plugboard_settings`` parameter can accept either:
@@ -347,6 +350,7 @@
    :param string model: the model name to create; see the :ref:`rotor-table-label` table
    :param integer ring_setting: the ring setting (0-25) to use
    :returns: the newly created :class:`Rotor <enigma.rotors.rotor.Rotor>`
+   :raises RotorError: when an unknown model name is provided
 
 
 .. function:: enigma.rotors.factory.create_reflector(model)
@@ -357,6 +361,7 @@
    :param string model: the model name to create; see the :ref:`reflector-table-label` table
    :returns: the newly created reflector, which is actually of type
       :class:`Rotor <enigma.rotors.rotor.Rotor>`
+   :raises RotorError: when an unknown model name is provided
 
 
 Rotor exceptions
@@ -367,3 +372,82 @@
 if the rotor object is given an invalid parameter during a :meth:`set_display
 <enigma.rotors.rotor.Rotor.set_display>` operation.
 
+
+Plugboards
+----------
+
+The plugboard, or *Steckerbrett* in German, allows the operator to swap up to 10
+keys and indicator lamps for increased key strength.
+
+Plugboards have little use on their own. They are placed inside an :class:`EnigmaMachine
+<enigma.machine.EnigmaMachine>` object, which then calls the public ``Plugboard``
+methods.
+
+Plugboard class reference
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: enigma.plugboard.Plugboard([wiring_pairs=None])
+
+   The plugboard allows the operator to swap letters before and after the entry
+   wheel. This is accomplished by connecting cables between pairs of plugs that
+   are marked with letters (Heer & Luftwaffe models) or numbers (Kriegsmarine).
+   Ten cables were issued with each machine; thus up to 10 of these swappings
+   could be used as part of a machine setup.
+
+   Each cable swaps both the input and output signals. Thus if A is connected
+   to B, A crosses to B in the keyboard to entry wheel direction and also in
+   the reverse entry wheel to lamp direction.
+
+   The constructor configures the plugboard according to a list or tuple of
+   integer pairs, or None.
+
+   :param wiring_pairs: A value of ``None`` or an empty list/tuple indicates no
+      plugboard connections are to be used (i.e. a straight mapping).  Otherwise
+      ``wiring_pairs`` must be an iterable of integer pairs, where each integer
+      is between 0-25, inclusive. At most 10 such pairs can be specified. Each
+      value represents an input/output path through the plugboard. It is invalid
+      to specify the same path more than once in the list.
+
+   :raises PlugboardError: If an invalid ``wiring_pairs`` parameter is given.
+
+   .. classmethod:: from_key_sheet([settings=None])
+
+      This is a convenience function to build a plugboard according to a 
+      settings string as you may find on a key sheet.
+
+      Two syntaxes are supported, the Heer/Luftwaffe and Kriegsmarine styles:
+
+      In the Heer syntax, the settings are given as a string of
+      alphabetic pairs. For example: ``'PO ML IU KJ NH YT GB VF RE DC'``.
+
+      In the Kriegsmarine syntax, the settings are given as a string of number
+      pairs, separated by a '/'. Note that the numbering uses 1-26, inclusive.
+      For example: ``'18/26 17/4 21/6 3/16 19/14 22/7 8/1 12/25 5/9 10/15'``.
+
+      To specify no plugboard connections, settings can be ``None`` or an empty
+      string.
+
+      :param settings: A settings string as described above, or ``None``.
+      :raises PlugboardError: If the settings string is invalid, or if
+         it contains more than 10 pairs. Each plug should be present at
+         most once in the settings string.
+
+   .. method:: signal(n)
+
+      Simulate a signal entering the plugboard on wire n, where n must be
+      an integer between 0 and 25.
+
+      :param integer n: The wire number the input signal is on (0-25).
+      :returns: The wire number of the output signal (0-25).
+      :rtype: integer
+
+      Note that since the plugboard always crosses pairs of wires, it doesn't
+      matter what direction (keyboard -> entry wheel or vice versa) the signal
+      is coming from.
+
+
+Plugboard exceptions
+~~~~~~~~~~~~~~~~~~~~
+
+:class:`Plugboard <enigma.plugboard.Plugboard>` objects may raise
+``enigma.plugboard.PlugboardError`` when an invalid constructor argument is given.
--- a/enigma/machine.py	Sat Jun 02 22:32:42 2012 -0500
+++ b/enigma/machine.py	Sun Jun 03 17:33:43 2012 -0500
@@ -68,7 +68,8 @@
         list. The acceptable values are:
             - A list/tuple of integers with values between 0-25
             - A string; either space separated letters or numbers, e.g. 'B U L'
-              or '1 20 11'
+              or '2 21 12'. If numbers are used, they are 1-based to match
+              historical key sheet data.
             - None means all ring settings are 0.
 
         reflector: a string that names the reflector to use
@@ -99,7 +100,7 @@
                 if s.isalpha():
                     ring_settings.append(ord(s.upper()) - ord('A'))
                 elif s.isdigit():
-                    ring_settings.append(int(s))
+                    ring_settings.append(int(s) - 1)
                 else:
                     raise EnigmaError('invalid ring setting: %s' % s)