Source code for crypto_enigma.cypher

#!/usr/bin/env python
# encoding: utf8

# Copyright (C) 2016 by Roy Levien.
# This file is part of crypto-enigma, an Enigma Machine simulator.
# released under the BSD-3 License (see LICENSE.txt).

"""
This is a supporting module that implements the simple substitution cypher employed by the Enigma machine to
encode messages. It will not generally be used directly.
"""

from __future__ import (absolute_import, print_function, division, unicode_literals)

from .utils import *


# A note on the use of string indexing to implement encryption:
# Improvements from implementing mappings as lists of numbers rather than strings are negligible and not worth the
# loss of clarity and correspondence to the underlying math.


# TBD - Fix encapsulation here; sould not be used by other modules (e.g., reversed encoding should start with mapping) <<<
def num_A0(c):
    return ord(c) - ord('A')


def chr_A0(n):
    return chr(n + ord('A'))


[docs]class Mapping(unicode): """A substitution cypher mapping. The Enigma machine, and the components from which it is constructed, use **mappings** to perform a `simple substitution encoding`_. Mappings describe * the cryptographic effects of each component's fixed `~.components.Component.wiring`; * the encoding they perform individually in a machine based on their rotational `~EnigmaConfig.positions` and the direction in which a signal passes through them (see `~.components.Component.mapping`); and, * the progressive (`~EnigmaConfig.stage_mapping_list`) and overall (`~EnigmaConfig.enigma_mapping_list` and `~EnigmaConfig.enigma_mapping`) encoding performed by the machine as a whole. """ def __init__(self, str): """Mappings are expressed as a string of letters indicating the mapped-to letter for the letter at that position in the alphabet — i.e., as a permutation of the alphabet. For example, the mapping **EKMFLGDQVZNTOWYHXUSPAIBRCJ** encodes **A** to **E**, **B** to **K**, **C** to **M**, ..., **Y** to **C**, and **Z** to **J**: >>> mpg = Mapping(u'EKMFLGDQVZNTOWYHXUSPAIBRCJ') >>> mpg.encode_string(u'ABCYZJ') u'EKMCJZ' >>> mpg.encode_string(u'ABCDEFGHIJKLMNOPQRSTUVWXYZ') == mpg True Note that there is no way to directly create `Mapping` for use by an `EnigmaMachine` or `Component`. The closest one can get is to configure a plugboard with `component`: >>> component(u'AE.BK.CM.FD').wiring # doctest: +ELLIPSIS u'EKMF...' """ super(Mapping, self).__init__() self._len = len(self) # standard simple-substitution cypher encoding
[docs] def encode_char(self, ch): """Encode a single character using the mapping. Args: ch (char): A character to encode using the `Mapping`. Returns: chr: The character, replaced with the corresponding characters in the `Mapping`. Example: In the context of this package, this is most useful in low level analysis of the encoding process: >>> wng = component(u'AE.BK.CM.FD').wiring >>> wng.encode_char(u'A') u'E' >>> wng.encode_char(u'K') u'B' >>> wng.encode_char(u'Q') u'Q' For example, it can be used to confirm that only letters connected in a plugboard are unaltered by the encoding it performs: >>> pbd = component(u'AE.BK.CM.FD') >>> all(pbd.wiring.encode_char(c) == c for c in filter(lambda c: c not in pbd.name, u'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) True >>> all(pbd.wiring.encode_char(c) != c for c in filter(lambda c: c in pbd.name, u'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) True """ if 0 <= num_A0(ch) < self._len: return self[num_A0(ch)] else: return ' '
[docs] def encode_string(self, string): """Encode a string using the mapping. Args: string (str): A string to encode using the `Mapping`. Returns: str: A string consisting of each of the characters replaced with the corresponding character in the `Mapping`. Examples: This just the collected results of applying `encode_char` to each letter of the string: >>> component(u'AE.BK.CM.FD').wiring.encode_string(u'ABKCFEKMD') u'EKBMDABCF' >>> ''.join(component(u'AE.BK.CM.FD').wiring.encode_char(c) for c in u'ABKCFEKMD') u'EKBMDABCF' Note that, critically, the mapping used by an Enigma machine *changes before each character is encoded* so that: .. testsetup:: cfg = EnigmaConfig.config_enigma(u"B-I-II-III", u"ABC", u"XO.YM.QL", u"01.02.03") str=u'ABKCJFIRUFDLSLKFDHLSJHFLSDJFHLJSDHFLSJDFHSLDJFHSLDFJHSFEKMD' >>> cfg.enigma_mapping().encode_string(str) != cfg.enigma_encoding(str) True """ return ''.join([self.encode_char(ch) for ch in string])