AESとECB・CBCモード
暗号文の各ブロックが何に依存し、CBCだけでは改変を検出できない理由を説明します。
説明
AESは16バイトのブロックを暗号化する共通鍵暗号です。鍵長とブロック長は同じ概念ではありません。ここでは128ビットの公開試料鍵を使い、複数ブロックの結び付け方が暗号文へどう表れるかを調べます。
同じ16バイトを二つ並べてECBとCBCで暗号化します。復号、IVの変更、端数の埋め方、暗号文の改変を順に試し、秘匿と改変検出に必要な処理を確認します。
暗号文の各ブロックが何に依存し、CBCだけでは改変を検出できない理由を説明します。
1同じ16バイトを二つ並べる
鍵は16バイト、平文は32バイトです。0123456789ABCDEFはASCIIで16バイトになります。
from sc05 import *
p =同一ブロックを二つ用意する b"0123456789ABCDEF" * 2
print鍵と平文の長さを確認する(len(KEY),len(p),p[:16] == p[16:])二つのブロックが同一なので、暗号化の連鎖の有無を同じ入力で比較できます。
2ECBで各ブロックを暗号化する
ECBは同じ鍵で各ブロックを独立に処理します。
ecb = encrypt(p,modes.ECB())AES-ECBで暗号化する
print(ecb[:16].hex())
print(ecb[16:].hex())
print(ecb[:16] == ecb[16:]二つの暗号文ブロックを比較する)同じ平文ブロックから同じ暗号文ブロックが出ます。本文が読めなくても、同じ部分の繰返しが分かります。
3CBCで前の暗号文へ連鎖させる
最初はIVと平文をXORし、次は直前の暗号文と平文をXORしてAESへ渡します。IVはこの比較用に固定した公開値です。
cbc = encrypt(p,modes.CBC(IV))IVを指定してCBCで暗号化する
print(cbc[:16].hex())
print(cbc[16:].hex())
print(cbc[:16] == cbc[16:]連鎖した二つの結果を比較する)平文が同じでも、AESへ入力される値が異なるので暗号文は一致しません。運用上のCBCでは予測できないIVを用意し、IVの完全性も保護します。
4最初のブロックを式で検算する
CBCの最初の暗号文はAES_K(P1 XOR IV)です。
first = encrypt(xor(p[:16],IV)平文とIVのXORを求める,modes.ECB())
print(first.hex(), first == cbc[:16]CBCの先頭ブロックと照合する)CBCの先頭と一致します。モードの働きとAES一回の変換を分けて確認できました。
5復号して元の平文へ戻す
CBC復号には同じ鍵とIVが必要です。
print(decrypt(cbc,modes.CBC(IV))同じIVで復号する)
print(decrypt(cbc,modes.CBC(IV)) == p)元の32バイトに戻りました。パディングを付けていないため、ここではその除去もありません。
6端数をPKCS#7で埋める
3バイトのabcを16バイトへ埋める場合、不足13バイトのそれぞれに0x0dを入れます。
short = pad(b"abc")不足分をパディングする
print(short.hex(),len(short))
print(unpad(復号後のパディングを除くdecrypt(encrypt(short,modes.CBC(IV)),modes.CBC(IV))))復号後に13個の0x0dを除くとabcへ戻ります。元が16の倍数でも、PKCS#7では追加の16バイトを付けます。
7IVの1ビットを変更する
暗号文を保ったままIVの先頭1ビットを反転します。
changed = decrypt(cbc,modes.CBC(flip(IV,0)IVの先頭バイトの最下位ビットを反転する))
print(changed)
print(xor(changed[:16],p[:16])変化した平文ビットを表示する.hex())
print(changed[16:] == p[16:])平文の先頭だけ同じ1ビットが反転し、2ブロック目は同じです。復号できることは改変されていない証拠にはなりません。
まとめ
AESのブロックは16バイトです。ECBは同じ入力ブロックを同じ暗号文にします。
CBCではIVと直前の暗号文が計算に加わります。
CBCの復号だけでは改変の有無を検証できません。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sc05 import *
p = b"0123456789ABCDEF" * 2
print(len(KEY),len(p),p[:16] == p[16:])16 32 True
ecb = encrypt(p,modes.ECB())
print(ecb[:16].hex())
print(ecb[16:].hex())
print(ecb[:16] == ecb[16:])0757816030cb78c3b96e6fe53e595df2 0757816030cb78c3b96e6fe53e595df2 True
cbc = encrypt(p,modes.CBC(IV))
print(cbc[:16].hex())
print(cbc[16:].hex())
print(cbc[:16] == cbc[16:])159e1dbe75bfe5c45c3cd075e4cc7831 b2703cef8d2a2629557f75e60df50080 False
first = encrypt(xor(p[:16],IV),modes.ECB())
print(first.hex(), first == cbc[:16])159e1dbe75bfe5c45c3cd075e4cc7831 True
print(decrypt(cbc,modes.CBC(IV)))
print(decrypt(cbc,modes.CBC(IV)) == p)b'0123456789ABCDEF0123456789ABCDEF' True
short = pad(b"abc")
print(short.hex(),len(short))
print(unpad(decrypt(encrypt(short,modes.CBC(IV)),modes.CBC(IV))))6162630d0d0d0d0d0d0d0d0d0d0d0d0d 16 b'abc'
changed = decrypt(cbc,modes.CBC(flip(IV,0)))
print(changed)
print(xor(changed[:16],p[:16]).hex())
print(changed[16:] == p[16:])b'1123456789ABCDEF0123456789ABCDEF' 01000000000000000000000000000000 True
演習完了