前回は、第一次大戦前までに利用されてきた暗号について紹介しました。中世までは、暗号作成者が平文の文字をひとつずつ手作業で暗号文字に変換していましたが、20世紀になって電気工学が発達すると、それを利用した暗号化処理ができるようになりました。いわゆる「暗号機」の誕生です。
第二次世界大戦時、ドイツの「エニグマ」をはじめ、日本の「パープル暗号機」やアメリカの「シガバ暗号機」など様々な暗号機が活躍しました。この暗号を解読する作業は、戦局を大きく左右するほどの重要な役割を果たしており、もし連合国による暗号解読が失敗していれば、戦争は大幅に長引いていただろうとも言われています。
この章では、ドイツが利用した「エニグマ」の仕組みと、連合国がどのようにそれを解読したのかを中心に紹介したいと思います。
前章で紹介した多アルファベット暗号は、手作業での暗号化・復号化が面倒であることから普及するのに時間がかかりました。この作業の負担を軽減するために、多アルファベット暗号の生みの親であるアルベルティが考案したのが暗号円盤と呼ばれる暗号機で、暗号機としては最も古いものになります。
暗号円盤の構造は非常に単純で、アルファベットが周囲に並んで記述された大小二つの円盤を重ねて、中心をピンで留めた形になっています。内側と外側にアルファベットが並んだ構造になるので、内側の円盤を任意の位置まで回転させれば、カエサル暗号に利用することができ、さらに、キーワードに従って位置を変化させることで、ヴィジュネル暗号にも利用することができます。
暗号円盤の構造は単純ですが、暗号化作業を効率よく行うことができる実用的な道具であり、実際にアメリカの南北戦争でも利用されていました。
1918年、ドイツの技術者であるアルトゥール・シェルビウス(Arthur Scherbius)は、親友のリヒャルト・リッター(E. Richard Ritter)と会社を設立し、その中で新しい暗号機を発明しました。この暗号機こそが、第二次大戦中にドイツ軍によって利用されていた「エニグマ(Enigma)」でした。
シェルビウスが目指したものは、今まで手作業で行われていた暗号化と復号化の処理を当時の最新技術によって効率よく行うことができ、さらに解読が非常に困難な暗号機を作成することでした。エニグマが完成すると、彼は、軍部とビジネス界に対してそれを売り込み始めました。強力で解読不可能な暗号機は大きな需要があるに違いないと考えていたわけですが、実際には、エニグマが高価だったこともあって売り込みに失敗、最初はほとんど売れなかったようです。
ドイツ軍がエニグマの購入に踏み切ったのは、第一次大戦時のドイツ軍の暗号がイギリスに傍受・解読されていたことに気づいてからでした。1925年、シェルビウスはエニグマの大量生産を開始し、その翌年、ドイツ軍はエニグマを暗号機として採用しています。
エニグマは、いくつかの部品から構成されています。入力した文字をスクランブルするためのローター(Rotor)、文字を入力するためのキーボード(Keyboard)、暗号化した文字をランプで示すためのランプボード(Lamp Board)、入力された電気信号をスクランブルしつつ逆順に送り返すためのリフレクタ(Reflector)、そしてキーボードからローターへの電気信号の流れをスクランブルするためのプラグボード(Plugboard)を組み合わせることで、複雑なスクランブル処理を行うことが可能になっています。
エニグマの中で、ローターは非常に重要な役目を担っています。ローターは、内部をワイヤで配線した円盤形の部品で、円盤の片側から入力された電気信号が配線を通ってもう一方より出力されるのですが、この配線がスクランブルされており、ローターを一つ使うことで、単アルファベット換字式暗号化処理ができるようになっています。さらに、ローターは一文字入力する度に一文字分ずつ(アルファベットが円盤に並んでいるのであれば26分の1)回転するようになっているため、同じ文字を入力したとしても、ローターに入力する箇所は毎回変化することになります。これで、ローターを使って多アルファベット換字式暗号化処理が実現できることになります。
しかし、これだけでは、ローターが一回転する度に同じパターンが繰り返されることになるため、ヴィジュネル暗号と同じ弱点を持つことになります。そこで、この弱点を補うため、エニグマではローターを三つ組み込んで、スクランブルの仕方をさらに複雑にしました。第一のローターが一回転すると、第二のローターが一文字分だけ回転し、さらに第二のローターが一回転すると第三のローターが一文字分だけ回転するようになっているため(車の走行距離計と同じ仕組みです)、ローター内の配線の組み合わせはアルファベット全てが円盤上に並んでいる場合
26 X 26 X 26 = 17,576通り
になります。さらに三つのローターの配置も変更が可能になっていて、例えば一番めと三番めのローターを入れ換えるようなこともできるため、配置の仕方は6通りになり、それを掛けると配線の組み合わせは105,456通りにもなります。
下図は、三つのローターを並べた状態を表した模式図です。実際には各ローター(下図では枠で表されています)は円盤形で、配線も三次元構造になっていますが、それらを簡略化して平面上で表してあります。ローターの下端に伸びた配線は上端とつながっていると考えてください。またここでは、全アルファベットではなく、AからFまでの六文字だけを持ったローターとしてあります。左側の状態では、例えばAを入力すると、三つのローターを通過した結果Dが出力されます。一文字を入力すると、一番右端にあるローターは一文字分回転し(図では下側にシフトしています)、配線構造が変化します。その結果(図の右側の状態)、同じAを入力しても結果はDではなくFになります。
以下にローター部のサンプル・プログラムを示します。今回はC++を使い、部品ごとにクラスを作成してからそれらを組み合わせてエニグマ装置を組み立てる形式を取ってみたいと思います。オブジェクト指向言語もJavaやC#をはじめとしてかなり普及しているので、もはやC言語にこだわる必要もないと判断しての変更になります。
/* swap Vectorの要素を交換する vector<T>& vec : 対象のVector unsigned int i1, i2 : 交換する要素のインデックス */ Template<class T> bool swap( vector<T>& vec, unsigned int i1, unsigned int i2 ) { if ( i1 >= vec.size() || i2 >= vec.size() ) return( false ); if ( i1 == i2 ) return( true ); T buff = vec[i1]; vec[i1] = vec[i2]; vec[i2] = buff; return( true ); } /* エニグマ用ローター */ class Rotor { vector<unsigned int> inShift; // 入力時のシフト量 vector<unsigned int> outShift; // 出力時のシフト量 unsigned int location; // ローターの位置 public: // コンストラクタ Rotor( unsigned int length, unsigned int randomizeCount = 1000 ); unsigned int size() const { return( inShift.size() ); } unsigned int encode( unsigned int i ) const; // 暗号化 unsigned int decode( unsigned int i ) const; // 復号化 bool rotate(); // ローターを回転させる void init() { location = 0; } // ローター位置の初期化 void init( unsigned int i ) { location = i % size(); } // ローター位置の初期化 unsigned int current() const { return( location ); } // ローターの現在位置 }; /* Rotor コンストラクタ unsigned int length : ローターの文字数 unsigned int randomizeCount : ローターの配列をスクランブルする回数 */ Rotor::Rotor( unsigned int length, unsigned int randomizeCount ) : location( 0 ) { for ( unsigned int i = 0 ; i < length ; i++ ) inShift.push_back( i ); outShift.resize( length ); for ( unsigned int i = 0 ; i < randomizeCount ; i++ ) { unsigned int i1 = (unsigned int)( (double)rand() * length / ( (double)RAND_MAX + 1 ) ); unsigned int i2 = (unsigned int)( (double)rand() * length / ( (double)RAND_MAX + 1 ) ); swap( inShift, i1, i2 ); } for ( unsigned int i = 0 ; i < length ; i++ ) { int value = inShift[i]; value -= i; if ( value < 0 ) value += length; inShift[i] = value; outShift[( i + value ) % length] = value; } } /* Rotor::encode 暗号化 unsigned int i : 入力位置 戻り値 : 出力位置(範囲外の場合は入力位置をそのまま返す) */ unsigned int Rotor::encode( unsigned int i ) const { if ( i >= size() ) return( i ); i += inShift[( i + location ) % size()]; i %= size(); return( i ); } /* Rotor::decode 復号化 unsigned int i : 入力位置 戻り値 : 出力位置(範囲外の場合は入力位置をそのまま返す) */ unsigned int Rotor::decode( unsigned int i ) const { if ( i >= size() ) return( i ); i += size() - outShift[( i + location ) % size()]; i %= size(); return( i ); } /* Rotor::rotate ローターを回転させる 戻り値 : 回転後に元に戻ったらtrueを返す */ bool Rotor::rotate() { location++; if ( location >= size() ) location = 0; return( location == 0 ); } |
ローターは、入力と出力とのずれ量(inShiftとoutShift)で表現しています。コンストラクタでは、引数として渡された文字数を元に0から始まる整数列を用意して、これをランダムにスクランブルします。その後、スクランブルした数列の各要素とその位置との差をシフト量としてinShiftに登録すると同時に、inShiftの位置に要素を加えた値をoutShift上の位置として(これが出力位置を意味することになります)、その位置に、inShiftに登録したシフト量と同じ値を登録します。
シフト量は必ず正の整数となるように、シフト量が負になった場合は数列のサイズを加えます。また、outShiftの位置を決めるとき、サイズを越えた場合は逆にサイズ分だけ引き算をします。説明だけではわかりにくいと思いますので、下図を参考にしてください。
入力値 in を暗号化する場合は、inShift[in]をinに加えることで処理することができます(この時、数列のサイズを越えた場合はサイズ分だけ引き算します)。逆に、反対側から入力したデータ out を復号する場合は、outShift[out]をoutから引くことで処理することになります(負数の場合はサイズを加算します)。要素をシフト量で表しているのは、ローター自体が回転して全体の要素自体がシフトしてしまうため、出力位置を固定されたインデックスで表現することができないためです。要素をシフト量で表しておくことで、数列全体がシフトしても出力位置を正しく特定することができます。
ローターを回転させるためには、数列の全要素をシフトすればいいわけですが、実際にシフト処理を行うと重くなるので、数列の先頭位置を変化させることで仮想的にシフトさせる形にします。そのためのパラメータがlocationになります。locationの初期値は0ですが、メンバ関数のinitを使うことで任意の位置に変更することができます。例えば、locationが2に初期化された場合、数列の3番めの要素が先頭になります。入力信号にlocation分だけ加算した値の位置にある数列の要素をシフト量として、前述したように、入力信号にinShift上の要素を加算することで暗号化を、入力信号からoutShift上の要素を減算することで復号化を、それぞれ行うことができます。実際の処理はencodeとdecodeで行っています。
ローターの回転はメンバ関数のrotateで行っています。単純にlocationの値に1を加算して、一周したら0に戻しているだけですが、一周した時は戻り値としてtrueを返すようにして、複数のローターを組み合わせた場合に備えてあります。
リフレクタは、ローターと同じような内部配線を持った円盤形の部品ですが、ローターのように回転することはなく、信号が入力されたのと同じ側から出力される点がローターと異なっています。信号をスクランブルしつつ反射させることからリフレクタ(反射器)という名称が付けられています。
下図は、ローターにリフレクタを組み込んだ時の模式図を示しています。例えば、Aから入力された信号はリフレクタで反射された後再びローターを通り、Fを出力することになります。
リフレクタは、暗号を複雑にすることに貢献しているわけではありませんが、この部品のおかげで、復号化処理を簡単に行うことができるようになっています。上図を見れば明らかなように、暗号化されたアルファベットを入力すると必ず平文アルファベットが出力されるようになっています。これは、暗号化したときと復号化したときとで、ローターの配線内容・並び方・位置が同じである限り保証されます。つまり、暗号化と復号化で同じ条件にローターがセットされていれば、暗号化された文字をそのまま入力すれば正しい平文が得られることになります。
以下にリフレクタ部のサンプル・プログラムを示します。
/* vector内の要素をスクランブルする vector<unsigned int>& vec : スクランブルするvector unsigned int length : vectorの処理後の要素数 unsigned int count : スクランブル回数 */ void scramble( vector<unsigned int>& vec, unsigned int length, unsigned int count ) { vector<unsigned int> numSeq; vec.clear(); for ( unsigned int i = 0 ; i < length ; i++ ) { numSeq.push_back( i ); vec.push_back( i ); } for ( unsigned int i = 0 ; i < count ; i++ ) { if ( numSeq.size() < 2 ) break; unsigned int i1, i2; do { i1 = (unsigned int)( (double)rand() * numSeq.size() / ( (double)RAND_MAX + 1 ) ); i2 = (unsigned int)( (double)rand() * numSeq.size() / ( (double)RAND_MAX + 1 ) ); } while ( i1 == i2 ); vec[numSeq[i1]] = numSeq[i2]; vec[numSeq[i2]] = numSeq[i1]; numSeq.erase( numSeq.begin() + ( ( i1 > i2 ) ? i1 : i2 ) ); numSeq.erase( numSeq.begin() + ( ( i1 > i2 ) ? i2 : i1 ) ); } } /* エニグマ用リフレクタ */ class Reflector { vector<unsigned int> reflector; public: // コンストラクタ Reflector( unsigned int length ); unsigned int size() const { return( reflector.size() ); } unsigned int operator[]( unsigned int i ) const; }; /* Reflector コンストラクタ unsigned int length : リフレクタの文字数 */ Reflector::Reflector( unsigned int length ) { scramble( reflector, length, length / 2 ); } /* Reflector::operator[] 入力データに対する応答値を返す unsigned int i : 入力データ 戻り値 : 応答値(範囲外の場合は入力データをそのまま返す) */ unsigned int Reflector::operator[]( unsigned int i ) const { if ( i >= size() ) return( i ); return( reflector[i] ); } |
ローター部に比べると、往復それぞれの入出力や回転を考慮する必要がないためシンプルな作りになっています。コンストラクタでは、ローター同様にサイズ分の数列を用意して、ヘルパ関数のscrambleで二つずつ要素を抽出しながらペアを作り、それぞれの要素を交換する処理を行っています。入力信号に対する反射時の位置は、添字演算子を多重定義することでobj[index]の形で取得することができます。
三つのローターを使って暗号化を行った場合、その初期設定は、前にも述べたように105,456通りの組み合わせがあります。この場合、一人の解読者が一つの組み合わせで解読を試みるのに要する時間が仮に一分だったとすると、全てを試すのに約73日かかります。エニグマが敵の手に渡った場合、10名の人員を投入して解読作業が行われれば、最長でも約一週間で解読ができてしまうことになります。そこで、さらに安全性を高める目的で、エニグマには、キーボードと一番目のローターとの間にプラグボードという部品が挿入されています。
プラグボードは、キーボードから入力された電気信号をローターに渡す前に、別の信号と交換する役割を持っています。例えば、AとCの信号を交換して、Aが入力された場合はCの信号が通る経路を、逆にCが入力された場合はAの信号が通る経路を伝わるようにすることができます。
下図に、プラグボードの働きを模式図で示します。
初期のエニグマ機は、六つのケーブルを使って六組のペアを選択することができました。26個のアルファベットから六組のペアを抽出する時の組み合わせは、26個のアルファベットから12個のアルファベットを抽出する組み合わせに、12個のアルファベットを六組のペアにするときの組み合わせを掛けることで求めることができます。
26個のアルファベットから12個のアルファベットを抽出する組み合わせは
26C12 = 26! / ( 12! X ( 26 - 12 )! ) = 9657700 通り
12個のアルファベットを6組のペアにする組み合わせは
3 X 5 X 7 X 9 X 11 = 10395 通り (*)
なので、実に100,391,791,500通りもの組み合わせがあります。
以下にプラグボード部のサンプル・プログラムを示します。
/* エニグマ用プラグボード */ class PlugBoard { vector<unsigned int> plug; public: /* PlugBoard コンストラクタ unsigned int length : 入力できる文字の数 unsigned int exchangeCount : 文字の交換が可能な数 */ PlugBoard( unsigned int length, unsigned int exchangeCount = 6 ) { scramble( plug, length, exchangeCount ); } // プラグを交換する bool exchange( unsigned int i1, unsigned int i2 ); unsigned int size() const { return( plug.size() ); } /* encode/decode 暗号化と復号化(処理は全く同じ) unsigned int i : 入力インデックス 戻り値 : 暗号値(範囲外の場合は入力インデックスをそのまま返す) */ unsigned int encode( unsigned int i ) const { return( ( i >= plug.size() ) ? i : plug[i] ); } unsigned int decode( unsigned int i ) const { return( encode( i ) ); } }; /* PlugBoard::exchange プラグを交換する unsigned int i1, i2 : 交換する要素 */ bool PlugBoard::exchange( unsigned int i1, unsigned int i2 ) { if ( i1 >= size() || i2 >= size() ) return( false ); // 既に交換されたプラグならば元に戻す if ( plug[i1] != i1 ) { plug[plug[i1]] = plug[i1]; plug[i1] = i1; } if ( plug[i2] != i2 ) { plug[plug[i2]] = plug[i2]; plug[i2] = i2; } if ( i1 == i2 ) return( true ); plug[i1] = i2; plug[i2] = i1; return( true ); } |
コンストラクタの第二引数で、ランダムにプラグを交換する回数を指定することができます。また、後でプラグを交換するために、メンバ関数のexchangeが用意されています。
exchangeではプラグの交換ができますが、すでにプラグが交換済みだった場合は一度元に戻してから改めて交換処理を行うようにしてあります。これは、実機でプラグ交換をする場合の操作に合わせるためです。
(*) 4個のアルファベットabcdを2組のペアにする組み合わせは
ab-cd, ac-bd, ad-bc
の3通り。これに2個のアルファベットefを追加したとき
ef-ab-cd, ef-ac-bd, ef-ad-bc → efペアを追加
ea-fb-cd, ea-fc-bd, ea-fd-bc → aとfを交換
eb-af-cd, eb-ac-fd, eb-ad-fc → bとfを交換
ec-ab-fd, ec-af-bd, ec-ad-bf → cとfを交換
ed-ab-cf, ed-ac-bf, ed-af-bc → dとfを交換
で、5 X 3 = 15通りになる。この操作を繰り返すと、2m個のアルファベットをm組のペアにする組み合わせは
(2m - 1) X (2m - 3) X ... X 5 X 3 通り
となる。
残りの部品は、キーボードとランプボードになります。これらはまとめてEnigmaIFクラスにまとめます。
/* エニグマ用インターフェース(Keyboard,Lumpboard) */ class EnigmaIF { string key; // キーボード上の文字コードを表す文字列 public: // コンストラクタ EnigmaIF( unsigned char start = 1, unsigned char end = UCHAR_MAX ); // 入力文字のインデックスを返す(見つからなかったら最後の要素の次のインデックスを返す) unsigned int keyIn( unsigned char c ) const; // 指定インデックスのキーを返す(範囲外の場合は空白文字を返す) unsigned char keyOut( unsigned int i ) const { return( ( i >= size() ) ? ' ' : key[i] ); } unsigned int size() const { return( key.length() ); } // キーボード上の文字数を返す }; /* EnigmaIF コンストラクタ unsigned char start : キーボードの開始文字 unsigned char end : キーボードの終了文字 指定範囲startとendの中で、印字不可能なコードは除外する */ EnigmaIF::EnigmaIF( unsigned char start, unsigned char end ) { for ( unsigned int c = start ; c <= end ; c++ ) if ( isprint( c ) ) key += (unsigned char)c; } /* EnigmaIF::keyIn 入力キーのインデックスを返す unsigned char c : 入力キー 戻り値 : 入力キーのインデックス (見つからなかったら最後の要素の次のインデックスを返す) */ unsigned int EnigmaIF::keyIn( unsigned char c ) const { unsigned int i = key.find( c ); if ( i == string::npos ) i = size(); return( i ); } |
最後にこれらを組み合わせて、エニグマ全体を完成させます。
/* getSeedOfRand 乱数の種を求める */ void getSeedOfRand() { struct timeval tv; struct timezone tz; gettimeofday( &tv, &tz ); srand( tv.tv_sec ); } /* エニグマ */ class Enigma { EnigmaIF* enigmaIF; vector<Rotor*> vecRotor; Reflector* reflector; PlugBoard* plugBoard; public: // Enigma コンストラクタ Enigma( unsigned char start = 1, unsigned char end = UCHAR_MAX, unsigned int rotorCount = 3, unsigned int exchangeCount = 6 ); Enigma( const Enigma& enigma ); // Enigma デストラクタ ~Enigma(); string convert( const string& inData ); // 文字列を暗号化する // 指定した文字の番号を返す unsigned int keyIn( unsigned char c ) const { return( enigmaIF->keyIn( c ) ); } // 指定した場所のキーを返す unsigned char keyOut( unsigned int i ) const { return( enigmaIF->keyOut( i ) ); } // Enigmaで処理できる文字数を返す unsigned int ioSize() const { return( enigmaIF->size() ); } // ローターの数を返す unsigned int rotorCount() const { return( vecRotor.size() ); } // 代入演算子の多重定義 Enigma& operator=( const Enigma& enigma ); // ローターの初期化 void init(); void init( const string& key ); void init( const vector<unsigned int>& key ); bool rotate(); // ローターの回転 bool negRotate(); // ローターの逆回転 string current() const; // 各ローターの位置を返す bool exchangeRotor( unsigned int i1, unsigned int i2 ); // ローターの交換 // プラグボードのプラグ交換 bool exchangePlug( unsigned int i1, unsigned int i2 ) { return( plugBoard->exchange( i1, i2 ) ); } // プラグボードのリセット void resetPlug() { plugBoard->reset(); } }; /* Enigma コンストラクタ unsigned char start : キーボードの開始文字 unsigned char end : キーボードの終了文字 unsigned int rotorCount : ローターの数 unsigned int exchangeCount : プラグボードの交換回数 */ Enigma::Enigma( unsigned char start, unsigned char end, unsigned int rotorCount, unsigned int exchangeCount ) { getSeedOfRand(); enigmaIF = new EnigmaIF( start, end ); plugBoard = new PlugBoard( ioSize(), exchangeCount ); for ( unsigned int i = 0 ; i < rotorCount ; i++ ) vecRotor.push_back( new Rotor( ioSize() ) ); reflector = new Reflector( ioSize() ); } /* Enigma コピー・コンストラクタ const Enigma& enigma : コピー元 Enigmaオブジェクト */ Enigma::Enigma( const Enigma& enigma ) { enigmaIF = new EnigmaIF( *( enigma.enigmaIF ) ); plugBoard = new PlugBoard( *( enigma.plugBoard ) ); for ( unsigned int i = 0 ; i < enigma.rotorCount() ; i++ ) vecRotor.push_back( new Rotor( *( ( enigma.vecRotor )[i] ) ) ); reflector = new Reflector( *( enigma.reflector ) ); } /* Enigma デストラクタ */ Enigma::~Enigma() { delete enigmaIF; delete plugBoard; for ( unsigned int i = 0 ; i < rotorCount() ; i++ ) delete vecRotor[i]; delete reflector; } /* Enigma::convert Enigmaで文字列を暗号化する const string& inData : 入力文字列 戻り値 : 暗号化した文字列 */ string Enigma::convert( const string& inData ) { string s; for ( unsigned int i = 0 ; i < inData.length() ; i++ ) { // キーボードからの入力 unsigned int index = keyIn( inData[i] ); if ( index >= ioSize() ) continue; // 範囲外なら無視する // プラグボードでの変換 index = plugBoard->encode( index ); // ローターでの変換 for ( unsigned int i = 0 ; i < rotorCount() ; i++ ) { Rotor* p = vecRotor[i]; index = p->encode( index ); } // リフレクターでの変換 index = ( *reflector )[index]; // ローターでの逆順変換 for ( unsigned int i = rotorCount() ; i > 0 ; i-- ) { Rotor* p = vecRotor[i - 1]; index = p->decode( index ); } // プラグボードでの逆変換 index = plugBoard->decode( index ); // 文字の出力 s += keyOut( index ); rotate(); // ローターを回転 } return( s ); } /* Enigma::operator= 代入演算子の多重定義 const Enigma& enigma : コピー元 Enigmaオブジェクト 戻り値 : 自分自身 */ Enigma& Enigma::operator=( const Enigma& enigma ) { if ( this == &enigma ) return( *this ); //自己代入 delete enigmaIF; delete plugBoard; for ( unsigned int i = 0 ; i < rotorCount() ; i++ ) delete vecRotor[i]; vecRotor.clear(); delete reflector; enigmaIF = new EnigmaIF( *( enigma.enigmaIF ) ); plugBoard = new PlugBoard( *( enigma.plugBoard ) ); for ( unsigned int i = 0 ; i < enigma.rotorCount() ; i++ ) vecRotor.push_back( new Rotor( *( ( enigma.vecRotor )[i] ) ) ); reflector = new Reflector( *( enigma.reflector ) ); return( *this ); } /* Enigma::init ローターの初期化 */ void Enigma::init() { for ( unsigned int i = 0 ; i < rotorCount() ; i++ ) ( vecRotor[i] )->init(); } /* Enigma::init ローターの初期化 const string& key : 初期化時に使用するキーコード */ void Enigma::init( const string& key ) { for ( unsigned int i = 0 ; i < rotorCount() ; i++ ) { unsigned int index = 0; if ( i < key.length() ) { unsigned int buff = keyIn( key[i] ); if ( buff < ioSize() ) index = buff; } ( vecRotor[i] )->init( index ); } } /* Enigma::rotate ローターの回転 最後尾のローターから回転し、0に戻ったらその前の ローターを回転させる 戻り値 : 最も先頭にあるローターが0に戻ったらTrueを返す (つまり全て0の位置になったらTrue) */ bool Enigma::rotate() { for ( unsigned int i = rotorCount() ; i > 0 ; i-- ) if ( ! ( vecRotor[i - 1] )->rotate() ) return( false ); return( true ); } /* Enigma::negRotate ローターの逆回転 最後尾のローターから逆回転し、0からの繰り下がりがあったらその前の ローターを逆回転させる 戻り値 : 最も先頭にあるローターで繰り下がりがあったらTrueを返す (つまり全て0の位置の状態で逆回転をしたらTrue) */ bool Enigma::negRotate() { for ( unsigned int i = rotorCount() ; i > 0 ; i-- ) if ( ! ( vecRotor[i - 1] )->negRotate() ) return( false ); return( true ); } /* Enigma::current 各ローターの位置を返す */ string Enigma::current() const { string s; for ( unsigned int i = 0 ; i < rotorCount() ; i++ ) { unsigned int index = ( vecRotor[i] )->current(); unsigned char c = keyOut( index ); s += c; } return( s ); } /* Enigma::exchangeRotor ローターの交換 unsigned int i1, i2 : 交換するローターの番号 */ bool Enigma::exchangeRotor( unsigned int i1, unsigned int i2 ) { if ( i1 >= rotorCount() || i2 >= rotorCount() ) return( false ); Rotor* rotor = vecRotor[i1]; vecRotor[i1] = vecRotor[i2]; vecRotor[i2] = rotor; return( true ); } |
今回作成したサンプルは、ローターやリフレクタの配線構造は乱数で決まるため、同じ平文を処理しても動作させる度に暗号文は変化します。また、一度構築したら、プラグボードとローターの配置以外は設定を変更することはできません(プラグボードの配線はメンバ関数のexchangePlugで、ローターの配置はexchangeRotorで変換することができます)。
ローターの初期化はメンバ関数initで行うことができます。引数を指定しない場合、全ローターの位置を0にします。また、文字列を指定した場合、文字列中の各文字に対応する位置をセットします。この時、文字列の先頭は一番目のローター、次の文字は二番目のローターという具合に対応しています。指定した文字が登録されていなかったり、文字列長が足りず割り当てができないローターは0で初期化されます。
暗号化・復号化の処理はconvertが行っています。ここでの処理は各部品のメンバ関数を順番に呼び出しているだけで、以下のような流れになります。
メンバ関数rotateは、まず一番目のローターを一目盛り分回転させてから一回転して元に戻ったかをチェックして、もし一回転していたら次のローターも同じように一目盛り分回転させるという操作を繰り返します。ローターが一回転したかどうかの判定には、Rotorオブジェクトのメンバ関数rotateが返す戻り値が利用されます。
エニグマによって、連合国側の暗号解読は不可能となり、やがて解読の試みをあきらめてしまいます。これでドイツ軍は安全な通信手段を手に入れたかに見えましたが、解読できるという希望をまだ捨てず、それを試みようとしていた国が一つだけありました。それが、第一次対戦後に国土が回復したばかりのポーランドです。
ポーランドは当時、東側のロシア(当時のソ連)と西側のドイツに挟まれて、再び消滅してしまう危険性を抱えていました。そのような中で、両国の動きを監視するためには暗号解読が必須のアイテムとなり、1919年、暗号解読局「ビュロ・シフルフ(Sekcja Szyfrów)」を設立します。ビュロ・シフルフは、ポーランド・ソビエト戦争(1919-1921)においてもソ連の暗号通信を大量に解読するなど、かなりの成果を上げていました。ところが1926年になって、ドイツ軍がエニグマを利用するようになると暗号の解読は不可能になり、ドイツの動きを監視するための情報がほとんど入手できなくなりました。
当時ドイツの暗号局職員であったハンス=ティロ・シュミット(Hans-Thilo Schmidt)は、スパイとしてフランス情報部と接触し、エニグマに関する情報を売り渡しました。フランスはポーランドと軍事協定を結んでいたため、ポーランドはフランスからこの情報を入手し、解読を試みることになります。その中で最も中心的な役割を果たしたのが、数学者のマリアン・レイェフスキ(Marian Rejewski)でした。
当時のドイツ軍は、エニグマの設定を毎日変更するようにしていました。この設定は「日鍵」と呼ばれ、一ヶ月分の日鍵をコードブックに記載してオペレータに渡すようにしていました。日鍵の内容としては、以下のような項目が記載されることになります。
この設定の組み合わせ総数は、
ローターの位置の組み合わせ X ローターの配置の組み合わせ X プラグボードの設定の組み合わせ
となり、
17,576 X 6 X 100,391,791,500 = 10,586,916,764,424,000
という莫大な数になります。
さらに、日鍵によって暗号化するメッセージを極力少なくし、日鍵を推理するための材料を少なくするするために、メッセージを暗号化する時は、「メッセージ鍵」という日鍵とは異なる鍵を用い、日鍵はメッセージ鍵を送信するときだけに使うようにしました。このメッセージ鍵は、ローターの位置だけを指定しているため、プラグボードの設定とローターの配置は固定ですが、ローターの位置はメッセージを送信する度に異なることになります。ローターの位置を例えばP-I-Oとした場合、送信者は、日鍵を使ってこの位置を二回入力します(P-I-O-P-I-O)。この内容は暗号化され、例えば(C-B-E-R-V-M)と変換・送信されます。この内容を受信した者が、日鍵の設定内容で初期化したエニグマにこの文字を入力すると、メッセージ鍵が二回繰り返された文字列が得られるわけです。
メッセージ鍵を二回送信したのは、入力や無線送信時のミスを避けるための策でした。しかし、前章でも紹介した、アメリカのVENONA作戦における旧ソ連のワンタイムパッド暗号解読の例にあるように、「繰り返し」というものは暗号解読への突破口となり得ます。レイェフスキはこの原則に則って、メッセージ鍵を攻略のための手掛かりとして利用することを考えました。
メッセージ鍵は必ず、同じ日鍵を使って暗号化されます。ドイツ軍の暗号を傍受して、四つのメッセージを受信したとき、その先頭六文字はメッセージ鍵が暗号化された結果になります。
一番目のメッセージ鍵 | J J Z K X W |
---|---|
二番目のメッセージ鍵 | W A F Z A U |
三番目のメッセージ鍵 | H O X U M D |
四番目のメッセージ鍵 | S W B G C I |
上表は、四つのメッセージ鍵が暗号化された結果を示しています。この内容からわかるのは、一番目の文字と四番目の文字のどちらもメッセージ鍵の最初の文字を暗号化しているため、それらは同じ文字を暗号化した結果であり、その関係は二番目と五番目、三番目と六番目にも当てはまるということです。例えば、一番目のメッセージ鍵のJとKはそれぞれ同じ文字を暗号化したものであり、結果が異なるのは、Kに暗号化されるまでにローターが三目盛り分回転したからになります。この、一番目と四番目の文字との関連を表にすると、以下のようになります。
一番目の文字 | 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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
四番目の文字 | U | K | G | Z |
一日中に十分な数のメッセージを入手することができれば、上に示した表を完成させることができます。その結果が次のようになったと仮定します。
一番目の文字 | 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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
四番目の文字 | D | N | T | X | P | H | L | U | W | K | B | V | J | S | M | R | C | Y | G | E | Q | O | Z | A | F | I |
二番目と五番目、三番目と六番目の文字についても同様の作業によって表を作成することができますが、これだけでは日鍵はおろか、メッセージ鍵が何であるかを突き止めることもできません。ただ、他の日鍵が使用されれば表の内容は全く違ったものになるため、これが、日鍵のパターンを知るための手掛かりになり得るのではないかと推察することができます。ここでレイェフスキは、文字の連鎖のパターンに着目しました。例えば、一番目の文字のAは四番目の文字ではDになり、一番目の文字のDは四番目の文字ではXになります。このように連鎖を作成すると、次のようなパターンを作ることができます。
A→D→X→A (3)
B→N→S→G→L→V→O→M→J→K→B (10)
C→T→E→P→R→Y→F→H→U→Q→C (10)
I→W→Z→I (3)
連鎖の末尾にはその文字数が記述されています。この文字数は、日鍵が変わる度に変化し、短い連鎖がたくさんあるときがあれば、長い連鎖が少数あるだけの時もあるでしょう。
連鎖のパターンは相変わらず、一京通り以上という途方もない数の組み合わせが存在します。しかし、その文字数に着目した場合、それはローターの設定だけに依存して、プラグボードの影響は受けません。例えば、上記の連鎖においてプラグボードがAとFで交換されていたと仮定して、これを元に戻してからVとWを交換した場合、連鎖は以下のように変化します。
F→D→X→F (3)
B→N→S→G→L→W→O→M→J→K→B (10)
C→T→E→P→R→Y→A→H→U→Q→C (10)
I→V→Z→I (3)
プラグボードは、単純な単アルファベット換字を行っているに過ぎず、ローターとの入出力前に二つのアルファベットを交換しているだけなので、連鎖の内容は変わっても(単にアルファベットが入れ替わっただけで)連鎖の文字数には影響しません。よって、文字数だけに着目したとき、その組み合わせは、ローターの位置の組み合わせ X ローターの配置の組み合わせ = 105456通りにまで少なくすることができます。
レイェフスキは、シュミットのスパイ行為のおかげで手に入れることができたエニグマのレプリカを使い、ローターの設定ごとの連鎖の内容を調べて一覧表にする作業を一年以上かけて行い、完成した一覧表を利用してエニグマの解読を行いました。まず、傍受した暗号メッセージの先頭六文字を調べ、上で説明した方法で連鎖を作成し、その文字数を割り出します。次に、連鎖の文字数の組み合わせが一致するローターの設定を一覧表から見つければ、その日の日鍵の設定内容のうち、ローターの設定については得ることができるわけです。この一覧表はCard catalogと呼ばれ、1934年に完成しました。
ローターの設定がわかれば残るはプラグボードの設定のみとなりますが、この作業は次のように行います。
まず、プラグボードの設定を全て初期化(文字の交換を行わない)してから、傍受した暗号文を入力すると、出力された結果の中に意味のわかる文字列が表れることがあります。例えば、
THWCODWBOOK
という文字列があった場合、これは"The Code Book"が正しいものと推定することができます。この推定が正しければ、プラグボードによってEとWが交換されていることになります。また逆に、その他の文字 BCDHKOTは他の文字とは交換されていないこともわかります。これを繰り返すことで、プラグボードによって交換されたアルファベットを特定することが可能になるわけです。
レイェフスキはさらに、エニグマを改造して、ローターの設定を自動的に割り出す装置を製作しました。ローターの配置は六通りあるため、それぞれの配置に対して一台ずつ、計六台のエニグマを組み合わせ、それぞれの配置に対して並行して処理することができるようになっており、二時間程度で日鍵を突き止めることができました。1938年、ドイツがメッセージ鍵の送信方法を変更したためにCard catalogが利用できなくなったとき、一覧表を作成し直す代わりにこの装置を使って日鍵を突き止めることができました。この装置はボンブ(Bomb)と呼ばれています。
しかし、やがてエニグマのローターが五つに増え、その中から三つを選択する方法を取るようになった上、プラグボードのケーブル本数が六本から十本に増えたことにより、ビュロ・シフルフによるエニグマの解読もリソース不足の問題によりできなくなり、ポーランドは連合国に今までの成果をそのまま提供するという決断を下すことになります。エニグマのレプリカとBombに関する資料は、ドイツ軍によるポーランド侵攻の二週間前にロンドンへ送られ、ブレッチレー・パーク(Bletchley Park)にある英国政府暗号解読班へと解読作業が引き継がれました。ビュロ・シフルフに比べてリソースのより豊富な英国政府暗号解読班は、組合せの増えたエニグマの暗号にも対抗することができました。
ブレッチレー・パークの暗号解読班は、より素早く解読できるようにいくつかの工夫を行いました。例えば、Cillyと呼ばれる予測可能なメッセージ鍵を最初に試すことで、ときには日鍵を突き止める前に暗号を解読することも可能でした。これは、オペレータの手抜きという人的要因によるエニグマの弱点となります。
また、予測を困難にしようとして逆に不利になるような人的ミスもいくつかありました。例えば、三つのローターの配置を前日と重ならないようにするという取り決め(今日のローターの配置が1-3-5ならば、それぞれの配置が重なるようなパターン(1-5-2や2-3-4など)は翌日の組み合わせとして利用されない)は、ローターの配置の組み合わせを少なくする結果となるため却って暗号解読者に有利となります。同様に、プラグボードにて、隣り合う文字同士を交換しないという取り決めも、組み合わせを少なくする結果となってしまいます。
このように、エニグマ攻略の舞台がブレッチレー・パークへ移ってからも解読のための様々な方法が模索されつづけましたが、その中で暗号解読に最も貢献した人物として、アラン・チューリング(Alan Mathison Turing)の名を挙げることができます。彼は論文の中で、チューリング・マシン(Turing Machine)と呼ばれる仮想的な機械を考案したことでも有名な人物です。
エニグマ攻略は、日鍵によってメッセージ鍵を二度暗号化するという運用上の弱点を最大限に利用した方法により達成されたものでした。しかし、ドイツ軍がその弱点に気づいてしまえば反復を止めてしまうことになり、もはや現状の暗号解読手法は利用できなくなってしまいます。よって、そうなる前に他の手段を見つけ出すことがチューリングの課題となりました。
チューリングはまず、解読済みのメッセージに着目しました。これらを調査した結果、送信された時刻と場所によってメッセージがある一定の形式を持っていることに気づきます。チューリングは、送信時刻とその場所を手がかりに部分的ならば暗号を解読できると確信しました。例えば、毎日午前六時五分過ぎに送信されるメッセージは気象情報を表していることがすでに判明しているとします。するとこれらのメッセージには、「天候」を意味するドイツ語のwetterがほぼ間違いなく含まれていることになります。さらにチューリングは、その単語がメッセージ内のどこにあるのかまでをほぼ確定することができました。このように、平文と暗号文の一部があるフレーズで結び付いたとき、それをクリブ(crib)と言います。チューリングは、暗号解読のための鍵としてこのクリブを利用することを検討しました。
クリブの例として、WETTERにあたる部分がRQJWTEと暗号化されていたとします。この場合、エニグマ機の設定を変更しながらWETTERと入力する操作を続けていき、RTJWQEと変換されれば正しい組み合わせが見つかったことになります。
しかし、このままでは設定の組み合わせは約一垓五千九百京通り(垓は京の一万倍)となるため、チューリングは、レイェフスキと同じように、ローターとプラグボードの組み合わせを分離させる方法を模索しました。そこで着目したのがクリブによるループ構造です。
エニグマの設定 | s | s + 1 | s + 2 | s + 3 | s + 4 | s + 5 |
---|---|---|---|---|---|---|
平文 | W | E | T | T | E | R |
暗号文 | R | Q | J | W | T | E |
前に示した例では、エニグマの初期設定がsだったとき、WがRに暗号化されます。そしてRは、エニグマの設定がs + 5(初期設定sから五回入力を行った後の設定)の時にEに変換されます。このように順にたどっていくと、次のようなループを構成することができます。
エニグマの設定 | 平文→暗号文 |
---|---|
s | W → R |
s + 5 | R → E |
s + 4 | E → T |
s + 3 | T → W |
注) s + 1でEがQに、s + 2でTがJに暗号化されていますが、QとJはループに含まれないため無視します。
ここで、上記のようなループ構成を持ったクリブを使ってエニグマの設定をチェックするのに、一台ではなく、ループに含まれる変換処理の数(上記の例では四台)分のエニグマ機を同時に使うことを想定してみます。四台のエニグマ機はそれぞれ同じ設定にしておきますが、第二のエニグマ機は第一のエニグマ機よりも五つ分(s+5)、第三のエニグマ機は第一のエニグマ機よりも四つ分(s+4)、そして第四のエニグマ機は第一のエニグマ機よりも三つ分(s+3)だけローターの位置を進めておきます。この状態で、第一のエニグマ機はWをRに、第二のエニグマ機はRをEに、第三のエニグマ機はEをTに、第四のエニグマ機はTをWにそれぞれ暗号化することができれば、その時のエニグマの設定が求めている解になります。
さらに、それぞれのエニグマ機の入出力を順につなぐことを想定してみます。第一のエニグマ機の出力文字であるRを第二のエニグマ機の入力文字Rと、第二のエニグマ機の出力文字であるEを第三のエニグマ機の入力文字Eと、といった具合に接続を行うと、正しい設定になったところで回路は閉じることになるため、例えばこの回路に電球を取り付けておけば、正しい設定になったときに電球に灯りが点ることになります。
下図は、四台のエニグマの入出力を接続した時の回路図になります。
上図の中で、L1からL4は、各入力文字がプラグボードで変換された時の未知の文字を示しています。一台目のエニグマでは、入力されたWがプラグボードでL1に変換され、さらにローター三台とリフレクタによってL2になってから、プラグボードで再び変換されてRを出力しています。この結果は、二台目のエニグマで同じくRとして入力されますが、ここで、一台目のエニグマと同様に、プラグボードによってL2に変換されていることに注目してください。プラグボードの設定はどのエニグマも同じなので、上記のようなループ構成にすることによりプラグボードによる変換処理は互いに打ち消し合うことになります。つまり、ループ構成によって、プラグボードの設定は全て無視することができることを意味します。
残念ながら、プラグボードによって変換された未知の文字を知ることはできないため、以下のような操作によって解読を試みることになります。
しかし、La以外(LbからLxまで)はあらかじめ全て接続しておいても問題はないため、操作方法は次のように簡略化できます。
ローターの配線の組み合わせは17576通り。五つのローターから三つのローターを選択して並べる時の組み合わせは60通りになり、ローターの配置と位置をチェックする操作は
17576 X 60 = 1,054,560通り
となります。仮に、上記操作を自動化したとして、ひとつの組み合わせを一秒で処理することができた場合、全ての処理を完了するのに約12日必要となります。また、ローターの配置の組み合わせ数分だけ回路を用意することができれば、操作すべき回数はさらに減少して、五時間以内に処理することもできるようになります。
ローターの設定が判明すれば、あとはプラグボードの設定を見つけ出すだけですが、これは前にも述べた通り、プラグボードの設定を初期化した上で暗号文を平文化して、交換されたアルファベットを意味のわかる単語から類推することで実現できます。
ローターの設定を解読するためにチューリングが考案した装置はレイェフスキの製作したそれに類似しているためやはりボンブと呼ばれ、1940年3月14日に試作品第一号(VICTORY)がブレッチレーに到着しました。ところが、試運転の結果は予想をはるかに下回ったため、性能改善のための改造が必要となります。そうしている間にドイツ軍はとうとう、メッセージ鍵の反復送信をしないよう送信プロトコルを変更し、新しいボンブが到着するまでの間は解読できる暗号の数が激減、プロトコル変更が行われた1940年5月10日から、新たなボンブ(AGNES DEI ; 神の子羊)が到着した1940年8月8日までの約三ヶ月間、チューリングをはじめとするブレッチレー・パークのメンバは居ても立ってもいられない状況だったのではないでしょうか。しかし、新しいボンブは期待どおりの性能を示し、エニグマ解読における重要な役割を果たすことになります。
以下は、ボンブによる解読のシミュレーションを行ったサンプル・プログラムになります。
まずは、クリブからループ構成を得るためのプログラム部分を紹介します。
// Crib クリブの構成要素 struct Crib { unsigned int rotorLoc; // 初期状態を'0'としたときのローターの相対位置(クリブ上の文字の位置と同じ) unsigned char crypto; // 暗号化した結果の文字列 // コンストラクタ Crib( unsigned int _rotorLoc = 0, unsigned char _crypto = ' ' ) : rotorLoc( _rotorLoc ), crypto( _crypto ) {} // '=='の多重定義 bool operator==( const Crib& crib ) const { return( crypto == crib.crypto ); } }; typedef vector<unsigned int> VecUInt; typedef map<unsigned char, Crib> MapCrib; /* makeLoop クリブの構成要素からループを作成する const MapCrib& mapCrib : クリブの構成要素を持ったMap unsigned int startIdx : ループ読み込みの開始位置 戻り値 : 作成したループの要素(Vector) */ VecUInt makeLoop( const MapCrib& mapCrib, unsigned int startIdx ) { vector<Crib> vecCrib; VecUInt vecInt; // ループ読み込みの開始位置がクリブの範囲外なら空のベクタを返す if ( startIdx >= mapCrib.size() ) return( vecInt ); MapCrib::const_iterator mapIt = mapCrib.begin(); // クリブ上の対象要素 for ( unsigned int i = 0 ; i < startIdx ; i++ ) ++mapIt; int loopStart; do { // 対象要素がすでにベクタ上にあれば、ループが構成されたことになる vector<Crib>::iterator vecIt = find( vecCrib.begin(), vecCrib.end(), mapIt->second ); loopStart = ( vecIt == vecCrib.end() ) ? -1 : vecIt - vecCrib.begin(); vecCrib.push_back( mapIt->second ); // 対象要素の暗号化結果から次の対象要素を探す mapIt = mapCrib.find( ( mapIt->second ).crypto ); // 対象要素が見つからなければループ構成できない if ( mapIt == mapCrib.end() ) return( vecInt ); } while ( loopStart < 0 ); // ループの先頭より前の部分を削除 vecCrib.erase( vecCrib.begin(), vecCrib.begin() + loopStart + 1 ); for ( unsigned int i = 0 ; i < vecCrib.size() ; i++ ) vecInt.push_back( vecCrib[i].rotorLoc ); return( vecInt ); } /* hasSameLoop 同じループが登録されているかチェックする const vector<VecUInt>& vecVecInt : ループを登録したベクタ const VecUInt& vecInt : 対象のループ 戻り値 : true ... あった ; false ... なかった */ bool hasSameLoop( const vector<VecUInt>& vecVecInt, const VecUInt& vecInt ) { for ( unsigned int i = 0 ; i < vecVecInt.size() ; i++ ) if ( findFromVector( vecVecInt[i], vecInt[0] ) >= 0 ) return( true ); return( false ); } /* makeAllLoop メッセージとその暗号から、構成可能なループを全て作成する const string& plain : 平文メッセージ const string& crypto : 暗号化メッセージ unsigned int minSize : ループとして採用する最小要素数 bool debugFlag : デバッグ用 戻り値 : 作成したループを持つベクタ */ vector<VecUInt> makeAllLoop( const string& plain, const string& crypto, unsigned int minSize, bool debugFlag ) { vector<VecUInt> vecVecInt; MapCrib mapCrib; // ループ構成要素をメッセージから抽出してMapを作成する for ( unsigned int i = 0 ; i < plain.length() && i < crypto.length() ; i++ ) { if ( mapCrib.find( plain[i] ) == mapCrib.end() && plain.find( crypto[i] ) != string::npos ) { Crib c( i, crypto[i] ); mapCrib[plain[i]] = c; } } if ( debugFlag ) { cout << "Crib : "; for ( MapCrib::iterator it = mapCrib.begin() ; it != mapCrib.end() ; it++ ) cout << it->first << "-" << ( it->second ).crypto << " "; cout << endl; } // ループ開始位置を変えながらループ作成 for ( unsigned int i = 0 ; i < mapCrib.size() ; i++ ) { VecUInt vecInt = makeLoop( mapCrib, i ); if ( vecInt.size() >= minSize ) if ( ! hasSameLoop( vecVecInt, vecInt ) ) // 同じループ構成がすでにないかチェック vecVecInt.push_back( vecInt ); } return( vecVecInt ); } |
makeAllLoopは、与えられたクリブから得られる全てのループを返します。最初に、クリブの中からループ構成可能な部分だけを抽出します。ループ構成可能となる条件として、必ず暗号化された文字が平文文字列に含まれていることを満たす必要があるため、そうでない部分は除外します。また、平文文字列の中で同じ文字が存在した場合、最初に登録された文字だけを登録しています(この部分については改良の余地はあります)。次に、ループチェックの開始位置を先頭より一つずつ順にずらしながら、makeLoopを使ってループを構成し、hasSameLoopで、すでに登録されたループの中に一致するものがないかをチェックします。
makeLoopはループ作成のメインルーチンになります。暗号化された文字が平文文字列のどの位置にあるかを開始位置から順にチェックしながら繋いでいきます。ここで、追加した要素と同じものがすでにループ内に存在していれば、ループが閉じたことになります。しかし、暗号化文字が平文文字列に含まれているものだけが登録されていても、必ずループが閉じるとは限りません。平文文字列の中から除外された文字が暗号化文字となっている場合は、その個所でループが切れてしまします。サンプル・プログラムではこの場合に空のベクタを返すようにしています。ループが閉じた場合は、ループが繋がった個所より前の要素を削除すれば、完全なループが形成されます。
以下に、処理の例を示します。
クリブの内容
ローター位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
平文文字列 | A | B | C | D | E | F | G | H | I | J |
暗号化文字列 | H | C | F | G | X | J | C | B | L | D |
ループ構成可能な要素
ローター位置 | 0 | 1 | 2 | 3 | 5 | 6 | 7 | 9 |
---|---|---|---|---|---|---|---|---|
平文文字列 | A | B | C | D | F | G | H | J |
暗号文字列 | H | C | F | G | J | C | B | D |
Aからスタートして順に連結。
A→H→B→C→F→J→D→G→C
Cがすでにループ内に存在しているため、ここでループが閉じます。Cより前にある要素を全て削除してループが完成します。
C→F→J→D→G→C
ここで、Jからスタートして順に連結してみます。
J→D→G→C→F→J
今度もループは形成されましたが、その内容は開始位置が異なる以外、前のループ構成と全く同じです。このようにループが重なった場合、重複して登録しないようにする必要があります。重複しているかどうかの判定は、構成したループの中の一つの要素(例えば先頭の要素)が、別のループの要素として存在しているかチェックすることで行うことができます。平文文字→暗号化文字のルートは一意なため、重なった要素がひとつでも存在すれば、そこから続くルートも同じ内容になります。つまり、ループ自体も同一となってしまうわけです。
次は、ボンブによる解読のシミュレーションを行ったサンプル・プログラムです。
// Bombクラス class Bomb { vector<Enigma*> vecEnigma; // エニグマによるループ構造 VecUInt cribLoc; // クリブ上での、ローターの相対位置 bool debugFlag; // デバッグ用 public: /* Bomb コンストラクタ const Enigma& enigma : 解読する対象のエニグマ vector<unsigned int> _cribLoc : クリブ上での、ローターの相対位置 */ Bomb( const Enigma& enigma, VecUInt _cribLoc, bool _debugFlag = false ); // ローター位置の解読を実行する // ローター位置の全パターンで解読 vector<string> decode(); // 一つのローター位置で解読 bool decode( const string& rotorLoc ); // ローター位置のリストにある要素で解読(不一致のものはベクタから削除) void decode( vector<string>& vecStr ); }; /* Bomb コンストラクタ const Enigma& enigma : 解読する対象のエニグマ VecUInt _cribLoc : クリブ上での、ローターの相対位置 */ Bomb::Bomb( const Enigma& enigma, VecUInt _cribLoc, bool _debugFlag ) { cribLoc = _cribLoc; debugFlag = _debugFlag; for ( unsigned int i = 0 ; i < cribLoc.size() ; i++ ) { vecEnigma.push_back( new Enigma( enigma ) ); vecEnigma[i]->resetPlug(); // プラグボードはリセットしておく } } /* Bomb::decode 全てのローター位置の組み合わせに対して ループが閉じるかをチェックする 戻り値 : ループが閉じた組み合わせを持ったベクタ */ vector<string> Bomb::decode() { vector<string> vecStr; if ( vecEnigma.size() < 2 ) return( vecStr ); // ローター位置は全て0にリセット vecEnigma[0]->init(); string rotorLoc = vecEnigma[0]->current(); while ( 1 ) { // ループのチェック if ( decode( rotorLoc ) ) vecStr.push_back( rotorLoc ); // ローター位置をひとつ進める vecEnigma[0]->init( rotorLoc ); if ( vecEnigma[0]->rotate() ) break; rotorLoc = vecEnigma[0]->current(); } return( vecStr ); } /* Bomb::decode ローター位置のベクタにある要素でループが閉じるかをチェックする (不一致のものはベクタから削除) vector<string>& vecStr : ローター位置のベクタ */ void Bomb::decode( vector<string>& vecStr ) { if ( vecEnigma.size() < 2 ) return; for ( unsigned int i = vecStr.size() ; i > 0 ; i-- ) { if ( ! decode( vecStr[i - 1] ) ) vecStr.erase( vecStr.begin() + i - 1 ); } } /* Bomb::decode 指定したローター位置でループが閉じるかをチェックする const string& rotorLoc : ローター位置 */ bool Bomb::decode( const string& rotorLoc ) { if ( vecEnigma.size() < 2 ) return( false ); // ループ上の各エニグマのローター位置を初期化 for ( unsigned int i = 0 ; i < vecEnigma.size() ; i++ ) { vecEnigma[i]->init( rotorLoc ); for ( unsigned int j = 0 ; j < cribLoc[i] ; j++ ) vecEnigma[i]->rotate(); } // 入力可能な文字全てに対して繰り返す for ( unsigned int i = 0 ; i < vecEnigma[0]->ioSize() ; i++ ) { vector<string> message; char c = vecEnigma[1]->keyOut( i ); string s( 1, c ); // ループ上で順次暗号化 for ( unsigned int j = 1 ; j <= vecEnigma.size() ; j++ ) { message.push_back( s ); s = vecEnigma[j % vecEnigma.size()]->convert( s ); } if ( s[0] == c ) { // ループが閉じたか? if ( debugFlag ) { cout << "Rotor Location=" << rotorLoc << " : "; for ( unsigned int j = 0 ; j < message.size() ; j++ ) cout << message[j] << "->"; cout << s << endl; } return( true ); } // ローターを反転して元の位置に戻す for ( unsigned int j = 0 ; j < vecEnigma.size() ; j++ ) { vecEnigma[j]->negRotate(); } } return( false ); } |
ボンブの作成には当然、エニグマのオブジェクトを利用しています。解析したいエニグマとクリブから作成したループ構成を引数として、ボンブを構築します。コンストラクタでは単純に、ループの要素数だけエニグマを並べているだけです。ローターの位置の設定などは全て解読時に行います。また、プラグボードは、意味をなさないためリセットしてあります。これは、各エニグマの入出力結果を見たい場合、その内容が見やすくなるようにするための処置です。
解読のためのメンバ関数であるdecodeは全部で三種類あります。引数を持たないdecodeは、ローター位置の全組み合わせに対して処理を行うためのもので、ループが閉じた組み合わせのベクタを返します。この返り値を引数として、二番目のdecodeを使ってさらに候補を絞りこみます。利用方法としては、最初のループを、引数を持たないdecodeを使って、全てのローター位置の組み合わせに対して解読し、次のループからは、候補となるローター位置の組み合わせのベクタを渡してさらに絞りこむような形になります。
実際の解読処理は最後のdecodeが行っています。指定したローター位置に従って各エニグマのローター位置をセットした後、入力文字を変えながら各エニグマで処理を行い、最後に出力された結果が入力した文字と一致するかを確認してループが閉じたかをチェックします。
今回作成したサンプルを使い、簡単なテスト・プログラムで動作試験をして見ましたが、絞りこんだ結果はたいてい、アルファベットの数だけになりました。例えば、ローターの位置にPSWを指定すると、候補はASWからZSWまで、つまり先頭だけが、取りうる全ての文字を持つことになり、候補を一つだけに絞りこむことはできませんでした。いろいろと原因を考えてみましたが、今のところは判明していません。
今回のサンプル・プログラムのソースをアップロードしておきます。御自由にお使い下さい。
動作確認はVine Linux 4.1上で行いました。特殊なライブラリは使用していないため、どんな環境でもビルド・動作は可能だと思います。
前に戻る | タイトルに戻る |