次の方法で共有


Encrypt It

新しい Advanced Encryption Standard を使用してデータの安全性を保持する

James McCaffrey


この資料では、読者が C# およびビット操作に精通していることを前提としています。

Level of Difficulty 1 2 3

Download the code for this article: AES.exe (もはや利用できます) (143KB)

Browse the Code Online (英語)

翻訳元: Keep Your Data Secure with the New Advanced Encryption Standard (英語)

要約

Advanced Encryption Standard (AES) は、電子データの暗号化に関する国立標準技術研究所 (NIST: National Institute of Standards and Technology) の仕様です。この仕様は、金融機関、電気通信、政府機関のデータなどのデジタル情報を暗号化する容認された手段になることが期待されています。この資料では、AES の概要を示し、AES によって使用されるアルゴリズムを説明します。この資料には、.NET データの暗号化の完全な C# 実装と例が含まれています。この資料を読み終えると、AES を使用してデータを暗号化し、AES に基づくソフトウェアをテストし、ユーザーのシステムで AES 暗号化を使用できるようになります。


目次

  1. AES アルゴリズムの概要
  2. GF (28) でのフィールド加算と乗算
  3. キー拡張
  4. C# での AES クラス コンストラクタ
  5. C# での AES Cipher メソッド
  6. C# での AES InvCipher メソッド
  7. AES クラスを使用する
  8. 実装の選択肢
  9. まとめ

国立標準技術研究所 (NIST: National Institute of Standards and Technology) は、2002 年 5 月 26 日に新しい Advanced Encryption Standard (AES) 仕様を確立しました。この資料では、C# で記述された AES の実用的な実装と、AES とは正確にはどのようなもので、コードがどのように機能するかについて説明します。ここでは AES を使用してデータを暗号化する方法、およびここに用意したコードを拡張して市場に出荷できる品質の AES クラスを開発する方法を示します。また、ユーザーのソフトウェア システムに AES 暗号化を組み込む方法と理由、および AES に基づくソフトウェアのテスト方法も説明します。

この資料で提示するコードおよびこの資料に基づくその他すべての実装は、適用可能な連邦政府の暗号モジュール輸出規制に従うものとします (正確な規制については、Commercial Encryption Export Controls (英語) を参照してください)。

AES は、電子データの保護に使用できる新しい暗号アルゴリズムです。 特に、AES は 128 ビット、192 ビット、256 ビットのキーを使用できる対称キーによる反復ブロック暗号で、128 ビット (16 バイト) のブロック単位にデータを暗号化および暗号化解除します。キーのペアを使用する公開キー暗号とは異なり、対称キー暗号はデータの暗号化と暗号化解除に同じキーを使用します。ブロック暗号によって返される暗号化されたデータは、入力データと同じビット数を保持します。反復暗号はループ構造を使用して、入力データの並べ替えと置き換えを繰り返し実行します。図 1 は、192 ビット キーを使用して、1 ブロック 16 バイトのデータを暗号化および暗号化解除中の AES の動作を示しています。

図 1 Some Data
図 1 Some Data

AES は、以前の Data Encryption Standard (DES) の後継仕様です。DES は、1977 年に連邦政府標準として承認され、ハードウェア、ソフトウェア、暗号解読理論の進化により、1998 年に DES で暗号化されたメッセージが 56 時間で暗号化解除できるようになるまで発展し続けました。それ以降、DES で暗号化されたデータへの攻撃が数多く成功するようになり、現時点では DES はその役割を終えたと見なされています。

1999 年終盤に、Joan Daemen と Vincent Rijmen という 2 人の研究者によって作成された Rijndael ("レインドール" と発音します) アルゴリズムが、セキュリティ、実装効果、汎用性、単純性のデザイン基準を最も満たす提案として、NIST によって選択されました。AES と Rijndael という用語が同じ意味で使用される場合がありますが、この 2 つは別のものです。AES は、銀行や金融取引、電気通信、個人情報や政府機関情報など、商用アプリケーションで使用されるデータを含めて、あらゆる形式の電子データを暗号化するための業界標準になることが幅広く期待されています。


1. AES アルゴリズムの概要

AES アルゴリズムは、並べ替えと置き換えに基づいています。並べ替えはデータを再配列することで、置き換えはデータの 1 単位を別のデータに置き換えることです。AES では、いくつか異なる技法を使用して並べ替えと置き換えが行われます。これらの技法を説明するために、図 1 に示すデータを使用して、AES 暗号化の具体的な例を調べていくことにしましょう。

以下は、ここで暗号化する 128 ビット値と各バイト位置を示すインデックス配列です。

00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 
0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15

192 ビット キー値を以下のように考えます。

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 
0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19 20 21 22 23

図 2 Sbox
図 2 Sbox

AES コンストラクタが呼び出されると、暗号化によって使用される 2 つのテーブルが初期化されます。最初のテーブルは、Sbox という名前の置換ボックスです。これは、16 16 のマトリックスになっています。Sbox の最初の 5 行、5 列を図 2 に示します。この初期化処理中に、暗号化ルーチンはキー配列を受け取り、このキー配列を使用して、図 3 に示す w[] という名前の "キー スケジュール" テーブルを生成します。

図 3 Key Sched.
図 3 Key Sched.

w[] の最小の Nk (6) 行には、オリジナルのキー値 (0x00 ~ 0x17) によりシード値が設定されます。残りの行はそのシード キーから生成されます。変数 Nk は、シード キーのサイズを 32 ビット ワード単位で表します。w[] が正確に何行生成されるかは、後半に AES 実装を調べるときにわかるでしょう。重要な点は、使用するキーが 1 つだけでなく、数多く存在することです。このような新しいキーはラウンド キーと呼ばれ、元のシード キーと区別されます。

図 4 State
図 4 State

AES 暗号化ルーチンは、16 バイトの入力配列を State という名前の 4 4 バイトのマトリックスにコピーすることから始まります (図 4 参照)。AES 暗号化アルゴリズムには Cipher という名前が付けられ、State[] で演算されます。このアルゴリズムは擬似コードで記述できます (図 5 参照)。

図 5 Cipher Algorithm Pseudocode

Cipher(byte[] input, byte[] output)
{
  byte[4,4] State;
  copy input[] into State[]
  AddRoundKey
  for (round = 1; round < Nr-1; ++round)
  {
    SubBytes
    ShiftRows
    MixColumns
    AddRoundKey
  }
  SubBytes
  ShiftRows
  AddRoundKey
  copy State[] to output[]
}

暗号化アルゴリズムでは、仕様で AddRoundKey と呼ばれる予備的な処理手順が実行されます。AddRoundKey では、キー スケジュールの最初の 4 行を使用して、State マトリックスでバイト単位に XOR 演算が行われ、入力 State[r,c] とラウンド キー w[c,r] の XOR 演算が行われます。

