CTRモードとカウンターの再利用
nonceや初期カウンターの重複を避ける理由を、16バイトのXOR計算で説明します。
説明
CTRはカウンターブロックをAESで暗号化し、その出力と平文をXORします。復号も同じXORです。同じ鍵で同じカウンター列を再利用すると、二つの平文の関係が暗号文から分かってしまいます。
公開試料で鍵ストリームを取り出し、暗号文から元の文を復元します。次に同じカウンターで別の文を暗号化し、片方の平文を知っているともう片方も復元できることを確かめます。
nonceや初期カウンターの重複を避ける理由を、16バイトのXOR計算で説明します。
1鍵とカウンターを設定する
ここでは128ビット全体をカウンターブロックとして扱います。nonceとブロック番号の分割方法はプロトコルにより異なります。
from sc05 import *
counter =最初のカウンターブロックを指定する bytes.fromhex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff")
p1 =16バイトの平文を用意する b"amount=00000100!"
print(len(p1),counter.hex())平文は16バイトで、カウンターを一回暗号化した出力を使います。
2AES出力を鍵ストリームにする
CTRで16個の0を暗号化すると、XORの相手である鍵ストリームそのものが出ます。
stream = encrypt(bytes(16),modes.CTR(counter))最初の鍵ストリームを取得する
print(stream.hex())
print(stream == encrypt(counter,modes.ECB())AES一回の結果と照合する)カウンターをAESへ一回入力した結果と一致します。鍵ストリームは平文の内容に依存しません。
3XORによる暗号化と復号を試す
同じ値で2回XORすると元へ戻ります。
c1 = encrypt(p1,modes.CTR(counter))平文をCTRで暗号化する
print(c1.hex())
print(c1 == xor(p1,stream))
print(xor(c1,stream)鍵ストリームで平文へ戻す)暗号文は平文と鍵ストリームのXORです。鍵ストリームを知られれば暗号文から平文を取り出せます。
4別の文へ同じカウンターを使う
金額を100から900へ変更し、同じ鍵と初期カウンターを再利用してしまった場合です。
p2 = b"amount=00000900!"
c2 = encrypt(p2,modes.CTR(counter))別の平文でカウンターを再利用する
print(xor(c1,c2).hex())
print(xor(c1,c2) == xor(p1,p2)暗号文と平文の関係を照合する)暗号文同士のXORで同じ鍵ストリームが打ち消されます。残るのは平文同士のXORです。
5既知の平文からもう一方を復元する
片方の平文p1と二つの暗号文だけで計算します。
recovered = xor(xor(c1,c2),p1)既知の平文をXORして他方を復元する
print(recovered)
print(recovered == p2)秘密鍵を入力せずにp2を復元できました。同じ鍵ストリームを再使用したことが原因です。
6重複しないカウンター列を使う
この例では1ブロックだけ使用したので、次のブロック値はまだ使っていません。複数ブロックなら使用範囲全体の重複を避けます。
next_counter =未使用のカウンターブロックを用意する (int.from_bytes(counter,"big")+1).to_bytes(16,"big")
c3 = encrypt(p2,modes.CTR(next_counter))
print(next_counter.hex())
print(xor(xor(c1,c3),p1) == p2前の復元式が成立するか調べる)初期値を一つずらしただけでは長いメッセージ同士の重複を防げない点に注意が必要です。この1ブロック例では使用範囲が重なりません。
7暗号文のビット反転は平文へ伝わる
CTRは改変検出機能を持ちません。暗号文の先頭を1ビット反転して復号します。
tampered = flip(c1,0)暗号文の1ビットを変更する
print(decrypt(tampered,modes.CTR(counter))変更後の平文を確認する)
print(xor(decrypt(tampered,modes.CTR(counter)),p1).hex())平文の同じ位置だけが反転します。nonce管理に加え、認証付き暗号などで完全性を保護する必要があります。
まとめ
CTRはAESで生成した鍵ストリームと平文をXORします。
同じ鍵とカウンター列の再利用は、平文同士の関係を露出させます。
カウンターの重複防止と改変検出は別の要件です。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sc05 import *
counter = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff")
p1 = b"amount=00000100!"
print(len(p1),counter.hex())16 f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
stream = encrypt(bytes(16),modes.CTR(counter))
print(stream.hex())
print(stream == encrypt(counter,modes.ECB()))ec8cdf7398607cb0f2d21675ea9ea1e4 True
c1 = encrypt(p1,modes.CTR(counter))
print(c1.hex())
print(c1 == xor(p1,stream))
print(xor(c1,stream))8de1b006f6144180c2e22645dbae91c5 True b'amount=00000100!'
p2 = b"amount=00000900!"
c2 = encrypt(p2,modes.CTR(counter))
print(xor(c1,c2).hex())
print(xor(c1,c2) == xor(p1,p2))00000000000000000000000008000000 True
recovered = xor(xor(c1,c2),p1)
print(recovered)
print(recovered == p2)b'amount=00000900!' True
next_counter = (int.from_bytes(counter,"big")+1).to_bytes(16,"big")
c3 = encrypt(p2,modes.CTR(next_counter))
print(next_counter.hex())
print(xor(xor(c1,c3),p1) == p2)f0f1f2f3f4f5f6f7f8f9fafbfcfdff00 False
tampered = flip(c1,0)
print(decrypt(tampered,modes.CTR(counter)))
print(xor(decrypt(tampered,modes.CTR(counter)),p1).hex())b'`mount=00000100!' 01000000000000000000000000000000
演習完了