一覧SC-05-1 · 共通鍵暗号

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 / 公開試料と固定時刻

1
from sc05 import * p = b"0123456789ABCDEF" * 2 print(len(KEY),len(p),p[:16] == p[16:])
16 32 True
2
ecb = encrypt(p,modes.ECB()) print(ecb[:16].hex()) print(ecb[16:].hex()) print(ecb[:16] == ecb[16:])
0757816030cb78c3b96e6fe53e595df2
0757816030cb78c3b96e6fe53e595df2
True
3
cbc = encrypt(p,modes.CBC(IV)) print(cbc[:16].hex()) print(cbc[16:].hex()) print(cbc[:16] == cbc[16:])
159e1dbe75bfe5c45c3cd075e4cc7831
b2703cef8d2a2629557f75e60df50080
False
4
first = encrypt(xor(p[:16],IV),modes.ECB()) print(first.hex(), first == cbc[:16])
159e1dbe75bfe5c45c3cd075e4cc7831 True
5
print(decrypt(cbc,modes.CBC(IV))) print(decrypt(cbc,modes.CBC(IV)) == p)
b'0123456789ABCDEF0123456789ABCDEF'
True
6
short = pad(b"abc") print(short.hex(),len(short)) print(unpad(decrypt(encrypt(short,modes.CBC(IV)),modes.CBC(IV))))
6162630d0d0d0d0d0d0d0d0d0d0d0d0d 16
b'abc'
7
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

演習完了