たとえば、State マトリックスの最初の行にバイト列 {00, 44, 88, cc} が保持され、キー スケジュールの最初の列に {00, 04, 08, 0c} が保持されているとすると、State[0,2] の新しい値は State[0,2](0x88) と w[2,0](0x08) との XOR の結果、または 0x80 になります。

1 0 0 0 1 0 0 0 
0 0 0 0 1 0 0 0  XOR

1 0 0 0 0 0 0 0

AES 暗号化アルゴリズムのメイン ループでは、仕様で SubBytes、ShiftRows、MixColumns、および AddRoundKey と呼ばれる 4 つの異なる演算が State マトリックスで実行されます。AddRoundKey 演算は、AddRoundKey が呼び出されるたびにキー スケジュールの次の 4 行が使用されることを除けば、予備的な AddRoundKey と同じです。SubBytes ルーチンでは、State マトリックスの各バイトを受け取り、Sbox テーブルによって決定される新しいバイトに置き換えることによって、置き換え演算が行われます。たとえば、State[0,1] の値が 0x40 でその置き換え値を検索する場合、State[0,1] の値 (0x40) を取り出し、左の桁 (4) を x とし、右の桁 (0) を y とします。 その後、図 2 に示すように、x と y を Sbox のインデックスとして使用して、置き換え値を検索します。

ShiftRows では、State マトリックス内のバイト列を左に回転することによって、並べ替え演算が行われます。図 6 に、State[] で ShiftRows が機能する方法を示します。State の行 0 は 0 個分左に位置が回転され、行 1 は 1 個分、行 2 は 2 個分、行 3 は 3 個分左にそれぞれ位置が回転されます。

図 6 Running ShiftRows on State
図 6 Running ShiftRows on State

MixColumns 演算は、AES アルゴリズムの中では理解するには最も複雑な置き換え演算です。この演算では、各バイトが、そのバイトの列内の値の算術フィールド加算と乗算の結果に置き換えられます。次のセクションでは、特殊なフィールド加算と乗算について詳しく説明します。

State[0,1] の値を 0x09、列 1 のその他の値を 0x60、0xe1、0x04 とします。State[0,1] の新しい値は以下のように示されます。

State[0,1] = (State[0,1] * 0x01) + 
             (State[1,1] * 0x02) +
             (State[2,1] * 0x03) +
             (State[3,1] * 0x01)

           = (0x09 * 0x01) + (0x60 * 0x02) + (0xe1 * 0x03) +
             (0x04 * 0x01)

           = 0x57

加算と乗算は特殊な算術フィールド演算で、通常の整数加算や整数乗算ではありません。

4 つの演算 SubBytes、ShiftRows、MixColumns、AddRoundKey は Nr 回実行されるループ内部で呼び出されます。この回数は指定したキー サイズのラウンド数から 1 を差し引いた値になります。暗号化アルゴリズムが使用するラウンド数は 10、12、または 14 になり、シード キーのサイズが 128 ビット、192 ビット、または 256 ビットのいずれかによって決まります。この例では、Nr が 12 になるので、4 つの演算は 11 回呼び出されます。暗号化アルゴリズムは、この繰り返し完了後に、SubBytes、ShiftRows、および AddRoundKey を呼び出して処理を終了してから、State マトリックスを出力パラメータにコピーします。

まとめると、AES 暗号化アルゴリズムには中核となる 4 つの演算があります。AddRoundKey では、シード キー値から生成されるラウンド キーを使用して、4 バイトのグループが置き換えられます。SubBytes では、置換テーブルを使用して、個別のバイト列が置き換えられます。ShiftRows では、4 バイトの行を回転することによって、4 バイトのグループが並べ替えられます。MixColumns では、フィールド加算とフィールド乗算の両方の組み合わせを使用して、バイト列が置き換えられます。

ページのトップへ


2. GF(28) でのフィールド加算と乗算

ここまで説明してきたように、AES 暗号化アルゴリズムでは MixColumns ルーチンを除いて、置き換えと並べ替えにきわめてわかりやすい技法が使用されています。MixColumns ルーチンでは、特殊な加算と乗算が使用されます。AES により使用される加算と乗算は、数学的なフィールド理論に基づいています。 特に、AES は GF(28) というフィールドに基づいています。

GF(28) フィールドは、加算と乗算に加えて、0x00 から 0xff までの 256 個の値のセットで構成されます。そのため、(28) と呼ばれます。GF は Galois Field の略で、フィールド理論を確立した数学者の名前にちなんでいます。GF(28) の特性の 1 つは、加算または乗算の演算結果が {0x00 ... 0xff} のセットの値になる必要があることです。フィールドの理論はかなり難解ですが、GF(28) 加算の最終結果は単純です。GF(28) 加算は単なる XOR 演算です。

ただし、GF(28) での乗算は複雑になります。後半の C# 実装でもお分かりのように、AES 暗号化ルーチンと暗号化解除ルーチンでは、0x01、0x02、0x03、0x09、0x0b、0x0d、および 0x0e の 7 つの定数を乗算する方法だけを理解しておく必要があります。そこで、GF(28) 乗算理論を一般的に説明する代わりに、このような 7 つの特定のケースだけを説明します。

GF(28) で 0x01 を乗算する場合は特別です。これは通常の算術演算で 1 を乗算するのに相当し、同じ方法で機能します。つまり、値に 0x01 を掛けるとその値自体に等しくなります。

では、0x02 の乗算を見ていくことにしましょう。加算の場合と同様に理論は複雑ですが、最終結果はきわめて単純です。被乗数が 0x80 未満の場合は、単に被乗数を 1 ビット左にシフトした値が乗算結果になります。被乗数が 0x80 以上の場合は、被乗数を 1 ビット左にシフトした値と 0x1b とを XOR 演算した値が乗算結果になります。これにより、"フィールド オーバーフロー" が防がれ、積が範囲内に収まります。

GF(28) での加算と 0x02 の乗算が確立されると、それらを使用して任意の定数の乗算を定義できます。GF(28) での 0x03 の乗算は、2 の累乗と加算に分解できます。任意の値 b に 0x03 を乗算する場合、0x03 = 0x02 + 0x01 を利用します。したがって、以下のようになります。

b * 0x03 = b * (0x02 + 0x01)
         = (b * 0x02) + (b * 0x01)

0x02 と 0x01 の乗算方法と加算方法がわかっているので、この演算は実行可能です。同様に、任意のバイト b に 0x0d を掛ける場合は、以下のように分解されます。

b * 0x0d = b * (0x08 + 0x04 + 0x01)
         = (b * 0x08) + (b * 0x04) + (b * 0x01)
         = (b * 0x02 * 0x02 * 0x02) + (b * 0x02 * 0x02) + (b * 0x01)

暗号化と暗号化解除アルゴリズムの AES MixColumns に必要なその他の乗算は、以下に示す同じ汎用のパターンに従います。

