エニグマをpythonで実装

# 初めに
エニグマという響きがかっこよく、過去・現代・未来関係なく暗号にはロマンを感じます。作ってみたいという欲求やpythonの練習になると思い、今回実装してみました。
qiitaにも実装してみたという記事、詳細な説明記事などがたくさんあります。
以下は特に参考になった記事です。

- エニグマを実装してみた  
https://qiita.com/opengl-8080/items/995778d1cce43ed5babc
- 簡単な線形代数で考えるエニグマ暗号  
https://qiita.com/tommyecguitar/items/5e07b622eaa329ed78a2
- Enigmaの実装  
https://qiita.com/KentaKudo/items/8c0536ce684627b80fe5
- エニグマ！！  
https://qiita.com/deaikei/items/01e962c4c15b2efcc84f


# エニグマとは
wikipediaが歴史背景含め参考になります。

- エニグマ（暗号機）（https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%8B%E3%82%B0%E3%83%9E_(%E6%9A%97%E5%8F%B7%E6%A9%9F)）


# 実装手順
簡単な線形代数で考えるエニグマ暗号  https://qiita.com/tommyecguitar/items/5e07b622eaa329ed78a2
が文字の入れ替えを実行するうえで大変参考になりました。

以下は実装するうえで基本としたことです。

- アルファベットA～Zを26次元のベクトルとみなす
  - A・・・(1, 0, 0, ・・・, 0)
  - B・・・(0, 1, 0, ・・・, 0)
  - ・・・
  - Z・・・(0, 0, 0, ・・・, 1)
- 26次の単位行列 $I$ を作る
- アルファベットの $i$ 番目と $j$ 番目の入れ替えは $I$ の $i$ 行目と $j$ 行目を入れ替えた行列を右からかける
  - 例
      - 文字AとBの入れ替えは $I$ の1行目と2行目を入れ替えた行列を右からかける
      - 文字AとCの入れ替えは $I$ の1行目と3行目を入れ替えた行列を右からかける
- 簡単にするため、プラグボードとリフレクターの設定は引数で渡す
- ローターの設定はrandomモジュールを使用

こちらに置きました。

https://github.com/quryu-1/enigma

```python
import numpy as np
import random


class Enigma():
    def __init__(self, plane, alphabet, plug_perm, ref_perm):
        self.alphabet = alphabet
        self.char_dict = {x: n + 1 for n, x in enumerate(alphabet)}
        self.size = len(alphabet)
        self.identity = np.eye(self.size)
        self.plug_perm = plug_perm
        self.ref_perm = ref_perm
        self.rot1_perm = random.sample([n for n in range(1, 27)], self.size)
        self.rot2_perm = random.sample([n for n in range(1, 27)], self.size)
        self.rot3_perm = random.sample([n for n in range(1, 27)], self.size)
        self.plane = plane

```

### エニグマの設定を行列で表す

```python


    def get_matrix(self, cnt1, cnt2, cnt3):
        rot_rotor_perm = [(x + 1) % self.size for x in range(1, self.size + 1)]

        col_perm_mat = np.mat([self.identity[n - 1] for n in rot_rotor_perm])
        row_perm_mat = col_perm_mat.T

        plugbord = np.mat([self.identity[n - 1] for n in self.plug_perm])
        reflector = np.mat([self.identity[n - 1] for n in self.ref_perm])
        rotor1 = np.mat([self.identity[n - 1] for n in self.rot1_perm])
        rotor2 = np.mat([self.identity[n - 1] for n in self.rot2_perm])
        rotor3 = np.mat([self.identity[n - 1] for n in self.rot3_perm])

        m = plugbord *\
            (row_perm_mat ** cnt1) * rotor1 * (col_perm_mat ** cnt1) *\
            (row_perm_mat ** cnt2) * rotor2 * (col_perm_mat ** cnt2) *\
            (row_perm_mat ** cnt3) * rotor3 * (col_perm_mat ** cnt3) *\
            reflector *\
            ((row_perm_mat ** cnt3) * rotor3 * (col_perm_mat ** cnt3)).I *\
            ((row_perm_mat ** cnt2) * rotor2 * (col_perm_mat ** cnt2)).I *\
            ((row_perm_mat ** cnt1) * rotor1 * (col_perm_mat ** cnt1)).I *\
            plugbord
        return m
```



