2025, Oct 05 01:16

AES-CBC: почему важен IV и как избежать UnicodeDecodeError в Python

Разбираем, почему при AES-CBC нужен тот же IV при расшифровке, как сохранить IV вместе с шифртекстом и избежать UnicodeDecodeError в Python. Пример кода.

При шифровании AES в режиме CBC легко споткнуться о один тонкий, но критически важный момент: вектор инициализации (IV). Если при расшифровке используется IV, отличный от того, что был при шифровании, восстановленные байты окажутся неверными, а любая попытка интерпретировать их как текст UTF‑8 может завершиться сбоем с UnicodeDecodeError. Ниже — минимальный пример, который демонстрирует проблему и простой способ её устранить.

Постановка задачи

Следующий фрагмент Python выполняет шифрование AES-CBC, записывает шифртекст в файл, затем читает и расшифровывает его. На первый взгляд всё в порядке, но позже расшифровка не даёт корректный текст.

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Hash import SHA256 as H256
def digest_hex(buf):
    h = H256.new(buf)
    return h.hexdigest()
secret = '1234'
print('Key length:', len(secret))
blk_sz = 16
msg = "The quick brown fox jumped over the lazy dog"
print('Plain text length:', len(msg))
kbuf = pad(secret.encode(), blk_sz)
print('akey length', len(kbuf))
msg_bytes = msg.encode()
enc_ctx = AES.new(kbuf, AES.MODE_CBC)
padded = pad(msg_bytes, blk_sz)
print('sha payload:', digest_hex(padded))
ct = enc_ctx.encrypt(padded)
print('Encrypted sha:', digest_hex(ct))
with open('data.bin', 'wb') as fh:
    fh.write(ct)
# ---- позже, расшифровка ----
dec_ctx = AES.new(kbuf, AES.MODE_CBC)
with open('data.bin', 'rb') as fh:
    raw = fh.read()
print('file contents sha:', digest_hex(raw))
pt = dec_ctx.decrypt(raw)
print('decrypted sha:', digest_hex(pt))
clear = unpad(pt, blk_sz)
print('Plain text:', clear.decode())

Шаг расшифровки выдаёт нетекстовые байты, и декодирование падает с ошибкой наподобие:

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa8 in position 1: invalid start byte

Что на самом деле происходит

В режиме CBC шифрование и расшифровка обязаны использовать один и тот же IV. Если в фазе расшифровки взять другой IV, первый блок преобразуется неверно, и ошибка «расползается» по расшифрованному выводу. Хотя код успешно выполняет вызовы шифрования и расшифровки, полученные байты — это не исходный открытый текст, поэтому декодирование в UTF‑8 заканчивается ошибкой.

Как исправить проблему

Решение — сохранять IV вместе с шифртекстом и передавать этот же IV в расшифровщик. Простой приём — при записи на диск дописывать IV перед шифртекстом, а при чтении брать первые 16 байт как IV для AES-CBC.

Шифрование с сохранением IV:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Hash import SHA256 as H256
def digest_hex(buf):
    h = H256.new(buf)
    return h.hexdigest()
secret = '1234'
blk_sz = 16
msg = "The quick brown fox jumped over the lazy dog"
kbuf = pad(secret.encode(), blk_sz)
enc_ctx = AES.new(kbuf, AES.MODE_CBC)
padded = pad(msg.encode(), blk_sz)
ct = enc_ctx.encrypt(padded)
with open('data.bin', 'wb') as fh:
    fh.write(enc_ctx.iv + ct)

Расшифровка с использованием сохранённого IV:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Hash import SHA256 as H256
def digest_hex(buf):
    h = H256.new(buf)
    return h.hexdigest()
secret = '1234'
blk_sz = 16
# kbuf должен быть сформирован точно так же, как при шифровании
from Crypto.Util.Padding import pad
kbuf = pad(secret.encode(), blk_sz)
with open('data.bin', 'rb') as fh:
    payload = fh.read()
iv, raw = payload[:16], payload[16:]
dec_ctx = AES.new(kbuf, AES.MODE_CBC, iv=iv)
pt = dec_ctx.decrypt(raw)
clear = unpad(pt, blk_sz)
print(clear.decode())

Почему это важно

Без правильного IV вы не получите исходный открытый текст, даже если сами вызовы API завершаются без исключений. Это приводит к запутанным симптомам вроде несовпадающих хешей и ошибок при декодировании. Сохранение и повторное использование того же IV при расшифровке делает процесс симметричным и предотвращает повреждение результата.

Что запомнить

При использовании AES в режиме CBC всегда сохраняйте IV, сгенерированный при шифровании, и передавайте ровно этот IV при расшифровке. Практичный подход — объединять IV и шифртекст в один бинарный блок и восстанавливать их в том же порядке при чтении. Соблюдение этой дисциплины избавляет от трудноотлавливаемых ошибок вроде UnicodeDecodeError на финальном этапе и гарантирует совпадение расшифрованных байт с исходным сообщением.

Материал основан на вопросе на StackOverflow от deostroll и ответе 0ro2.