b * 0x09 = b * (0x08 + 0x01)
         = (b * 0x02 * 0x02 * 0x02) + (b * 0x01)

b * 0x0b = b * (0x08 + 0x02 + 0x01)
         = (b * 0x02 * 0x02 * 0x02) + (b * 0x02) + (b * 0x01)

b * 0x0e = b * (0x08 + 0x04 + 0x02)
         = (b * 0x02 * 0x02 * 0x02) + (b * 0x02 * 0x02) + (b * 0x02)

まとめると、GF(28) での加算は XOR 演算です。GF(28) での乗算は、加算と 0x02 の乗算に分解され、0x02 の乗算は条件付き 1 ビット左シフトになります。AES 仕様には、GF(28) での演算についての多くの詳細情報が含まれています。

ページのトップへ


3. キー拡張

AES の暗号化と暗号化解除アルゴリズムでは、シード キーのバイト配列から生成されるキー スケジュールが使用されます。AES 仕様では、これは KeyExpansion ルーチンと呼ばれています。基本的に、1 つのキーを使用するのではなく、初期キーから複数のキーを生成することにより、ビットの拡散が大幅に増加します。KeyExpansion を理解することは、それほど困難ではありませんが、AES アルゴリズムでは複雑な部分の 1 つです。KeyExpansion ルーチンは大まかな擬似コードでは以下のようになります。

KeyExpansion(byte[] key, byte[][4] w) 
{
  シード キーを w の最初の行にコピーします

  for each w の残りの行
  {
    前回の 2 行を使用して新しい行を作成します
  } 
}

"前回の 2 行を使用して新しい行を作成します" ルーチンでは、2 つのサブルーチン RotWord と SubWord、および Rcon という名前の定数のテーブル ("ラウンド定数" 用) が使用されます。ここでは、まずこれら 3 つの項目を調べ、KeyExpansion ルーチンの全体的な説明に戻りましょう。

RotWord ルーチンは単純です。このルーチンは 4 バイトの配列を受け取り、その配列を 1 個分位置を左に回転します。ラウンド スケジュール テーブル w[] には 4 列あるので、RotWord は w[] の 1 行を左に回転することになります。KeyExpansion で使用される RotWord 関数は、暗号化アルゴリズムで使用される ShiftRows ルーチンと非常によく似ています。ただし、後者が暗号化状態テーブル State[] 全体を処理するのに対して、前者はキー スケジュール w[] の 1 行で機能します。

SubWord ルーチンでは、置換テーブル Sbox を使用してキー スケジュール テーブル w[] の特定の行でバイト単位の置き換えが行われます。KeyExpansion での置き換えでは、暗号化アルゴリズムでの同様の置き換えとまったく同じ演算が行われます。置き換えられる入力バイトは、置換テーブル Sbox のインデックスとして使用される (x,y) の組に分解されます。たとえば、0x27 の置き換えでは、この値が x = 2 と y = 7 に分解され、結果として Sbox[2,7] の値 0xcc が返されます。

KeyExpansion ルーチンでは、ラウンド定数テーブルと呼ばれる配列 Rcon[] が使用されます。これらの定数は 4 バイトの定数で、それぞれキー スケジュール テーブルの行に相当します。AES KeyExpansion ルーチンでは、11 個のラウンド定数が必要になります。このような定数のリストを図 7 に示します

図 7 Initializing Rcon

private void BuildRcon()
{
  this.Rcon = new byte[11,4] { {0x00, 0x00, 0x00, 0x00},  
                               {0x01, 0x00, 0x00, 0x00},
                               {0x02, 0x00, 0x00, 0x00},
                               {0x04, 0x00, 0x00, 0x00},
                               {0x08, 0x00, 0x00, 0x00},
                               {0x10, 0x00, 0x00, 0x00},
                               {0x20, 0x00, 0x00, 0x00},
                               {0x40, 0x00, 0x00, 0x00},
                               {0x80, 0x00, 0x00, 0x00},
                               {0x1b, 0x00, 0x00, 0x00},
                               {0x36, 0x00, 0x00, 0x00} };
}  // BuildRcon()

各ラウンド定数の左端のバイトは、GF(28) フィールドでの 2 の累乗値になります。別の見方をすると、前述の GF(28) での乗算の説明で示したように、各値が前の値の 0x02 倍になっていることがわかります。0x80 0x02 = 0x1b となるのは、前述のように、0x80 を左にシフトしてから 0x1b と XOR 演算が行われるためです。

では、KeyExpansion 内部のループを詳しく見ていくことにしましょう。前述よりもう少し詳しくした擬似コードでは、ループは以下のようになります。

for (row = Nk; row < (4 * Nr+1); ++row)
{
  temp = w[row-1]

  if (row % Nk == 0) 
    temp = SubWord(RotWord(temp)) xor Rcon[row/Nk]
  else if (Nk == 8 and row % Nk == 4)
    temp = SubWord(temp)

  w[row] = w[row-Nk] xor temp
}

if 句を無視して考えると、キー スケジュール テーブル w[] の各行が、前回行と Nk 行前 (キー サイズによって 4、6、または 8) の行との XOR 演算の結果になることがわかります。最初の if 条件により、SubWord、RotWord、およびラウンド定数との XOR 演算がキー スケジュールの行が 4、6、8 行おきのいずれかで変更されます。何行おきに変更されるかは、キー サイズが 128 ビット、192 ビット、256 ビットのいずれであるかによって決まります。256 ビット キーの場合、キー スケジュールに新たな変化をつけるために、2 番目の条件により、12、20、28 など、8 行おきに行が変更されます。

KeyExpansion がどのように機能するかを、この資料の最初に示した例を使って説明しましょう。 シード キーを以下に示すように 192 ビット/6 ワード値とします。

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16

キー スケジュール バイト テーブル w[] は 4 次元の列で、Nb (Nr + 1) は 4 (12 + 1) に等しくなります。つまり 52 行になります。KeyExpansion ルーチンにより、シード キーの値がキー スケジュール テーブル w[] の最初の行にコピーされます。ここではシード キーが 192 ビット (24 バイト) で、w[] は常に 4 列を保持するので、この場合 KeyExapansion によりシード キーが w[] の最初の 6 行にコピーされます。では KeyExpansion ルーチンによりキー スケジュール テーブルの残りの部分が設定される方法を見ていくことにしましょう。この例では、行 0 から 5 までにはシード キー値が設定されるので、最初に計算される行は行 6 になります。

temp = w[row-1] = 14 15 16 17

条件 (row % Nk == 0) が真の場合、最初に RotWord サブルーチンが適用され、以下のようになります。

temp = 15 16 17 14

次に SubWord が適用され、以下のようになります。

temp = 59 47 f0 fa

続いて、Rcon[row/Nk] = Rcon[6/6] = 01 00 00 00 との XOR 演算が行われ、以下のようになります。