### ローターの回転、平文の暗号化
```python
    def get_text(self, text):
        after = ''
        tmp = ''
        cnt1 = 0
        cnt2 = 0
        cnt3 = 0
        for char in text:
            tmp = ''
            if char not in self.alphabet:
                tmp += char
            else:
                vec = [0] * self.size
                vec[self.char_dict[char] - 1] = 1
                cnt1 += 1
                if cnt1 % self.size == 0:
                    cnt2 += 1
                if cnt2 % self.size == 0:
                    cnt3 += 1
                m = self.get_matrix(cnt1, cnt2, cnt3)
                vec = (vec * m).A[0]
                for n, x in enumerate(vec):
                    if int(x) == 1:
                        for key, value in self.char_dict.items():
                            if n == value - 1:
                                tmp = key
                                break
                        else:
                            continue
                        break
            after += tmp
        return after


if __name__ == "__main__":
    alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    plane = 'HELLO WORLD. MY NAME IS ALICE. NICE TO MEET YOU.'
    plug_perm = [2, 1, 4, 3, 5, 6, 7, 8, 9, 16, 11, 12, 20, 14,
                 15, 10, 17, 18, 19, 13, 21, 22, 23, 24, 25, 26]
    ref_perm = [26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15,
                14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
    enigma = Enigma(plane, alphabet, plug_perm, ref_perm)
    cipher = enigma.get_text(plane)
    decrypt = enigma.get_text(cipher)
    print(cipher)
    print(decrypt)

```

実行結果

```
VNPKB NPJCN. CG GNXF QP JTRTS. DZKS BW UCMB NCW.
HELLO WORLD. MY NAME IS ALICE. NICE TO MEET YOU.
```

# 修正 1
- コメントで指摘して頂いたので修正
  - `char_dict` の value は 0 始まりに修正
      - これに伴い、`n - 1` だった個所はすべて `n` に修正
      -   `col_perm_mat = np.mat([self.identity[n - 1] for n in rot_rotor_perm])` はローターを回す際必要なので `n - 1` で固定
  - 文字連結について、`\` で改行をやめ `()` で連結
  - 以下の暗号化した文字を取得する箇所を簡略化

```python

for n, x in enumerate(vec):
    if int(x) == 1:
        tmp = self.alphabet[n]
        break
```

修正前とは見違えるように読みやすくなりました。

```python
class Enigma():
    def __init__(self, plane, alphabet, plug_perm, ref_perm):
        self.alphabet = alphabet
        self.char_dict = {x: n for n, x in enumerate(alphabet)}
        self.size = len(alphabet)
        self.identity = np.eye(self.size)
        self.plug_perm = plug_perm
        self.ref_perm = ref_perm
        self.rot1_perm = random.sample(range(self.size), self.size)
        self.rot2_perm = random.sample(range(self.size), self.size)
        self.rot3_perm = random.sample(range(self.size), self.size)
        self.plane = plane

    def get_matrix(self, cnt1, cnt2, cnt3):
        rot_rotor_perm = list(range(self.size))

        col_perm_mat = np.mat([self.identity[n - 1] for n in rot_rotor_perm])
        row_perm_mat = col_perm_mat.T

        plugbord = np.mat([self.identity[n] for n in self.plug_perm])
        reflector = np.mat([self.identity[n] for n in self.ref_perm])
        rotor1 = np.mat([self.identity[n] for n in self.rot1_perm])
        rotor2 = np.mat([self.identity[n] for n in self.rot2_perm])
        rotor3 = np.mat([self.identity[n] for n in self.rot3_perm])

        m = (plugbord *
             (row_perm_mat ** cnt1) * rotor1 * (col_perm_mat ** cnt1) *
             (row_perm_mat ** cnt2) * rotor2 * (col_perm_mat ** cnt2) *
             (row_perm_mat ** cnt3) * rotor3 * (col_perm_mat ** cnt3) *
             reflector *
             ((row_perm_mat ** cnt3) * rotor3 * (col_perm_mat ** cnt3)).I *
             ((row_perm_mat ** cnt2) * rotor2 * (col_perm_mat ** cnt2)).I *
             ((row_perm_mat ** cnt1) * rotor1 * (col_perm_mat ** cnt1)).I *
             plugbord)
        return m

    def get_text(self, text):
        after = ''
        tmp = ''
        cnt1 = 0
        cnt2 = 0
        cnt3 = 0
        for char in text:
            tmp = ''
            if char not in self.alphabet:
                tmp += char
            else:
                vec = [0] * self.size
                vec[self.char_dict[char]] = 1
                cnt1 += 1
                if cnt1 % self.size == 0:
                    cnt2 += 1
                if cnt2 % self.size == 0:
                    cnt3 += 1
                m = self.get_matrix(cnt1, cnt2, cnt3)
                vec = (vec * m).A[0]
                for n, x in enumerate(vec):
                    if int(x) == 1:
                        tmp = self.alphabet[n]
                        break
            after += tmp
        return after


