エニグマ！！

# エニグマとは
暗号の歴史は古く、数千年以上に及ぶ。しかし、現代のように複雑な暗号が用いられるようになったのはコンピューターが発達して後の、直近50~60年のことである。それは、第二次世界大戦中に連合国を苦しめたエニグマ暗号とて例外ではなく、その基本原理はきわめて単純なものであった。
古来より用いられてきた最も基本的な暗号は、換字式暗号である。例えばアルファベット26文字を考えてみよう。アルファベットを順に並べ、一文字分ずらしたものと対応させてみる。

```text:アルファベットを一文字分ずらした対応表
a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, a
```

上の列のアルファベットを、対応する下の文字で置き換えることにすると、平文　"i am a cat"　は　"j bn b dbu"　と暗号化される。これが換字式暗号である。いまは一文字分ずらしたが、二文字分ずらす、なんらかの規則で順番をシャッフルするなど様々な変換方式が考えられる。
 一見すると、この暗号化された文書から変換方式を突き止め復号化するのは、きわめて困難に思える。しかし、この暗号を破る定石が存在する。頻度解析である。暗号化された文章で各アルファベットの出現頻度を集計し、その言語（英語とか）の暗号化されていない文章中でのアルファベットの出現頻度と照らし合わせる。暗号文がそれなりに長ければ、この方法で変換方式を特定することが可能である。
では、頻度解析を退けるためにはどうすればよいか？一文字ごとに変換方式をずらしてみてはどうだろうか。例えば、"i am a cat"　を暗号化する際に、一文字目 "i"はアルファベットを一文字分ずらした対応表を、二文目"a"にはアルファベットを二文字分ずらした対応表を用いるといった具合である。ただし、手作業でやるのは非常にめんどくさい。
これを自動化し、かつ同じアルゴリズムで復号化も行えるようにしたものがエニグマである。

# エニグマの仕組み

エニグマの構成要素は大きくは次の３つ、プラグボード、スクランブラー、リフレクターである。6つのアルファベットa, b, c, d, e, f のみで暗号化する場合の例を考えてみる。