temp = 58 47 f0 fa

最後に、w[row-Nk] = w[6-6] = 00 01 02 03 との XOR 演算が行われ、以下の結果が導き出されます。

w[6] = 58 46 f2 f9

このプロセスが、キー スケジュール テーブル w[] の残りのすべての行で繰り返されます。

まとめると、AES 暗号化と暗号化解除の重要な部分の 1 つは、初期シード キーから複数のラウンド キーを生成する部分です。このアルゴリズムでキー スケジュールが生成され、暗号化と暗号化解除のアルゴリズムと多くの点でよく似た、置き換えと並べ替えが使用されます。

ページのトップへ


4. C# での AES クラス コンストラクタ

ここで、AES 暗号化アルゴリズムのすべてのコンポーネントを調べ、C# でこれを実装していきましょう。AES アルゴリズムの公式仕様は、Federal Information Processing Standards Publication 197 に含まれています。ここでの実装をできる限り緊密にこの仕様に基づかせることを決めましたが、この仕様が実装ガイドよりも理論文書に近いものであることがすぐにわかりました。公式仕様をリソースとして有効活用するために、Standards Publication で使用されているのと同じ変数名を使用しました (意味がわかりにくい "Nr" や "w" の場合でもそのまま使用しました)。

ここでのデザインでは、以下に示すように 9 つのデータ メンバと 1 つの列挙型を使用しました。

public enum KeySize { Bits128, Bits192, 
                      Bits256 };  

private int Nb;         
private int Nk;         
private int Nr;         

private byte[] key;     
private byte[,] Sbox;   
private byte[,] iSbox;  
private byte[,] w;       
private byte[,] Rcon;   
private byte[,] State;

キー サイズは 128 ビット、192 ビット、または 256 ビットにしかならないので、これらを次のように列挙型にすることは大きな意味があります。

public enum KeySize { Bits128, Bits192, Bits256 };

仕様ドキュメントでは、基本的な記憶域単位として一般的にバイト列が使用されていますが、2 つの重要なデータ メンバのサイズには 4 バイト ワードが使用されます。2 つのメンバ Nb と Nk により、それぞれ、ワード単位のブロック サイズとワード単位のキー サイズが表されます。Nr はラウンド数を表します。ブロック サイズは常に 16 バイト (128 ビット、AES では 4 ワード) なので、定数として宣言しました。キー サイズには、列挙パラメータ KeySize の値によって、値 4、6、または 8 が代入されます。AES アルゴリズムはラウンド数分繰り返されることによって、暗号化されるデータの複雑性が増加されます。ラウンド数は 10、12、または 14 のいずれかになり、暗号解読理論に基づきます。ラウンド数は、キー サイズに直接依存します。

私はクラス インターフェイスをデザインするときに、逆行して考えていくことを好んでいます。つまり、アプリケーションからコンストラクタやメソッドが呼び出されるところを想像します。このようなアプローチにより、以下のように AES オブジェクトのインスタンスを作成することに決めました。

Aes a = new Aes(the key size, the seed key)

暗号化ルーチンと暗号化解除ルーチンは、以下のように呼び出されます。

a.Cipher(plainText, cipherText);
a.InvCipher(cipherText, decipheredText);

Cipher と InvCipher という名前はややわかりにくいものですが、AES 仕様ドキュメントでこの名前が使われているので、この名前を選択しました。以下に、AES クラスのコンストラクタのコードを示します。

public Aes(KeySize keySize, byte[] keyBytes)
{
  SetNbNkNr(keySize);

  this.key = new byte[this.Nk * 4];  
  keyBytes.CopyTo(this.key, 0);

  BuildSbox();
  BuildInvSbox();
  BuildRcon();
  KeyExpansion();  

}

コンストラクタでは、まず、図 8 に示すようなヘルパ メソッド SetNbNkNr を呼び出すことによって、Nb、Nk、および Nr の値が設定されます。 効率が問題になる場合は、このコードを直接コンストラクタに配置してメソッド呼び出しのオーバーヘッドを回避できます。

図 8 SetNbNkNr Method

private void SetNbNkNr(KeySize keySize)
{
  this.Nb = 4;     

  if (keySize == KeySize.Bits128)
  {
    this.Nk = 4;   
    this.Nr = 10;  
  }
  else if (keySize == KeySize.Bits192)
  {
    this.Nk = 6;   
    this.Nr = 12;
  }
  else if (keySize == KeySize.Bits256)
  {
    this.Nk = 8;   
    this.Nr = 14;
  }
}  // SetNbNkNr()

次に、コンストラクタに渡されるバイト列をクラスのフィールド変数にコピーする必要があります。他のクラス変数と共にキーが宣言され、以下のように値が設定されます。

this.key = new byte[this.Nk * 4];
keyBytes.CopyTo(this.key, 0);

コンストラクタで、プライベート ヘルパ メソッド BuildSbox と BuildInvSbox を使用して、置換テーブル Sbox[] と iSbox[] の初期化を呼び出すことにしました。ここで Sbox[] と iSbox[] は、キー拡張ルーチン、Cipher メソッドおよび InvCipher メソッドでそれぞれ必要になるので、Sbox[] の初期化と KeyExpansion メソッドの呼び出しを Cipher メソッドと InvCipher メソッドの両方に配置しました。しかし、これらをコンストラクタに配置した方がコード構造がより明確になります。sBox[] は、図 9 に示すように設定されます。iSbox[] を設定するコードも同じようになります。コードは読みやすいように構造化されています。後でおわかりになりますが、Sbox テーブルと iSbox テーブルの値の指定方法には、意外な代替案があります。

図 9 Sbox Initialization

private void BuildSbox()
{
  this.Sbox = new byte[16,16] {  // populate the Sbox matrix
/*        0     1     2     3     4     5     6     7     8     9     a     b     c     d     e     f */
/*0*/  {0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76},
/*1*/  {0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0},
/*2*/  {0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15},
/*3*/  {0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75},
/*4*/  {0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84},
/*5*/  {0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf},
/*6*/  {0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8},
/*7*/  {0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2},
/*8*/  {0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73},
/*9*/  {0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb},
/*a*/  {0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79},
/*b*/  {0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08},
/*c*/  {0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a},
/*d*/  {0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e},
/*e*/  {0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf},
/*f*/  {0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16} };

}  // BuildSbox()

キー スケジュール テーブル w[]、ラウンド定数テーブル Rcon[]、および状態マトリックス State[] を宣言し、プライベート ヘルパ メソッドにより、Rcon[] と w[] に値を代入します。ヘルパ メソッドを使用することは、テーブルを編成するのに最適な方法だと思っていますが、多くの場合、これはスタイル上の問題です。ラウンド定数テーブル Rcon に値を設定するコードは、図 7 に示されています。

図 7 Initializing Rcon