if __name__ == "__main__":
    alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    plane = 'HELLO WORLD. MY NAME IS ALICE. NICE TO MEET YOU.'
    plug_perm = [2, 1, 4, 3, 5, 6, 7, 8, 9, 16, 11, 12, 20, 14,
                 15, 10, 17, 18, 19, 13, 21, 22, 23, 24, 25, 26]
    ref_perm = [26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15,
                14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
    plug_perm = [x - 1 for x in plug_perm]
    ref_perm = [x - 1 for x in ref_perm]
    enigma = Enigma(plane, alphabet, plug_perm, ref_perm)
    cipher = enigma.get_text(plane)
    decrypt = enigma.get_text(cipher)
    print(cipher)
    print(decrypt)
```

# 修正2

- コメントで指摘して頂いたので修正
  - `rotor2` が 1/26 回転した直後から、`rotor3` がずっと回転した状態になっていた

- 修正前

```python
cnt1 += 1
if cnt1 % self.size == 0:
    cnt2 += 1
if cnt2 % self.size == 0:
    cnt3 += 1
```

- 修正後

```python
cnt1 += 1
if cnt1 % self.size == 0:
    cnt2 += 1
if cnt1 % self.size ** 2 == 0:
    cnt3 += 1
```

# 修正3

- コメントで教えて頂いた方法
  - divmod()で以下を実現
      - cnt2, cnt3 のインクリメント不要（cnt1 のインクリメントを用いる）
      - 26で割りきれるかどうかに関する条件分岐不要
      - 常に 0 $\leq$ cnt1. cnt2. cnt3 $\leq$ 25


こちらを使わせて頂きたいと思います。

- 修正前


```python
    def get_text(self, text):
        after = ''
        tmp = ''
        cnt1 = 0
        cnt2 = 0
        cnt3 = 0
        for char in text:
            tmp = ''
            if char not in self.alphabet:
                tmp += char
            else:
                vec = [0] * self.size
                vec[self.char_dict[char]] = 1
                cnt1 += 1
                if cnt1 % self.size == 0:
                    cnt2 += 1
                if cnt2 % self.size == 0:
                    cnt3 += 1
                m = self.get_matrix(cnt1, cnt2, cnt3)
                vec = (vec * m).A[0]
                for n, x in enumerate(vec):
                    if int(x) == 1:
                        tmp = self.alphabet[n]
                        break
            after += tmp
        return after
```

- 修正後（※以下はコメント欄から引用しています）

```python
    def get_text(self, text):
        after = ''
        cnt = 0
        for char in text:
            if char not in self.alphabet:
                after += char
                continue
            vec = [0] * self.size
            vec[self.char_dict[char]] = 1
            cnt += 1
            cnt2, cnt1 = divmod(cnt, self.size)
            cnt3, cnt2 = divmod(cnt2, self.size)
            m = self.get_matrix(cnt1, cnt2, cnt3 % self.size)
            vec = (vec * m).A[0]
            for n, x in enumerate(vec):
                if int(x) == 1:
                    after += self.alphabet[n]
                    break
        return after
```
  

## 参考記事
- エニグマを実装してみた（https://qiita.com/opengl-8080/items/995778d1cce43ed5babc）
- 簡単な線形代数で考えるエニグマ暗号（https://qiita.com/tommyecguitar/items/5e07b622eaa329ed78a2）
- Enigmaの実装（https://qiita.com/KentaKudo/items/8c0536ce684627b80fe5）
- エニグマ！！（https://qiita.com/deaikei/items/01e962c4c15b2efcc84f）
- エニグマ（暗号機）（https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%8B%E3%82%B0%E3%83%9E_(%E6%9A%97%E5%8F%B7%E6%A9%9F)）