執筆者: кемо
最終更新: 2024/12/17
こんにちは,KEMOです.C++の学習ついでにエニグマ暗号機を作ってみました.某講義でエニグマの話が少しだけ出たので,勢いで作成を決定しちゃいました.適当にガチャガチャしてたらできた気がするため,備忘録的な感じで公開します.初心者が書いたのでご意見あればKEMOにください.
そもそもエニグマ暗号機なんて知らねーよって方向けに解説します.エニグマは1918年にドイツ人によって開発された電気機械式暗号機械です.名称はギリシア語で「謎」を意味しています(強そう).第二次世界大戦でドイツ軍によって用いられました.
結構すごい暗号で周辺国はこいつの解読に手を焼く訳ですが,イギリスのチューリングというすごい学者によって解読されてしまいます(映画にもなってる).コンピュータ分野でチューリングの名を聞かない人はいないでしょう.彼の作ったコンピュータによって解読され,コンピュータの偉大さを伝えるため擦られ続ける憐れな暗号……それこそがエニグマです.要はやられ役ですね.
実は戦前に全方位イキリ外交を行っていたポーランドによって解読されていましたが,ドイツ軍はローターとプラグボード(後述)を追加するという荒業で突破しました.
ローターはエニグマにおける暗号化の中心となってる円盤です.この円盤の外側には文字がついたリング,内側には暗号化用の配線があります.1文字暗号化するたびに1回転することで,26パターン(アルファベットだけの場合)の換字を可能にします.このままだと少ないですよね.初期型では3枚(後に5枚)のローターを連結させることで263パターンの換字ができるようになりました.1枚目のローターは毎回回転しますが,その後に続くローターは前のローターが特定の場所に来た時に回転する設計です.
このパーツがエニグマの特徴といえます.構造としてはある配線から送られてきた信号を別の配線に返すだけです.詳しい説明は省きますが,リフレクターのおかげでエニグマではAがBと変換されるならBがAに変換されるという性質を持っています.これによって暗号文を入力すれば平文が返ってくるというシンプルな構造で運用ができるんですね.しかし,弱点も同時に獲得してしまっています.それはある文字Aは同じ文字Aに換字されないという性質です.暗号にとってパターンを潰されるというのは結構な欠点になってしまいます.
上記ローターとリフレクターは後から配線を変えることができません.しかし,実戦で使用するとなると敵に鹵獲される可能性があります.そこで,ドイツ軍によって導入されたのがプラグボードです.両端にプラグがついたケーブルで文字同士を入れ替えるパーツです.プラグボードは現地で配線を変更することができます.初期は3組の入れ替えでしたが後に5組の入れ替えとなります.
じゃあコード書いていきます.C++の勉強中だからクソコードでも許して.あと,(書いてる時の)わかりやすさ重視のため文字列操作を基本としてます.
まずはロータークラスから
class Rotor
{
private:
std::string ring;
std::string scrambler;
public:
Rotor() = default;
Rotor(std::string code)
{
ring = R"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~)";
scrambler = code;
}
void display_scrambler(void)
{
for(size_t i=0;i<scrambler.size();i++){
std::cout << i << ":" << ring[i] << ":" << scrambler[i] << std::endl;
}
std::cout << std::endl;
}
std::string get_scrambler(void)
{
return scrambler;
}
int forward_scrambling(int input)
{
for(size_t i=0;i<scrambler.size();i++){
if(scrambler[input]==ring[i]){
return i;
}
}
return -1;
}
int back_scrambling(int input)
{
for(size_t i=0;i<scrambler.size();i++){
if(scrambler[i]==ring[input]){
return i;
}
}
return -1;
}
void rotate()
{
scrambler = scrambler.substr(1) + scrambler[0];
ring = ring.substr(1) + ring[0];
}
void set_start_point(char point)
{
while(ring[0] != point)
{
rotate();
}
}
};
メンバ変数のringはローターの外側の刻印,scramblerが内側の配線となっています.本来のエニグマではA~Zの入出力を可能にしていましたが,今回はせっかくなのでASCIIの記号全部乗せで行きます.
他はコンストラクタと表示用の関数です.
次はリフレクター
class Reflector
{
private:
std::string wiring;
public:
Reflector(std::string code)
{
wiring = code;
}
void display_code(void)
{
for(size_t i=0;i<wiring.size();i++){
std::cout << wiring[i] << std::endl;
}
std::cout << std::endl;
}
int reflect(int input)
{
for(size_t i=0;i<wiring.size();i++){
if(wiring[i]==wiring[input] && i!=input){
return i;
}
}
return -1;
}
};
メンバ変数のwiringがリフレクター内部の配線です.reflect関数が反転を行います.
プラグボード
class PlugBoard
{
private:
char plug1_node1;
char plug1_node2;
char plug2_node1;
char plug2_node2;
char plug3_node1;
char plug3_node2;
public:
PlugBoard(char node[])
{
plug1_node1 = node[0];
plug1_node2 = node[1];
plug2_node1 = node[2];
plug2_node2 = node[3];
plug3_node1 = node[4];
plug3_node2 = node[5];
}
void display_connection(void)
{
std::cout << plug1_node1 << " <-> " << plug1_node2 << std::endl;
std::cout << plug2_node1 << " <-> " << plug2_node2 << std::endl;
std::cout << plug3_node1 << " <-> " << plug3_node2 << std::endl;
std::cout << std::endl;
}
bool check_connection(void)
{
std::unordered_set<char> plugs;
for(char c : {plug1_node1, plug1_node2, plug2_node1, plug2_node2, plug3_node1, plug3_node2}){
if(plugs.count(c)){
return false;
}
plugs.insert(c);
}
return true;
}
char exchange_char(char input_char)
{
if(input_char == plug1_node1){
return plug1_node2;
}
else if(input_char == plug1_node2){
return plug1_node1;
}
else if(input_char == plug2_node1){
return plug2_node2;
}
else if(input_char == plug2_node2){
return plug2_node1;
}
else if(input_char == plug3_node1){
return plug3_node2;
}
else if(input_char == plug3_node2){
return plug3_node1;
}
else{
return input_char;
}
}
};
かなり無茶苦茶な実装になってきました.メンバ変数のnodeが入れ替えたい文字,exchange関数が一対一の換字を行います.変換したい文字の配列を入れて,隣の文字同士を入れ替える雑仕様となっています.mapとか使えばもっといい感じの実装ができた気がします.重複検知のために集合を用いて調べるというのは我ながら名案でしたが,check関数がこの先使用されることはありません.
最後にエニグマクラスです.
class Enigma
{
private:
static const int MAX_ROTORS = 20;
int num_rotors;
Rotor rotors[MAX_ROTORS];
Reflector reflector;
PlugBoard plugboard;
int counters[MAX_ROTORS];
int rotor_outputs[MAX_ROTORS];
public:
Enigma(std::vector<std::string>& codes ,std::string& wiring, char node[])
: reflector(wiring), plugboard(node), num_rotors(codes.size())
{
for(int i=0;i<num_rotors;i++){
rotors[i] = Rotor(codes[i]);
counters[i] = 0;
}
}
void display_rotors()
{
for(int i=0; i<num_rotors; i++){
std::cout << "Rotor " << i+1 << std::endl;
rotors[i].display_scrambler();
}
}
void display_setting()
{
reflector.display_code();
plugboard.display_connection();
}
char translate_char(char plain_character, bool display_process = false)
{
char exchange_character = plugboard.exchange_char(plain_character);
if(display_process){
std::cout << plain_character << " -> " << exchange_character;
}
int input = exchange_character - '!';
char encrypted_character;
rotors[0].rotate();
counters[0] ++;
for(int i=0;i<num_rotors-1;i++){
if(counters[i] >= rotors[i].get_scrambler().length()){
counters[i] = 0;
rotors[i+1].rotate();
counters[i+1] ++;
}
}
rotor_outputs[0] = rotors[0].forward_scrambling(input);
if(display_process){
std::cout << " -> " << rotor_outputs[0];
}
for(int i=1;i<num_rotors;i++){
rotor_outputs[i] = rotors[i].forward_scrambling(rotor_outputs[i-1]);
if(display_process){
std::cout << " -> " << rotor_outputs[i];
}
}
int reflector_output = reflector.reflect(rotor_outputs[num_rotors-1]);
rotor_outputs[num_rotors-1] = rotors[num_rotors-1].back_scrambling(reflector_output);
if(display_process){
std::cout << " -> " << rotor_outputs[num_rotors-1];
}
for(int i=num_rotors-2;i>-1;i--){
rotor_outputs[i] = rotors[i].back_scrambling(rotor_outputs[i+1]);
if(display_process){
std::cout << " -> " << rotor_outputs[i];
}
}
encrypted_character = rotor_outputs[0] + '!';
if(display_process){
std::cout << " -> " << encrypted_character << std::endl;
}
encrypted_character = plugboard.exchange_char(encrypted_character);
return encrypted_character;
}
std::string translate_text(std::string plain_text, bool display_process = false)
{
std::string encrypted_text;
for(char c : plain_text){
encrypted_text += translate_char(c, display_process);
}
return encrypted_text;
}
void set_rotor(void)
{
char point;
for(int i=0;i<num_rotors;i++){
std::cout << "Rotor " << i + 1 << ": ";
std::cin >> point;
rotors[i].set_start_point(point);
counters[i] = 0;
}
}
};
メンバ変数は上から,最大ローター数,ローター数,ローター,リフレクター,プラグボード,カウンター,ローターの出力となっています.ちなみにコンストラクタがなんか複雑な感じになっていますが,こいつは初期化リストといいます.参照型のメンバ変数はこれを使わないと初期化できないらしいです(1敗).メンバ関数はいくつかありますが肝心の変換を担当するtranslate関数を紹介します.まずプラグボードによる置き換えが行われます.そして1つ目のローターが回転,もし一回転したなら2つ目が回転,2つ目も一回転したなら3つ目が......というようにローターが回転します.その後,ローターを伝わって変換されていきリフレクターに到達.次はローターを逆に伝わっていきます.最後にもう一度プラグボードによる変換が行われて1文字分の換字が終了します.これを文字列のすべての文字で行って暗号化が完了することになります.set関数はすべてのローターの位置決めをするための関数です.
本来のエニグマからいろいろと機能を削ったり単純化したりしたので,意外と短く書けたんじゃないでしょうか.まあ,そのせいでエラーハンドリングとか操作性とかも犠牲になっていますが本当に勘弁してください.1つ注意点なんですが,ローターの配線は結構ごちゃごちゃにしないと配線がまっすぐになって同じ文字に変換され続けるという致命的な欠陥があります.
Input: AAAAAAAAAA
Output: //////////
本来はこんな感じ
Input: AAAAAAAAAA
Output: {R9WK63H`t
このバグの特定にかなりの時間を要しました(デバッグしてくれた人ありがとう).
ってことでローターの配線を作成するためのコードも書きました.C++からは逃げましたPythonです.許してください.
import random
ascii_chars = [chr(i) for i in range(33, 127)]
random.shuffle(ascii_chars)
randomized_str = ''.join(ascii_chars)
print(randomized_str)
適当に暗号化してみましょう.ローターは3枚で,初期位置は3枚とも" ! ".プラグボードはAとB,FとC,VとMの置換とします.ローターとリフレクターの配線は以下の通り.
?_ed%RQ6U/pxS9#qBNhOX>F;-&{EI1!.JZCtbMD4u*az:k73f|L($j"0+mHgryKn,sPwv8T5'^i<[}WlAV2\@~G])=o`cY
erqi:tP"|d+l<paWD$!bn0`@-[);>9ySf\Cg~]8zkN%?F3=R/}6uv7HwUjsx{.&IL_YhEoMc2'#4^(5BVOJT*mA1XKQZG,
2>)hG'|Ha]rk<z/x{[#0^OXuMnZLdJU_RYQ9wB7cVvNmE61~`,*&?=4P8%yCD;}ioKp"j:q3-ft+el$@!sTAFgIW(Sb5.\
QWERTYUIOPASDFGHJKLZXCVBNMPLOKMIJNUHBYGVTFCRDXESZWAQqwertyuioppoiuytrewq12345678900987654321==
暗号化するのは「Okayama_University」とかにしましょうか.
Input: Okayama_University
Output: EBl34,0E,+g>xpl=e<
「EBl34,0E,+g>xpl=e<」という文字列が出力されましたね.問題なのは復号できるかどうかです.初期設定が同じならこれを打ち込めば平文が返ってくるはずです.set関数を用いてローターの初期位置を" ! "に戻します.
Input: EBl34,0E,+g>xpl=e<
Output: Okayama_University
無事に復号できることが確認されました.
まあ適当に遊んでみてください. エニグマらしい例題を出して終了とします.こんなあからさまな暗号はあるのかって感じですけどね......初期設定はさっきテストで使ったのと同じです.ぜひ解読してみてください.
「eMy$zxT+x~4Cq,6{>We/@r4S;0g<R-i`8}/emoT0_~IDf-L.*wUm6koD^e0X1.jW#」