private void BuildRcon()
{
  this.Rcon = new byte[11,4] { {0x00, 0x00, 0x00, 0x00},  
                               {0x01, 0x00, 0x00, 0x00},
                               {0x02, 0x00, 0x00, 0x00},
                               {0x04, 0x00, 0x00, 0x00},
                               {0x08, 0x00, 0x00, 0x00},
                               {0x10, 0x00, 0x00, 0x00},
                               {0x20, 0x00, 0x00, 0x00},
                               {0x40, 0x00, 0x00, 0x00},
                               {0x80, 0x00, 0x00, 0x00},
                               {0x1b, 0x00, 0x00, 0x00},
                               {0x36, 0x00, 0x00, 0x00} };
}  // BuildRcon()

Rcon[] の各行の左端のバイトは、GF(28) での 2 の累乗であることを思い出してください。そのため、以下のような式を使用して、計算によりこのテーブルを構築することができます。

newVal = prevVal * 0x02;

AES コンストラクタは、キー スケジュール テーブル w[] を構築することによって完了します。キー スケジュール テーブルの構築は、KeyExpansion メソッドで行われます (図 10 参照)。このコードは非常にわかりやすいものです。仕様ドキュメントでは、仮想 4 バイト ワードのデータ型が使用されています。C# にはこのようなデータ型が存在しないので、4 バイトの配列でこのデータ型をシミュレートします。new 演算子を使用してキー スケジュール w[] の領域は割り当てられた後、w[] の最初の Nk (4、6、または 8) 行に、コンストラクタに渡されたシード配列 key[] から次のように値が設定されます。

this.w[row,0] = this.key[4*row];
this.w[row,1] = this.key[4*row+1];
this.w[row,2] = this.key[4*row+2];
this.w[row,3] = this.key[4*row+3];

図 10 KeyExpansion Method

private void KeyExpansion()
{
  this.w = new byte[Nb * (Nr+1), 4];  
  
  for (int row = 0; row < Nk; ++row)
  {
    this.w[row,0] = this.key[4*row];
    this.w[row,1] = this.key[4*row+1];
    this.w[row,2] = this.key[4*row+2];
    this.w[row,3] = this.key[4*row+3];
  }
  
  byte[] temp = new byte[4];
  
  for (int row = Nk; row < Nb * (Nr+1); ++row)
  {
    temp[0] = this.w[row-1,0]; temp[1] = this.w[row-1,1];
    temp[2] = this.w[row-1,2]; temp[3] = this.w[row-1,3];
  
    if (row % Nk == 0)  
    {
      temp = SubWord(RotWord(temp));
  
      temp[0] = (byte)( (int)temp[0] ^ (int)this.Rcon[row/Nk,0] );
      temp[1] = (byte)( (int)temp[1] ^ (int)this.Rcon[row/Nk,1] );
      temp[2] = (byte)( (int)temp[2] ^ (int)this.Rcon[row/Nk,2] );
      temp[3] = (byte)( (int)temp[3] ^ (int)this.Rcon[row/Nk,3] );
    }
    else if ( Nk > 6 && (row % Nk == 4) )  
    {
      temp = SubWord(temp);
    }
  
    // w[row] = w[row-Nk] xor temp
    this.w[row,0] = (byte) ( (int)this.w[row-Nk,0] ^ (int)temp[0] );
    this.w[row,1] = (byte) ( (int)this.w[row-Nk,1] ^ (int)temp[1] );
    this.w[row,2] = (byte) ( (int)this.w[row-Nk,2] ^ (int)temp[2] );
    this.w[row,3] = (byte) ( (int)this.w[row-Nk,3] ^ (int)temp[3] );
  
  }  // for loop
}  // KeyExpansion()

2 バイト相互の XOR 演算は、このコードで多く使用されています。C# バイト型には XOR 演算子 ^ が定義されていないので、一度バイト型から int 型にキャストされ、演算後にバイト型に戻される必要があります。以下に例を示します。

temp[0] = (byte)((int)temp[0] ^ (int)this.Rcon[row/Nk,0]);

以下のコードは使用できません。

temp[0] = temp[0] ^ this.Rcon[row/Nk,0];

KeyExpansion メソッドでは、仕様との命名規則の一貫性を維持するために、プライベート メソッド SubWord と RotWord とが条件付きで呼び出されます。繰り返しになりますが、C# にはワード型が存在しないので、ここでは 4 バイトの配列を使用して実装されます。SubWord と RotWord のコードは非常に単純です。この資料に添付される AesLib ソース コードを調べることによって容易に理解できるでしょう。

やや複雑な部分になる SubWord での値の置き換えを見てみましょう。置き換え値を検索するために、入力バイトを左側の 4 ビットと右側の 4 ビットに分割したことを思い出してください。指定されたバイトごとに >> 演算子により 4 ビット右にシフトして x インデックスを取り出し、0000 1111 と論理 AND を取ることによって y 値を取り出します。実際のコードよりも読みやすくするためにやや長くなっていますが、以下のようなことを行っています。

int x = word[0] >> 4;
int y = word[0] & 0x0f;
byte substitute = this.Sbox[x,y];
result[0] = substitute;

実際のコードでは以下のようになっています。

result[0] = this.Sbox[word[0] >> 4, word[0] & 0x0f];

まとめると、AES コンストラクタは、128 ビット、192 ビット、または 256 ビットのキー サイズとシード キー値のバイト配列を受け取ります。コンストラクタでは、暗号化アルゴリズム用に入力ブロック サイズ、シード キー サイズ、およびラウンド数に値が代入され、key という名前のデータ メンバにシード キーがコピーされます。また、コンストラクタでは、暗号化メソッドと暗号化解除メソッドで使用される 2 つの置換テーブル、ラウンド定数のテーブル、およびラウンド キーのキー スケジュールの 4 つのテーブルが構築されます。

ページのトップへ


5. C# での AES Cipher メソッド

Cipher メソッドのコードは、図 11 に示されています。このメソッドは、大部分の作業をプライベート メソッド AddRoundKey、SubBytes、ShiftRows、および MixColumns に請け負わせるので、きわめて単純です。

図 11 The Cipher Method

public void Cipher(byte[] input, byte[] output)  
{
  // state = input
  this.State = new byte[4,Nb];  
  for (int i = 0; i < (4 * Nb); ++i)
  {
    this.State[i % 4, i / 4] = input[i];
  }

  AddRoundKey(0);
        
  for (int round = 1; round <= (Nr - 1); ++round)  
  {
    SubBytes(); 
    ShiftRows();  
    MixColumns(); 
    AddRoundKey(round);
  }  

  SubBytes();
  ShiftRows();
  AddRoundKey(Nr);
          
  // output = state
  for (int i = 0; i < (4 * Nb); ++i)
  {
    output[i] = this.State[i % 4, i / 4];
  }

}  // Cipher()

