首页 > 解决方案 > UnicodeDecodeError:“utf-8”编解码器无法解码位置 65534-65535 中的字节:数据意外结束

问题描述

我想用简单的 AES 加密来加密文件,这是我的 python3 源代码。

import os, random, struct
from Crypto.Cipher import AES

def encrypt_file(key, in_filename, out_filename=None, chunksize=64*1024):
    if not out_filename:
        out_filename = in_filename + '.enc'
    iv = os.urandom(16)
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    filesize = os.path.getsize(in_filename)
    with open(in_filename, 'rb') as infile:
        with open(out_filename, 'wb') as outfile:
            outfile.write(struct.pack('<Q', filesize))
            outfile.write(iv)
            while True:
                chunk = infile.read(chunksize)
                if len(chunk) == 0:
                    break
                elif len(chunk) % 16 != 0:
                    chunk += ' ' * (16 - len(chunk) % 16)
                outfile.write(encryptor.encrypt(chunk.decode('UTF-8','strict')))

它适用于某些文件,遇到一些文件的错误信息,如下所示:

encrypt_file("qwertyqwertyqwer",'/tmp/test1' , out_filename=None, chunksize=64*1024)

没有错误信息,工作正常。

encrypt_file("qwertyqwertyqwer",'/tmp/test2' , out_filename=None, chunksize=64*1024)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in encrypt_file
UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 65534-65535: unexpected end of data

如何修复我的 encrypt_file 功能?

照说t.m.adam,修复

outfile.write(encryptor.encrypt(chunk.decode('UTF-8','strict')))

作为

outfile.write(encryptor.encrypt(chunk))

尝试使用一些文件。

encrypt_file("qwertyqwertyqwer",'/tmp/test' , out_filename=None, chunksize=64*1024)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 16, in encrypt_file
TypeError: can't concat bytes to str

标签: pythonpython-3.xencryption

解决方案


您的代码的主要问题是您使用的是字符串。AES 适用于二进制数据,如果您使用的是 PyCryptodome,则此代码会引发 TypeError:

Object type <class 'str'> cannot be passed to C code

Pycrypto 接受字符串,但在内部将它们编码为字节,因此将字节解码为字符串是没有意义的,因为它将被编码回字节。此外,它使用 ASCII 编码(使用 PyCrypto v2.6.1、Python v2.7 测试),因此,此代码例如:

encryptor.encrypt(u'ψ' * 16)

会引发 UnicodeEncodeError:

File "C:\Python27\lib\site-packages\Crypto\Cipher\blockalgo.py", line 244, in encrypt
    return self._cipher.encrypt(plaintext)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-15

加密或解密数据时应始终使用字节。然后你可以将明文解码为字符串,如果它是文本的话。

下一个问题是您的填充方法。它会生成一个字符串,因此当您尝试将其应用于应为字节的纯文本时,您会收到 TypeError。如果你用字节填充,你可以解决这个问题,

chunk += b' ' * (16 - len(chunk) % 16)

但最好使用 PKCS7 填充(目前您使用的是零填充,但使用空格而不是零字节)。

PyCryptodome 提供填充功能,但您似乎正在使用 PyCrypto。在这种情况下,您可以实现 PKCS7 填充,或者更好地复制 PyCryptodome 的填充函数。

try:
    from Crypto.Util.Padding import pad, unpad
except ImportError:
    from Crypto.Util.py3compat import bchr, bord

    def pad(data_to_pad, block_size):
        padding_len = block_size-len(data_to_pad)%block_size
        padding = bchr(padding_len)*padding_len
        return data_to_pad + padding

    def unpad(padded_data, block_size):
        pdata_len = len(padded_data)
        if pdata_len % block_size:
            raise ValueError("Input data is not padded")
        padding_len = bord(padded_data[-1])
        if padding_len<1 or padding_len>min(block_size, pdata_len):
            raise ValueError("Padding is incorrect.")
        if padded_data[-padding_len:]!=bchr(padding_len)*padding_len:
            raise ValueError("PKCS#7 padding is incorrect.")
        return padded_data[:-padding_len]

和函数被复制pad并修改为仅使用 PKCS7 填充。请注意,当使用 PKCS7 填充时,填充最后一个块很重要,即使它的大小是块大小的倍数,否则您将无法正确取消填充。unpadCrypto.Util.Padding

将这些更改应用于encrypt_file函数,

def encrypt_file(key, in_filename, out_filename=None, chunksize=64*1024):
    if not out_filename:
        out_filename = in_filename + '.enc'
    iv = os.urandom(16)
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    filesize = os.path.getsize(in_filename)
    with open(in_filename, 'rb') as infile:
        with open(out_filename, 'wb') as outfile:
            outfile.write(struct.pack('<Q', filesize))
            outfile.write(iv)
            pos = 0
            while pos < filesize:
                chunk = infile.read(chunksize)
                pos += len(chunk)
                if pos == filesize:
                    chunk = pad(chunk, AES.block_size)
                outfile.write(encryptor.encrypt(chunk))

和匹配decrypt_file函数,

def decrypt_file(key, in_filename, out_filename=None, chunksize=64*1024):
    if not out_filename:
        out_filename = in_filename + '.dec'
    with open(in_filename, 'rb') as infile:
        filesize = struct.unpack('<Q', infile.read(8))[0]
        iv = infile.read(16)
        encryptor = AES.new(key, AES.MODE_CBC, iv)
        with open(out_filename, 'wb') as outfile:
            encrypted_filesize = os.path.getsize(in_filename)
            pos = 8 + 16 # the filesize and IV.
            while pos < encrypted_filesize:
                chunk = infile.read(chunksize)
                pos += len(chunk)
                chunk = encryptor.decrypt(chunk)
                if pos == encrypted_filesize:
                    chunk = unpad(chunk, AES.block_size)
                outfile.write(chunk)

此代码与 Python2/Python3 兼容,它应该可以与 PyCryptodome 或 PyCrypto 一起使用。

但是,如果您使用的是 PyCrypto,我建议您更新到 PyCryptodome。PyCryptodome 是 PyCrypto 的一个分支,它公开了相同的 API(因此您不必过多地更改代码),以及一些额外的功能:填充函数、经过身份验证的加密算法、KDF 等。另一方面,PyCrypto 不是不再维护,而且某些版本存在基于堆的缓冲区溢出漏洞:CVE-2013-7459


推荐阅读