「Nonceの使い回し」は死への招待状:AES-GCMの脆弱性が招く破滅
現場でコードレビューをしていると、未だに「暗号化しておけばとりあえず安心」という甘い考えで実装されたシステムに出くわす。特に、現代の暗号化のデファクトスタンダードである AES-GCM を使いながら、その心臓部である Nonce(ナンス) の管理を疎かにしているケースは、即座に修正が必要な「時限爆弾」だ。
今日は、なぜNonceの再利用が致命的なのか、そして現場でどう防ぐべきかを、実務的な観点から叩き込む。
—
1. なぜNonceの再利用が「ゲームオーバー」なのか
AES-GCM(Galois/Counter Mode)は、暗号化と同時にデータの改ざん検知(認証)も行える非常に優れたモードだ。しかし、これには鉄の掟がある。
「同じ鍵(Key)と、同じNonceの組み合わせを二度と使ってはならない」
もしNonceを再利用すると、何が起きるか。AES-GCMは内部的に「カウンタモード(CTR)」を利用している。同じ鍵とNonceで暗号化を行うと、生成される「キー・ストリーム(暗号化のための乱数系列)」が完全に一致してしまう。
攻撃者が狙う盲点:XORの魔法
暗号文を C1 = P1 XOR Stream, C2 = P2 XOR Stream とすると、再利用された暗号文同士をXOR演算するだけで、共通の Stream が打ち消し合う。
C1 XOR C2 = (P1 XOR Stream) XOR (P2 XOR Stream) = P1 XOR P2
こうなると、平文の一部が既知であれば残りの平文がすべて露呈する。さらに最悪なのは、認証タグ(GHASH)の偽造だ。Nonceが重複すると、認証鍵(Hash Key)を特定するための数式が解けてしまい、攻撃者はあなたのシステムが発行したかのように見える「偽の暗号文」を自由自在に生成できてしまう。
—
2. 実務で「絶対にやってはいけない」実装例
以下のような実装は、今すぐリファクタリング対象だ。
# 【ダメな例】固定のNonceやカウンターを使っている
from Crypto.Cipher import AES
key = b'0123456789ABCDEF0123456789ABCDEF'
nonce = b'fixed_nonce' # 絶対にやってはいけない:固定値や定数の利用
def encrypt(data):
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
return cipher.encrypt(data)
このコードをデプロイした瞬間、攻撃者は「同じ鍵とNonce」で暗号化された2つの通信をキャプチャするだけで、あなたの暗号基盤を無力化できる。
—
3. 防御の鉄則:セキュアな実装サンプル
では、どうすればいいか。答えはシンプルだ。「暗号化のたびに、暗号論的擬似乱数生成器(CSPRNG)で生成したユニークなNonceを使う」こと。Nonce自体は秘密にする必要はないため、暗号文の先頭に付与して保存・送信するのが一般的だ。
Pythonでの推奨実装 (PyCryptodome使用)
import os
from Crypto.Cipher import AES
import base64
def secure_encrypt(key, plaintext):
# 1. 毎回異なる96ビット(12バイト)のNonceを生成
nonce = os.urandom(12)
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
# 2. 暗号化と同時に認証タグを生成
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8'))
# 3. Nonce + Tag + Ciphertext を結合して保存・送信する
# 復号側でNonceが必要なため、一緒に送るのが作法
return base64.b64encode(nonce + tag + ciphertext)
def secure_decrypt(key, combined_data):
data = base64.b64decode(combined_data)
nonce, tag, ciphertext = data[:12], data[12:28], data[28:]
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
# 復号と同時にタグを検証。一致しなければValueErrorが発生
return cipher.decrypt_and_verify(ciphertext, tag).decode('utf-8')
Node.js (Web/Server) での実装
const crypto = require('crypto');
function encrypt(text, key) {
const iv = crypto.randomBytes(12); // Nonce (IV) は毎回ランダムに
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag(); // 認証タグを取得
// iv + tag + ciphertext を返却
return iv.toString('hex') + ':' + tag.toString('hex') + ':' + encrypted;
}
—
4. 現場のチーフとしてのアドバイス
実装時に以下の3点を必ずチェックリストに入れてほしい。
1. Nonceの使い回し厳禁: どんな状況でも、同じ鍵で同じNonceを二度使うな。もしNonceのビット長が短く、衝突が怖いなら、鍵自体を定期的にローテーションする運用を組み込め。
2. ライブラリの選定: 自前で暗号アルゴリズムを実装しようとするのは素人のすることだ。AES-GCM のような枯れた標準ライブラリを使い、設定項目を正しく選べ。
3. タグの検証を忘れるな: decrypt するだけでなく、必ず verify(認証タグの照合)を行え。タグを検証しないGCMは、ただのCTRモードであり、改ざん耐性はゼロだ。
暗号化は「魔法の杖」ではない。正しい手順を踏んで初めて、それは「強固な盾」になる。君たちが書くコードが、明日のインシデントを防ぐ最後の砦だ。手を抜かず、論理的に実装してくれ。
コメント