Cipher メソッドは、プレーン テキストの入力配列を状態マトリックス State[] にコピーすることから始まります。Cipher メソッドでは、AddRoundKey の初期呼び出し後、合計ラウンド数から 1 差し引いた回数分繰り返されます。仕様に記述されているように、最終ラウンドでは MixColumns の呼び出しが省略されます。

プライベート メソッド AddRoundKey と SubBytes のコードは 図 12 に示されています。AddRoundKey メソッドでは、キー スケジュール配列 w[] の適切な 4 行を参照できるように、どのラウンドの処理を行っているかを認識する必要があります。State[r,c] と XOR 演算されるのは、w[r,c] ではなく w[c,r] であることに注意してください。SubBytes メソッドでは、KeyExpansion メソッドで使用されたのと同じ、右 4 ビット シフトと 0x0f でのマスク処理技法を使用して、入力バイトからインデックスが抽出されます。

図 12 AddRoundKey and SubBytes Methods

private void AddRoundKey(int round)
{
  for (int r = 0; r < 4; ++r)
  {
    for (int c = 0; c < 4; ++c)
    {
      this.State[r,c] = (byte) ( (int)this.State[r,c] ^
                                 (int)w[(round*4)+c,r] );
    }
  }
}  // AddRoundKey()

private void SubBytes()
{
  for (int r = 0; r < 4; ++r)
  {
    for (int c = 0; c < 4; ++c)
    {
      this.State[r,c] = this.Sbox[ (this.State[r,c] >> 4),
                                   (this.State[r,c] & 0x0f) ];
    }
  }
}  // SubBytes

ShiftRows メソッドのコードは、図 13 に示されています。ShiftRows (RotateRows という名前の方が適切だと思われますが) では、row[0] は 0 個分左の位置へ、row[1] は 1 個分左の位置へという具合に、行が回転されることを思い出してください。

図 13 ShiftRows Method

private void ShiftRows()
{
  byte[,] temp = new byte[4,4];
  for (int r = 0; r < 4; ++r)  
  {
    for (int c = 0; c < 4; ++c)
    {
      temp[r,c] = this.State[r,c];
    }
  }

  for (int r = 1; r < 4; ++r)  // 
  {
    for (int c = 0; c < 4; ++c)
    {
      this.State[r,c] = temp[ r, (c + r) % Nb ];
    }
  }
}  // ShiftRows()

ShiftRows では、State[] が temp[] マトリックスにコピーされた後、次のように並べ替えが行われます。

this.State[r, (c + r) % Nb] = temp[r,c];

ここでは、1 行内にラップするために、% 演算子が利用されています。

MixColumns メソッド (図 14 参照) は、各バイトを受け取り、GF(28) 加算と乗算を使用した、バイトの配列内のその他のすべての値との一次結合により、置き換えが行われます。乗算に使用される定数係数は、フィールド理論に基づいており、0x01、0x02、または 0x03 のいずれかになります。特定の列 c の置き換えは次のようになります。

State[0,c] = 0x02 * State[0,c] + 0x03 * State[1,c] + 0x01 * State[2,c] +
    0x01 * State[3,c]
State[1,c] = 0x01 * State[0,c] + 0x02 * State[1,c] + 0x03 * State[2,c] +
    0x01 * State[3,c]
State[2,c] = 0x01 * State[0,c] + 0x01 * State[1,c] + 0x02 * State[2,c] +
    0x03 * State[3,c]
State[3,c] = 0x03 * State[0,c] + 0x01 * State[1,c] + 0x01 * State[2,c] +
    0x02 * State[3,c]

図 14 MixColumns Method

private void MixColumns()
{
  byte[,] temp = new byte[4,4];
  for (int r = 0; r < 4; ++r)  
  {
    for (int c = 0; c < 4; ++c)
    {
      temp[r,c] = this.State[r,c];
    }
  }
      
  for (int c = 0; c < 4; ++c)
  {
    this.State[0,c] = (byte) ( (int)gfmultby02(temp[0,c]) ^
                               (int)gfmultby03(temp[1,c]) ^
                               (int)gfmultby01(temp[2,c]) ^
                               (int)gfmultby01(temp[3,c]) );

    this.State[1,c] = (byte) ( (int)gfmultby01(temp[0,c]) ^
                               (int)gfmultby02(temp[1,c]) ^
                               (int)gfmultby03(temp[2,c]) ^
                               (int)gfmultby01(temp[3,c]) );

    this.State[2,c] = (byte) ( (int)gfmultby01(temp[0,c]) ^
                               (int)gfmultby01(temp[1,c]) ^
                               (int)gfmultby02(temp[2,c]) ^
                               (int)gfmultby03(temp[3,c]) );

    this.State[3,c] = (byte) ( (int)gfmultby03(temp[0,c]) ^
                               (int)gfmultby01(temp[1,c]) ^
                               (int)gfmultby01(temp[2,c]) ^
                               (int)gfmultby02(temp[3,c]) );
    }
  }  // MixColumns

このような式は既にやや長くなってしまっているので、ここでは 0x01、0x02、および 0x03 との GF(28) 乗算の積を返す、プライベート ヘルパ関数を作成することに決めました。ヘルパ関数は非常に短くなります。たとえば、バイト b と 0x03 のフィールド乗算に使用されるコードは次のようになります。

return (byte)((int)gfmultby02(b) ^ (int)b );

前述したように、0x02 との乗算がすべての GF(28) 乗算の基本演算になります。そのため、このメソッドでは仕様で使用されているのと同じメソッド名を使用するという慣例を曲げて gfmultby02 メソッドという名前を付けました。仕様ではこのルーチンは xtime と呼ばれています。

Cipher メソッドでは、暗号化された出力を作成するために、入力に対して 4 つの演算が繰り返し適用されます。AddRoundKey では、1 つのオリジナルのシード キーから派生される複数のラウンド キーを使用して、バイト列が置き換えられます。SubBytes では、置換テーブルの値を使用して、バイト列が置き換えられます。ShiftRows では、バイト列の行がシフトされることによってバイト列が並べ替えられ、MixColumns では、列内の値のフィールド加算と乗算を使用して、バイト列が置き換えられます。

ページのトップへ


6. C# での AES InvCipher メソッド

AES の解読アルゴリズムを支える基本的な前提は単純で、暗号化されたブロックを解読するために、各演算を逆の順序で行い、元に戻していくだけです。これが基本的な概念ですが、処理を行うために細かい点がいくつかあります。

AES 仕様では、解読ルーチンに Decipher や Decrypt ではなく、InvCipher という名前が付けられています。この命名は AES を支える数学的な考え方を反映したもので、逆 (Inverse) 数学演算という用語に基づいています。

