基礎編 第8回 で「共通鍵で暗号化する」概念を、前回 第35回 暗号の歴史と OTP で「絶対安全な暗号(OTP) は鍵配送の壁で実用にならない、だから計算量的に強い暗号が必要」 という背景を学びました。本講はその一段奥に踏み込みます。なぜ AES が世界中で使われているのか、その内部の 4 つのラウンド操作 はどう設計されているのか、そして「ただ暗号化するだけ」の ECB がなぜ危険で、CBC・CTR・GCM へと進化したのか。最後に OpenSSL で AES を実際に動かして、ファイルを暗号化・復号するところまで。TLS・WireGuard・Wi-Fi WPA3 など、日常的なすべての暗号通信が依存している土台を可視化します。
本講を終えると、以下を達成できるようになります。
TLS でブラウザが Web サーバと通信するとき、実は両方の暗号方式が 役割分担 しています。最初の鍵交換だけ公開鍵で、本文の暗号化は共通鍵 ─ これが現代の ハイブリッド暗号 の発想です。
| 性質 | 共通鍵暗号(本講) | 公開鍵暗号(次回) |
|---|---|---|
| 鍵の数 | 1 つ(共有) | 2 つ(公開鍵 + 秘密鍵) |
| 速度 | 高速 | 低速(数百倍重い) |
| 事前共有 | 必要 | 不要 |
| 主用途 | 本文の暗号化 | 鍵交換・署名 |
| 代表アルゴリズム | AES, ChaCha20 | RSA, ECDH, ECDSA |
| 典型鍵長 | 128〜256 bit | 2048〜4096 bit(RSA)/ 256 bit(EC) |
つながる知識: WireGuard・QUIC・TLS 1.3 はすべて「鍵交換は公開鍵(ECDH)、本文は共通鍵(AES-GCM か ChaCha20-Poly1305)」というパターン。共通鍵の進化を理解せずにこれらのプロトコルは語れません。
確認: 現代のプロトコルが「本文の暗号化は共通鍵」を採用する最大の理由はどれか。
正解:B。AES-GCM は最新 CPU で数 GB/秒の処理が可能。一方 RSA-2048 は 1 MB のデータをブロック分割して暗号化するだけで数十秒かかる。だから「鍵交換は公開鍵で 1 回 → 本文は共通鍵で延々と」というハイブリッド構成が標準化された。D は誤り(共通鍵こそ鍵配送問題の元凶で、その解決のために公開鍵が登場した)。
図の見方:ブロック暗号は「16B ずつ箱詰めして暗号化」。短い平文には padding を足す。ストリーム暗号は「鍵から作った擬似乱数を平文と XOR するだけ」で、長さは元の平文と同じ。実装が単純で速い。両者は使い分けではなく、AES が広く普及した時代の終わりに ChaCha20 が補完として登場した、という関係。
AES は Intel/AMD の AES-NI 命令 によりハードウェアアクセラレーションで非常に速いですが、モバイル・組込み(古い ARM など)や AES-NI のない環境では遅い。ChaCha20 は 純粋にソフトウェアだけで高速 な設計で、Google Chrome は早期から AES-NI なし環境で ChaCha20-Poly1305 を優先するようになりました。今の TLS 1.3 はサーバ・クライアントの能力に応じて両者を使い分けます。
Q. AES-NI 命令を持たない古いスマートフォンや IoT 機器で TLS 1.3 通信をすると、AES-GCM と ChaCha20-Poly1305 のどちらが選ばれやすいか? その理由は?
ChaCha20-Poly1305 が選ばれる。 AES は本来テーブル参照と GF(2^8) 乗算が中心で、専用ハードウェア(AES-NI / ARMv8 Crypto Extension)があると爆速だが、汎用 CPU だけだと数倍〜10倍遅くなり、サイドチャネル攻撃にも晒されやすい。
ChaCha20 は加算・回転・XOR(ARX)だけで構成されているので、普通の CPU 命令だけで定時間実行・高速。だから「AES-NI なし環境では ChaCha20」という TLS 1.3 のサイファースイート選択ロジックが正解になる。
これが Google Chrome が早期から ChaCha20-Poly1305 をサポートし、ARM Android / 古い x86 で優先的に提案する設計理由。RFC 7905(2016)で TLS への正式追加。
図の見方:1 ラウンドは 非線形(SubBytes)→ 拡散(ShiftRows + MixColumns)→ 鍵注入(AddRoundKey) の流れ。これを 10〜14 回繰り返すことで、平文の 1 ビット変化が暗号文の 約半分のビット をランダムに変える ─ シャノンの「拡散と混合」の原理が体現されています。
S-box は 8 bit → 8 bit の固定置換テーブル(256 エントリ)。中身は GF(2^8)(2^8 個の元を持つ有限体)上での 逆元計算 + アフィン変換 という数学的な構造で、線形攻撃・差分攻撃に耐えるように設計されています。MixColumns も GF(2^8) の演算で、これらが「ただランダムな置換」ではなく 数学的に裏付けられた構造 を持つことが、AES の長期的な安全性の根拠になっています。
| 鍵長 | ラウンド数 | 用途 |
|---|---|---|
| AES-128 | 10 | 大半の用途で十分(TLS の標準) |
| AES-192 | 12 | 中間。あまり使われない |
| AES-256 | 14 | 政府機密・量子計算耐性を考慮する場合 |
AES-128 が破られた事例は 2024 年現在 実用的にはない(2^128 通りの全数探索は宇宙の年齢を超える時間がかかる)。AES-256 は「念のため強い方を」という選択肢で、性能差はほとんど無視できる。
確認: AES のラウンド操作のうち、非線形性(代数的に解けない性質) を担っているのはどれか。
正解:D。SubBytes は GF(2^8) 上の逆元計算をベースにした非線形変換で、これがあるから AES は単純な行列計算では解けない。他の 3 操作はすべて線形(行列・XOR)で、SubBytes がなければ全体が線形変換になり、ガウス消去で鍵が復元できてしまう。差分解析・線形解析への耐性も SubBytes の S-Box 設計に依存。
ECB は最も単純なモード:平文を 16 B ずつに分け、それぞれを独立に AES で暗号化するだけ。問題は 「同じ平文ブロックは常に同じ暗号文ブロックになる」こと。画像を ECB で暗号化すると、元画像のパターンが暗号文にそのまま透けて見える ─ これが有名な「ECB ペンギン」現象。
図の見方:ECB は同じ入力 → 同じ出力なので、平文のパターンがそのまま見える。CBC は 「前のブロックの暗号文と XOR してから暗号化」 するので、同じ平文ブロックでも前後関係で結果が変わり、パターンが崩れる。最初のブロックだけは前がないので IV(initialization vector) を使う。
CBC は前のブロックを待つので 逐次的。大ファイルの暗号化や復号は並列化できません。CTR モードはこれを解決:カウンタ(0, 1, 2, 3, ...)を AES で暗号化して鍵ストリームを生成し、それを平文と XOR。各ブロックは独立に計算できるので並列化可能。
CTR モードの動作:
nonce | counter:0 → AES(K) → KS_0 → C_0 = P_0 ⊕ KS_0
nonce | counter:1 → AES(K) → KS_1 → C_1 = P_1 ⊕ KS_1
nonce | counter:2 → AES(K) → KS_2 → C_2 = P_2 ⊕ KS_2
...
並列処理: 全 KS_i を独立に計算可能
復号も同じ操作: P_i = C_i ⊕ KS_i
CTR の 大原則:同じ鍵で同じ nonce を 絶対に再利用してはいけない。再利用すると、2 つの暗号文を XOR するだけで両方の平文が漏れる ─ これは前回 第35回 で見た OTP の two-time pad 攻撃 とまったく同じ原理(C1⊕C2 = P1⊕P2 で鍵が消える)。CTR / GCM が事実上「鍵 + nonce で生成した擬似乱数列で OTP する」構造である以上、OTP の鍵再利用が致命的なのと同じ理由で、nonce の再利用も致命的になります。
| モード | 並列化 | パターン漏洩 | 認証 | 現代の評価 |
|---|---|---|---|---|
| ECB | ○ | あり(致命的) | なし | 使用禁止 |
| CBC | 暗号化:× 復号:○ | なし | なし | レガシー TLS、徐々に廃止 |
| CTR | ○ | なし | なし(別途必要) | GCM の中で使用 |
| GCM | ○ | なし | あり(GHASH) | 現代の標準(TLS 1.3) |
Q. ECB モードで「同じ平文ブロック → 同じ暗号文ブロック」になる、という性質がなぜ実用上致命的なのか? Linux のペンギン画像(Tux)を ECB で暗号化したらどう見えるか想像せよ。
画像のピクセル並びはパターンが多く、同じ色が連続する領域(背景や目など)は同じバイト列になる。ECB で暗号化するとそれらが 同じ暗号文ブロックに変換される ため、暗号文の状態でも 元画像の輪郭がはっきり残る(検索すると「ECB Tux」で有名な比較画像が出てくる)。
つまり ECB は「ブロック単位で独立に暗号化」するため、ブロック間の関係性(平文の同一性)を そのまま暗号文に漏らす。これが ECB が現代では使ってはいけないモードと言われる本質的理由。CBC/CTR/GCM は前のブロックや IV/nonce を混ぜることで、同じ平文でも異なる暗号文になるよう設計されている。
AEAD の "AD" は 「暗号化はしないが認証はしたい」追加データ。例えば TLS の場合、ヘッダ部(レコードタイプ・バージョン・長さ)は平文のまま 流す必要があるが、改ざんされたら困る。これを AD として渡すことで、暗号文と一緒に認証タグの計算に含められる。
AEAD 暗号化のイメージ:
plaintext = "Hello"
AD = "from=alice&to=bob" ← 暗号化しない、でも改ざんされたら検出
key = (32 byte の AES-256 鍵)
nonce = (96 bit のユニーク値、毎回変える)
↓ AES-GCM
ciphertext = (5 バイトの暗号文)
tag = (16 バイトの認証タグ)
復号側:
AES-GCM_decrypt(ciphertext, tag, key, nonce, AD)
→ plaintext または認証エラー
・暗号文 ciphertext が 1 ビットでも書き換わっていたらエラー
・AD が変わっていたらエラー
・nonce が間違っていたらエラー
つながる知識: TLS 1.3 では 暗号アルゴリズムは AEAD のみ に制限されました。それ以前(TLS 1.2 以下)は CBC + HMAC の「Encrypt-then-MAC」方式も使えましたが、パディングオラクル攻撃 など歴史的に多くの脆弱性を生んでいました。AEAD はこれらを構造的に排除する設計です。
確認: AEAD(AES-GCM など)が単純な「暗号化 → MAC を別計算」方式に対して持つ 本質的な利点 はどれか。
正解:C。CBC + HMAC を別々に組むと「Encrypt-then-MAC か MAC-then-Encrypt か」「順序を間違えるとどう脆弱になるか」を実装者が判断する必要があり、ここから パディングオラクル攻撃(Lucky13 等)が多発した。AEAD は API レベルで encrypt(key, nonce, plaintext, aad) → (ciphertext, tag) と一体化しているため、誤実装の余地が消える。これが「正しい設計は誤って使えないようにする」という暗号工学の典型例。
# テストファイル作成
echo "secret message from alice" > plain.txt
# 鍵と IV(nonce)をランダム生成
KEY=$(openssl rand -hex 32) # 256 bit 鍵 (64 hex chars)
IV=$(openssl rand -hex 12) # 96 bit nonce (24 hex chars)
echo "KEY=$KEY"
echo "IV=$IV"
# 暗号化(AES-256-GCM)
openssl enc -aes-256-gcm \
-K "$KEY" -iv "$IV" \
-in plain.txt -out cipher.bin
# 暗号文を hex で確認
xxd cipher.bin
# 復号
openssl enc -aes-256-gcm -d \
-K "$KEY" -iv "$IV" \
-in cipher.bin -out decrypted.txt
cat decrypted.txt
# → secret message from alice
# パスワードから鍵を派生(PBKDF2)
openssl enc -aes-256-cbc -salt -pbkdf2 \
-in plain.txt -out cipher.enc
# パスワード入力プロンプトが出る
# 復号
openssl enc -aes-256-cbc -d -pbkdf2 \
-in cipher.enc -out decrypted.txt
パスワードから直接 AES 鍵を作るのは 弱い(辞書攻撃に弱い)ため、-pbkdf2 オプションで PBKDF2 という鍵導出関数を通します。これにより総当たり攻撃のコストが意図的に高くなります。
# pip install cryptography
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
nonce = os.urandom(12)
aesgcm = AESGCM(key)
plaintext = b"secret message"
aad = b"to=bob&from=alice"
# 暗号化
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
# 復号(AAD が違うと InvalidTag 例外が飛ぶ)
recovered = aesgcm.decrypt(nonce, ciphertext, aad)
print(recovered) # → b"secret message"
| 暗号種別 | 古典での安全性 | 量子計算下 | 対策 |
|---|---|---|---|
| AES-128 | 安全 | 実効 64 bit に低下 | AES-256 に切り替え |
| AES-256 | 安全 | 実効 128 bit ─ 安全 | そのまま使える |
| RSA-2048 | 安全 | Shor で 1 日で破られる | PQC(Kyber 等)へ移行(第37回) |
| ECDH-P256 | 安全 | Shor で完全破綻 | PQC へ移行 |
つながる知識: 量子計算機が「いつ来るか」は不明ですが、攻撃者が 今のうちに暗号文を保存しておき、将来の量子計算機で復号する(Harvest Now, Decrypt Later)というシナリオが現実視されています。長期機密(国防・医療記録)では今すぐ AES-256 + PQC への移行が始まっています。詳細は次回の公開鍵暗号で。
確認: Grover アルゴリズムが量子計算機で動いた場合、AES-128 と AES-256 の安全性はどう変わるか。
正解:B。Grover アルゴリズムは無構造探索を √N 倍に高速化する。鍵総数 2^k の全数探索が 2^(k/2) 回の量子操作で済むため、AES-128 は事実上 64 bit 強度(現代の古典計算機でも厳しい)、AES-256 は 128 bit 強度(依然として実用安全)になる。だから 共通鍵は鍵長を倍にすれば量子耐性が確保できる。一方 RSA・ECDSA は Shor アルゴリズムで多項式時間に潰されるので「鍵を伸ばせば済む」世界ではない ─ これが PQC が公開鍵だけ必要な理由。
| 用語 | 1行説明 |
|---|---|
| 共通鍵暗号 / 対称暗号 | 同じ鍵で暗号化・復号。高速だが事前共有が必要 |
| AES | FIPS 197 (2001)。128 bit ブロック、10/12/14 ラウンド |
| ChaCha20 | ストリーム暗号。ソフト実装が速くモバイル向き |
| S-box | AES の SubBytes で使う 8→8 bit 非線形置換テーブル |
| 動作モード(mode of operation) | ブロック暗号で長いデータを処理する繋ぎ方 |
| ECB | 独立処理。同じ平文 → 同じ暗号文(使用禁止) |
| CBC | 前ブロックの暗号文と XOR してから暗号化(直列) |
| CTR | カウンタを暗号化して鍵ストリーム生成。並列可 |
| GCM | CTR + Galois 認証タグ。AEAD の代表例 |
| AEAD | 暗号化と認証を一体化。AES-GCM・ChaCha20-Poly1305 |
| nonce / IV | 毎回変える初期値。再利用は致命的 |
| AD(Associated Data) | 暗号化はしないが認証に含める追加データ |
| PBKDF2 | パスワードから鍵を導出する関数(辞書攻撃を遅くする) |
| Grover アルゴリズム | 量子計算で全数探索を √N に高速化(対策:鍵長 2 倍) |