初めに
エニグマという響きがかっこよく、過去・現代・未来関係なく暗号にはロマンを感じます。作ってみたいという欲求や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モジュールを使用
こちらに置きました。
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
エニグマの設定を行列で表す
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
ローターの回転、平文の暗号化
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
で固定
- これに伴い、
- 文字連結について、
\
で改行をやめ()
で連結 - 以下の暗号化した文字を取得する箇所を簡略化
-
for n, x in enumerate(vec):
if int(x) == 1:
tmp = self.alphabet[n]
break
修正前とは見違えるように読みやすくなりました。
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
がずっと回転した状態になっていた
-
-
修正前
cnt1 += 1
if cnt1 % self.size == 0:
cnt2 += 1
if cnt2 % self.size == 0:
cnt3 += 1
- 修正後
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
- divmod()で以下を実現
こちらを使わせて頂きたいと思います。
- 修正前
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
- 修正後(※以下はコメント欄から引用しています)
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))