このコードと Cipher のコードを比較してみると、2 つの例外を除いてほぼ予想どおりになっていることがわかるでしょう。1 つ目の例外は、InvCipher での逆メソッド呼び出し (InvSubBytes など) の順序が、Cipher メソッドでの対応する呼び出し (SubBytes など) の順序と正反対になっていないことです。2 つ目は、InvCipher により InvAddRoundKey メソッドではなく AddRoundKey メソッドが呼び出されていることです。InvCipher アルゴリズムでは、キー スケジュール テーブルが使用されますが、より大きな番号のインデックスから始まり、行 0 に向かって処理が進められていくことに注意してください。

InvSubBytes メソッド、InvShiftRows メソッド、および InvMixColumns メソッドは、関連する SubBytes メソッド、ShiftRows メソッド、および MixColumns メソッドのコードを綿密にミラー化したものです。InvSubBytes メソッドは、Sbox[] テーブルの代わりに逆置換テーブル iSbox[] を使用することを除けば、SubBytes メソッドと同様です。

ご想像どおり、iSbox[] は Sbox[] で行われたすべてのマッピングを単純に元に戻します。たとえば、0x20 に等しいバイト b があり、Sbox[] でその置換値を検索すると、値 0xb7 が得られるとします。iSbox[] で 0xb7 の置換値を検索すると、値 0x20 が得られることになります。

同様に、InvShiftRows メソッドにより、ShiftRows メソッドの処理が元に戻されます。つまり、row[0] が 0 個分右に、row[1] が 1 個分右に、row[2] が 2 個分右に、row[3] が 3 個分右にシフトされます。

InvMixColumns メソッドにより、MixColumns の作業が元に戻されますが、あまり明確な方法ではありません。MixColumns では、状態マトリックス内の各バイトが、元のバイトの列内のバイト列とその係数 0x01、0x02、および 0x03 との一次結合により置き換えられることを思い出してください。再度、フィールド理論がかかわってきます。結局逆演算は似ていることがわかりますが、次のように 0x09、0x0b、0x0d、および 0x0e との乗算が使用されます。

State[0,c] = 0x0e * State[0,c] + 0x0b * State[1,c] + 0x0d * State[2,c] +
    0x09 * State[3,c]
State[1,c] = 0x09 * State[0,c] + 0x0e * State[1,c] + 0x0b * State[2,c] +
    0x0d * State[3,c]
State[2,c] = 0x0d * State[0,c] + 0x09 * State[1,c] + 0x0e * State[2,c] +
    0x0b * State[3,c]
State[3,c] = 0x0b * State[0,c] + 0x0d * State[1,c] + 0x09 * State[2,c] +
    0x0e * State[3,c]

MixColumns メソッドと同様に、ここでも既に長くなってしまっている式をインラインに展開したり、汎用の乗算ヘルパ関数を作成したりするのではなく、専用のヘルパ関数を作成することにしました。では、任意のバイト b に定数 0x0e (10 進数の 14) を乗算する関数をどのように作成したかを示しましょう。数値 14 は、他の任意の数値と同様に、2 の累乗の合計で表現できます。この場合、14 は 2 + 4 + 8 と表現できます。さらに、4 は 2 の二乗、8 は 2 の三乗になるので、14 は 2 + 22 + 23 と表現できます。GF(28) では加算は単なる XOR (^) 演算であったことを思い出してください。ここでは既に gfmultby02 関数が作成されているので、これを使用すれば次のような結果になります。

return (byte)( (int)gfmultby02(gfmultby02(gfmultby02(b))) ^  /* 23 + */
                    (int)gfmultby02(gfmultby02(b)) ^         /* 22 + */
                    (int)gfmultby02(b) );                    /* 2    */

AES 暗号化アルゴリズムで使用されたすべての演算は逆順に実行できるので、暗号化解除アルゴリズムは、最終的には暗号化で行われたすべての演算を反転することになります。

ページのトップへ


7. AES クラスを使用する

C# で実装された場合の AES の特徴の 1 つはその単純性にあります。図 1 で示される出力を生成するために使用した 図 15 のコードを考えてみましょう。このコードでは、まず、16 バイトのプレーン テキスト入力と 24 バイト (192 ビット) のシード キーのハード コードされた値が宣言されます。次に AES オブジェクトが初期化され、暗号化用の Cipher メソッドによりプレーン テキストが暗号テキストに暗号化され、その後 InvCipher が使用されて暗号テキストの暗号が解除されます。非常に明快で単純です。

図 15 Using AES

static void Main(string[] args)
{
  byte[] plainText = new byte[] {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 
      0x66, 0x77,0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};
    
  byte[] cipherText = new byte[16];
  byte[] decipheredText = new byte[16];
    
  byte[] keyBytes = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 
      0x07,0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,0x10, 0x11, 
      0x12, 0x13, 0x14, 0x15, 0x16, 0x17};
  
  Aes a = new Aes(Aes.KeySize.Bits192, keyBytes);

  Console.WriteLine("\nAdvanced Encryption System Demo in .NET");
  Console.WriteLine("\nThe plaintext is: ");
  DisplayAsBytes(plainText);

  Console.WriteLine("\nUsing a " + Aes.KeySize.Bits192.ToString() + 
      "-key of: ");
  DisplayAsBytes(keyBytes);
   
  a.Cipher(plainText, cipherText);

  Console.WriteLine("\nThe resulting ciphertext is: ");
  DisplayAsBytes(cipherText);

  a.InvCipher(cipherText, decipheredText);

  Console.WriteLine("\nAfter deciphering the ciphertext, the result 
      is: ");
  DisplayAsBytes(decipheredText);

  Console.WriteLine("\nDone");
  Console.ReadLine();
}  // Main()

static void DisplayAsBytes(byte[] bytes)
{
  for (int i = 0; i < bytes.Length; ++i)
  {
    Console.Write(bytes[i].ToString("x2") + " " );
    if (i > 0 && i % 16 == 0) Console.Write("\n");
  }
  Console.WriteLine("");
}  // DisplayAsBytes()

AES オブジェクトはバイト配列で機能するので、他の .NET データ型で機能するように容易に適合させることができます。ここでは、1 つの 8 文字 (16 バイト) の文字列を受け取り、それを暗号化し、その後暗号化解除する、小さな Windows ベースのデモ アプリケーションを作成しました。サンプルの実行結果は 図 16 に示すようになります。

図 16 Encryption Demo
図 16 Encryption Demo

暗号化ルーチンと暗号化解除ルーチンはどちらもユーザーが指定したキー サイズを知っておく必要があるので、次のようにクラス スコープの変数を宣言しました。

private Aes.KeySize keysize;

シード キーはユーザーが指定しないことに注意してください。デモでは、コンストラクタにダミーの新しい byte[16] を指定することによって、すべてのバイトが 0 で構成される "null キー" が使用されます。また、シード キーはすべて 0 に初期化されることになるので、ダミー引数のサイズは無関係になります。Null キー暗号化および暗号化解除は、データの偶然の外部検査を防ぐのに、容易で効果的な方法です。System.Text の Encoding.Unicode.GetBytes メソッドと Encoding.Unicode.GetString メソッドは、.NET 文字列とバイト配列の相互変換を非常に容易にします。

