在我的一個專案中 ncp-downloader
當時的我沒有想太多,甚至沒有特別去研究 IV 的作用或查看 HLS 的文件,就隨便在網路上抄了一段程式碼來做解密,猜猜看他是怎麼做的?
AES.new(message, AES.MODE_CBC, get_random_bytes(16))
你沒看錯,我也不知道當時是怎麼想的,不疑有他就直接用隨機的 IV 解密了。
一直到最近,我終於想起了這件事,於是就認真研究了一下 HLS 文件中關於 RFC 8216
CBC is restarted on each segment boundary, using either the Initialization Vector (IV) attribute value or the Media Sequence Number as the IV
An EXT-X-KEY tag with a KEYFORMAT of “identity” that does not have an IV attribute indicates that the Media Sequence Number is to be used as the IV when decrypting a Media Segment, by putting its big-endian binary representation into a 16-octet (128-bit) buffer and padding (on the left) with zeros.
白話文就是說,如果他沒提供 IV 的話,就用他的序號轉成 16 bytes 的 buffer 並在左邊補 0 來當 IV。
於是我就更好奇了:為什麼我過去用隨機的 IV 解密出來的影片還是能正常播放,而且沒有什麼明顯的失真或是錯誤呢?於是我就決定來做個小實驗。
事前準備一下
先安裝 bitarray (用於轉換成二進制,方便我們觀察) 和 pycryptodome (用於 AES 加解密)。
pip install bitarraypip install pycryptodome
然後 import 一下我們需要的東西。
import osfrom bitstring import BitArrayfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadfrom Crypto.Random import get_random_bytes
最後,為了減少實驗的變因,我們先決定好 key、IV (加密、錯位一點、隨機),之後就都使用這組固定的 key 和 IV 來進行實驗。
key = os.urandom(32)
iv_encrypt = (0).to_bytes(16, 'big')
iv_decrypt = (1).to_bytes(16, 'big')
iv_decrypt_random = get_random_bytes(16)
實驗一:原始資料很短
message = b"Hello World!"print("size:", len(BitArray(message)))
size: 96
加密
cipher = AES.new(key, AES.MODE_CBC, iv=iv_encrypt)padded = pad(message, AES.block_size)encrypted = cipher.encrypt(padded)
print("encrypted:", encrypted)
encrypted: b’1\x9b\x0c\xca\xbb<\xb2]\x16w\xcb\n\x9d\x83kg’
用錯位一點的 IV 解密
cipher = AES.new(key, AES.MODE_CBC, iv=iv_decrypt)decrypted = cipher.decrypt(encrypted)
print("decrypted:", decrypted)
decrypted: b’Hello World!\x04\x04\x04\x05’
original_bits = BitArray(padded)decrypted_bits = BitArray(decrypted)
diff = original_bits ^ decrypted_bits
print("diff:", diff)print("distance:", diff.count(True))
diff: 0x00000000000000000000000000000001
distance: 1
解密後資料的後面 \x04...
是 padding 的部分,正常來說全部都會是一樣的數字,所以這裡我們觀察到的是:IV 從 0->1,導致了最最終的解密結果也差了 1。不過錯誤的位置在 padding 上,所以沒有影響到原始資料(要注意的是,如果 padding 的部分是錯的話,就沒辦法 unpad)。
用隨機的 IV 解密
cipher = AES.new(key, AES.MODE_CBC, iv=iv_decrypt_random)decrypted_random_iv = cipher.decrypt(encrypted)
print("decrypted (random):", decrypted_random_iv)
decrypted (random): b’\x88\xeb\x85\x13\x10\xbf\x9d\xdb[\xc3\xb4\xa5\xfc\xe0\xa8\x98’
original_bits = BitArray(padded)decrypted_random_iv_bits = BitArray(decrypted_random_iv)
diff = original_bits ^ decrypted_random_iv_bits
print("diff:", diff)print("distance:", diff.count(True))
diff: 0xc08ee97f7f9fcab429afd084f8e4ac9c
distance: 70
這裡我們觀察到的是:IV 從 0->隨機,他的錯位變得很大,原始資料就 96 位元,解密的結果就差了 70 位元。解密的資料已經完全失真了。
實驗二:原始資料很長
當然,雖然說串流影片是將影片分割成很多小段,但是也不至於只有 96 位元,所以我們來用一個長一點的資料做實驗看看吧。
message = b"Hello World!" * 1000print("size:", len(BitArray(message)))
size: 96000
加密
cipher = AES.new(key, AES.MODE_CBC, iv=iv_encrypt)padded = pad(message, AES.block_size)encrypted = cipher.encrypt(padded)
print("encrypted:", encrypted)
encrypted: encrypted: b’…’ (省略,不重要而且超長)
用錯位一點的 IV 解密
cipher = AES.new(key, AES.MODE_CBC, iv=iv_decrypt)decrypted = cipher.decrypt(encrypted)
print("decrypted:", decrypted)
decrypted: b’Hello World!Helmo World!Hello World!…Hello World!\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10’
original_bits = BitArray(padded)decrypted_bits = BitArray(decrypted)
diff = original_bits ^ decrypted_bits
print("diff:", diff)print("distance:", diff.count(True))
distance: 1
在開頭後一段距離,我們觀察到了一個發生錯誤的地方,Hello 變成了 Helmo,這是因為 IV 從 0->1,導致了這個錯誤。不過,距離依舊只有 1。
用隨機的 IV 解密
cipher = AES.new(key, AES.MODE_CBC, iv=iv_decrypt_random)decrypted_random_iv = cipher.decrypt(encrypted)
print("decrypt (random):", decrypted_random_iv)
decrypt (random): b’\x88\xeb\x85\x13\x10\xbf\x9d\xdb[\xc3\xb4\xa5\xb0\x81\xc0\xf0o World!…Hello World!\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10’
original_bits = BitArray(padded)decrypted_random_iv_bits = BitArray(decrypted_random_iv)
diff = original_bits ^ decrypted_random_iv_bits
print("diff:", diff)print("distance:", diff.count(True))
distance: 70
這裡我們觀察到的是:IV 從 0->隨機,他的錯位變得很大,錯誤一樣發生在接近開頭的位置,距離與短資料的實驗結果一樣,都是 70。
備註
結論
從實驗結果來看,不論資料的長短,IV 對於解密結果的影響其實都是一樣的,從最後的實驗結果來看,對於一個數 MB 的影片片段來說,其實影響微乎其微,這造就了雖然我過去使用了錯誤的 IV 來解密,但是影片還是能正常播放的原因。(其實雖然說能夠正常播放,但是原本錯誤的解密結果 Windows 是沒辦法讀到影片縮圖的)
最後也附上實驗的 Notebook