![eniguma.jpg](https://qiita-image-store.s3.amazonaws.com/0/132984/bcbec902-1d7c-3e69-7504-58b78c41bd1b.jpeg)

プラグボードとリフレクターは固定されているが、スクランブラーは一文字暗号化するごとに回転する。スクランブラーが回転するごとに、上図の点線部分の配線が変化し、同一の文字でも異なる文字へと変換されるようになる。一文字暗号化されると、スクランブラー１が回転し、スクランブラー１が一回りしてもとの配置に戻ると、桁が上がってスクランブラー２が回転する。スクランブラー２が一回りすると、桁が上がり、スクランブラー３が回転する。そのため、上の例では、6*6*6通りの換字方式が順に適用される。
リフレクターは復号化のために設置されていて、ペアをつくって配線している。上の図では、文字 "a" を入力すると、配線をたどって暗号化された文字"b"が出力される。同じスクランブラーの配置の下で、逆に文字"b"を入力するとリフレクターを経由して文字"a"が返される。つまり、設定を初期化して暗号分を入力すれば、復号化された文章が返される。

# コード(エニグマ本体）

上の仕組みを、そっくりそのまま実装してみた。ただし、文字セットは偶数個であればなんでもOK。奇数個のときは少しコードを直さないといけない。
（python2を想定。たぶんpython3ではエラーがでる）

```python:enigma.py
import random
import pandas as pd

class Enigma:
    def __init__(self, characters, initial_rots=[0,0,0], seeds=[0,1,2,3,4]):        
        self.characters = characters
        self.char_num = len(characters) 
        self.seeds = seeds
        self.initial_rots = initial_rots[:]
        # mechanical parts
        # enigma machine is composed of three parts: 
        # plug board, scrumbler and reflector
        self.plug_board = self._make_converter(seeds[0]) # plug board does not rotate
        self.scrumbler_1 = self._make_converter(seeds[1]) # scrumblers rotate each time a character is encoded
        self.scrumbler_2 = self._make_converter(seeds[2])
        self.scrumbler_3 = self._make_converter(seeds[3])
        self.reflector = self._make_reflector(seeds[4]) # reflector does not rotate
        # number of rotations for each scrumblers
        self.rot_1 = 0
        self.rot_2 = 0
        self.rot_3 = 0
        # initialize settings for the scrumblers
        scrumblers = [self.scrumbler_1, self.scrumbler_2, self.scrumbler_3]
        for i in range(len(scrumblers)):
            while initial_rots[i]:
                self._rotate_scrumbler(scrumblers[i])
                initial_rots[i] -= 1

    # make a plug board or a scrumbler
    def _make_converter(self, i=0):
        char_in = self.characters[:]
        char_out = self.characters[:]
        random.seed(i)
        random.shuffle(char_out)
        return pd.DataFrame({'in':char_in, 'out':char_out})
    
    # make a reflector 
    def _make_reflector(self, i=0):
        char = self.characters[:]
        if self.char_num % 2 != 0:
            raise Exception('Length of characters must be an even nmber.')        
        random.seed(i)
        random.shuffle(char)
        char_1 = char[:self.char_num/2]
        char_2 = char[self.char_num/2:]
        df_1 = pd.DataFrame({'in':char_1, 'out':char_2})
        df_2 = pd.DataFrame({'in':char_2, 'out':char_1})
        return pd.concat([df_1, df_2], axis=0).reset_index(drop=True)    
    
    # rotate a scrunbler        
    def _rotate_scrumbler(self, scrumbler):
        for col in scrumbler.columns:
            x = list(scrumbler[col].copy())
            x.append(x.pop(0))
            scrumbler[col] = x
    
    # rotate scrumblers and update parameters
    def _rotate(self):
        if (self.rot_1 + 1) % self.char_num != 0:
            self._rotate_scrumbler(self.scrumbler_1)
            self.rot_1 += 1
        elif (self.rot_2 + 1) % self.char_num != 0:
            self._rotate_scrumbler(self.scrumbler_1)
            self._rotate_scrumbler(self.scrumbler_2)
            self.rot_1 += 1
            self.rot_2 += 1
        else:
            self._rotate_scrumbler(self.scrumbler_1)
            self._rotate_scrumbler(self.scrumbler_2)
            self._rotate_scrumbler(self.scrumbler_3)
            self.rot_1 += 1
            self.rot_2 += 1
            self.rot_3 += 1
    
    # initialize settings for the scrumblers 
    def set_rotations(self, rots=None):
        if type(rots) == str or type(rots) == unicode:
            raise Exception('set_rotations() dose not take strings')
        if rots==None:
            self.__init__(characters=self.characters, initial_rots=self.initial_rots, seeds=self.seeds)
        else: 
            old_rots = self.initial_rots[:] 
            self.__init__(characters=self.characters, initial_rots=rots, seeds=self.seeds)
            self.initial_rots = old_rots    
    
    # transmit a signal 
    def _transmit(self, scrumbler, port):
        code = scrumbler.loc[port, 'in']
        return scrumbler.index[scrumbler['out'] == code][0]
        
    def _inverse_transmit(self, scrumbler, port):
        code = scrumbler.loc[port, 'out']
        return scrumbler.index[scrumbler['in'] == code][0]
        
    # encode a character
    def _encode_character(self, ch):
        port = self.plug_board.index[self.plug_board['out'] == ch][0]
        # forward transmit
        port = self._transmit(self.scrumbler_1, port)
        port = self._transmit(self.scrumbler_2, port)
        port = self._transmit(self.scrumbler_3, port)
        # reflection
        port = self._transmit(self.reflector, port)
        # backward transmit
        port = self._inverse_transmit(self.scrumbler_3, port)
        port = self._inverse_transmit(self.scrumbler_2, port)
        port = self._inverse_transmit(self.scrumbler_1, port)
        
        return self.plug_board.loc[port, 'out']

    # encode a string
    def encode(self, string):
        code_string = ''
        for ch in string:
            code_string += self._encode_character(ch)
            self._rotate()
        return code_string
   
# character set  
alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 
              'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 
              'w', 'x', 'y', 'z', ' ', '?']
```

# Enigmaの使い方

1. 使用する文字セットをリストで与えて、Enigmaインスタンスを作成。インスタンスを作成するとき、オプションでseeds、initial_rotsを設定可能。設定しなければデフォルトが適用される。
    - seeds : プラグボード、スクランブラー、リフレクターの構造自体を変更
    - initial_rots : スクランブラーの初期位置を設定

2. 暗号化したい文章を文字列で与える。
3. 復号化するときは、スクランブラーの設定を暗号文作成直前の状態に初期化し、暗号文に対して再度暗号化を行う。

```python:エニグマの実行
import enigma

# example 1

# デフォルトの文字セットをいれておいた
print(enigma.alphabet)
# エニグマインスタンス作成
en = enigma.Enigma(characters=enigma.alphabet)
# 暗号化
hirabun = 'i am a cat'
# 暗号化
angou = en.encode(hirabun)
print(angou)
# 設定の初期化
en.set_rotations()
# 復号化
print(en.encode(angou))


# example 2

# 日本語も使える
# 文字セットは自分で用意(ユニコード型でないとエラー出るので注意)
hiragana = [u'あ', u'い', u'う', u'え', u'お', u'か']
# ひらがなセットでインスタンス作成
en = enigma.Enigma(characters=hiragana)
# 以下の機密情報を暗号化
hirabun = u'あああああああああああい'
# 暗号化
angou = en.encode(hirabun)
print(angou)
#設定の初期化
en.set_rotations()
#復号化
print(en.encode(angou))
```

# 参考文献
[暗号解読](https://www.amazon.co.jp/%E6%9A%97%E5%8F%B7%E8%A7%A3%E8%AA%AD%E3%80%88%E4%B8%8A%E3%80%89-%E6%96%B0%E6%BD%AE%E6%96%87%E5%BA%AB-%E3%82%B5%E3%82%A4%E3%83%A2%E3%83%B3-%E3%82%B7%E3%83%B3/dp/410215972X)　サイモン シン  (著), 　青木 薫 (翻訳)