ページのトップへ


8. 実装の選択肢

ここで、この資料で提示した AES 実装のいくつかの重要な変形、ここで示したコードの拡張の可能性、および AES に対する暗号解読攻撃について見てみることにしましょう。

これまで作成してきた多くのコードがそうであったように、AES アルゴリズムの実装にも有効な代替アプローチがあります。なぜ、これが重要なのでしょうか? AES は、ほんの少しのメモリ容量しかないスマート カードから、大規模マルチプロセッサ メインフレーム システムまで、広範なシステムに適用できることを目的としています。多くのシナリオでは、パフォーマンスが重要になり、場合によってはメモリや処理リソースに制限があります。AES の各ルーチンは、事実上、メモリの量に合わせてパフォーマンスを最適化するように変更できます。また、反対にパフォーマンスが最適になるように使用するメモリ量を調整できます。たとえば、置換テーブル Sbox[] に 256 個の値を割り当てれば十分であると直感的に考えられます。ただし、これらの値は GF(28) 理論に基づいており、プログラムによって生成できることがわかります。逆置換テーブルやラウンド定数テーブルでも同じことが当てはまります。

代替実装のもう 1 つの興味深い可能性は、Cipher メソッドと InvCipher メソッドで使用される GF(28) 乗算です。この資料の実装では、0x02 を乗算する基本関数と、gfmultby02 を呼び出す 6 つの加算関数をコーディングしました。もう 1 つの可能性は、ここで行ったように 7 つの独立した関数を実装するのではなく、汎用の乗算関数を作成することです。さらに極端な方法として、0x01、0x02、0x03、0x09、0x0b、0x0d、および 0x0e と 256 個すべての値を乗算した積の完全なテーブルを使用することができます。GF(28) 乗算へのもう 1 つのアプローチは、通常 alog[] と log[] という 2 つの 256 バイトの配列を照合するように乗算を実装することです。これは、この乗算が GF(28) の対数に似た特性に基づいているためです。

ここで提示した AES クラスは任意の形式の .NET データを完全に暗号化できますが、多くの点でこれを拡張することを検討したいと考えるでしょう。まず、この資料で重点が置かれているのが AES を明確に説明することにあったので、エラー チェックなどはまったく省略されています。経験では、この AES クラスのようなクラスに合理的な量のエラー チェックを追加すると、ソース コードのサイズは 3 倍になります。AES では非常に多くの配列が使用されているので、多くのインデックス境界チェックを実行する必要があります。たとえば、ここで示したコンストラクタでは、シード キー パラメータのチェックさえ行われていません。

また、多くの機能を追加してこの AES クラスを拡張することも検討したいと考えるでしょう。拡張の最も明確な出発点は、System.String や System.Int32 のような基本的な .NET データ型を暗号化および暗号化解除するメソッドを追加することになるでしょう。さらに意欲的な拡張としては、暗号化されたストリーム クラスを実装することが挙げられるでしょう。

AES はどの程度安全なのでしょうか? これは難しい質問ですが、一般的な合意事項としては、AES は使用できる暗号化アルゴリズムの中では最も安全なアルゴリズムです。AES は、現在まで、他のすべての暗号化アルゴリズムよりも多くの精査が行われてきました。AES の暗号化を解読する効果的な方法は、理論的にも、実践的にも、可能性のあるすべてのキーを総当りで生成するしかないという意味では "安全" であると考えられます。キー サイズが 256 ビットの場合は、合理的な時間内で AES を解読できる総当り攻撃は認識されていません (現在使用できる最高速のシステムでも数年かかることになります)。

AES に対する最も成功する可能性のある攻撃は、タイミング攻撃と呼ばれる攻撃を可能にする脆弱な実装に起因することに注意してください。攻撃者は異なるキーを使用して、暗号化ルーチンが必要とする時間を正確に計測します。暗号化ルーチンが注意深くコーディングされておらず、実行時間がキー値に依存していると、キーに関する情報を推測できます。AES でこの可能性が最も高くなるのは MixColumns ルーチンです。これはフィールド乗算が使用されているためです。この攻撃には 2 つの防御策があります。1 つは、すべての乗算が同じ命令数になるように、ダミー命令を挿入することです。もう 1 つは、既に説明したようにフィールド乗算を照合テーブルとして実装することです。

AES の実装には、計算ではなく照合テーブルを使用するのを始めとして、多くの可能性があります。この資料で示した基本的な AES クラスを使用して、すべての形式の .NET データを暗号化および暗号化解除できます。また、付加機能を備えたクラスに拡張することもできます。

ページのトップへ


9. まとめ

新しい AES はあらゆる形式の電子情報を暗号化するための DES に置き換わる業界標準になることは確実でしょう。AES 暗号テキストを解読できる既知の暗号解読攻撃は、可能性のあるすべての 256 ビット キーによる総当たり検索を使用するしかないという意味では、AES で暗号化されたデータは解読不可能と言えます。

Microsoft .NET Framework で AES クラスを実装する際に見つかった大きな障害は、公式仕様ドキュメントがソフトウェア開発者の見地からではなく、数学者の見地から記述されていたことでした。特に、仕様では読者が GF(28) フィールドにかなり精通していることが前提とされており、AES を適切に実装するために必要な GF(28) 乗算の重要な要素がいくつか省略されていたことです。この資料では、特に GF(28) フィールド乗算に関する、AES の不可解な点を極力取り除くように努めました。

マイクロソフトやサード パーティ ベンダから .NET Framework ライブラリの形式で AES 暗号化が広範に利用できるようになるのは時間の問題です。しかし、このコードを理解しておくことは多くの点で価値があります。この実装は特に単純で、リソースのオーバーヘッドが少なくなっています。さらに、ソース コードにアクセスし、理解することによって、AES クラスをカスタマイズでき、より効果的にこの実装を使用できるようになるでしょう。

セキュリティは、ソフトウェア デザイン プロセスや開発プロセスの結果として生じるものではなくなりました。AES は重要な進歩で、これを理解し、使用することによって、ソフトウェア システムの信頼性と安全性が大幅に向上することになるでしょう。

ページのトップへ


For related articles see:

For background information see:


James McCaffrey は Volt Information Sciences Inc. に勤務しています。この会社で、マイクロソフトのソフトウェア エンジニアの技術トレーニングを管理しています。また Internet Explorer や MSN Search など、いくつかのマイクロソフト製品の契約技術者として働いています。jmccaffrey@volt.com または v-jammc@microsoft.com から彼にメッセージを送ることができます。


この記事は、MSDN マガジン - November 2003 号からの翻訳です。

QJ: 031102

ページのトップへ