AES CTR 解密:Cryptography 和 Cryptodome 给出不同的结果

AES CTR decryption: Cryptography and Cryptodome give different results

下面使用 Cryptodome 对一些字节进行 AES 解密的代码片段如我所料:

from Crypto.Cipher import AES
from Crypto.Util import Counter

key = b'\x12' * 32
decryptor = AES.new(key, AES.MODE_CTR,counter=Counter.new(nbits=128, little_endian=True))
print(decryptor.decrypt(b'Something encrypted'))

下面使用 Python 密码学,并给出不同的结果:

from cryptography.hazmat.primitives.ciphers import Cipher, modes, algorithms

key = b'\x12' * 32
decryptor = Cipher(algorithms.AES(key), modes.CTR(b'[=11=]' * 16)).decryptor()
print(decryptor.update(b'Something encrypted'))

为什么?我如何更改 Python 加密版本以输出与 Cryptodome 相同的结果?

(上下文正在编写用于解压缩文件的 AES 解密代码,并且正在考虑使用 Python 密码学)

在 PyCryptodome 中,计数器默认从 1 开始。此外,小字节序的计数器计数如下:0x0100...0000, 0x0200...0000, 0x0300...0000

由于在密码学中无法配置计数器的字节序,并且使用大端,因此无法实现此计数。虽然起始值可以显式设置为 0x0100...00 ,但计数器随后会计数:0x0100...0000, 0x0100...0001, 0x0100...0002

这可以通过以下代码验证:

from Crypto.Cipher import AES
from Crypto.Util import Counter
key = b'\x12' * 32
decryptor = AES.new(key, AES.MODE_CTR,counter=Counter.new(nbits=128, little_endian=True, initial_value=1))
print(decryptor.decrypt(b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx').hex())

from cryptography.hazmat.primitives.ciphers import Cipher, modes, algorithms
key = b'\x12' * 32
decryptor = Cipher(algorithms.AES(key), modes.CTR(b'\x01' + b'[=10=]' * 15)).decryptor()
print(decryptor.update(b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx').hex())

输出:

faebe2ab213094fcd5c9ec3dae32372b 13b3b971b7694faa5e15f5387ac5a67f bc5dbc82ce54cf1bbe2719488b322078
faebe2ab213094fcd5c9ec3dae32372b 6ddda72218780c287bc74956395bf7db 0603820b26889ec64e7f7964a14518c5

因为第一个块的计数器值匹配,所以第一个块是相同的。但是,由于以下块的不同值,以下块不同。

当 PyCryptodome 配置为 little endian 时,我目前看不出如何使用 Cryptography 库生成 PyCryptodome 库结果。

(深受启发)

有 2 个问题

  • 传给CTR的值是计数器的初始值,必须是b'\x01' + b'[=14=]' * 15

  • 对于每块 16 个加密字节,这应该作为小端整数递增,但是 Python 密码学仅将其作为大端整数递增。

所以可能需要 Python 密码学来做到这一点,但是你必须在 之外 增加计数器并重新创建解密每个 16 字节块的上下文:

from cryptography.hazmat.primitives.ciphers import Cipher, modes, algorithms

key = b'\x12' * 32

def chunks(original):
    for i in range(0, len(original), 16):
        yield original[i:i+16]

def decrypt(chunks):
    for j, chunk in enumerate(chunks):
        yield Cipher(algorithms.AES(key), modes.CTR((j+1).to_bytes(16, byteorder='little'))).decryptor().update(chunk)

print(b''.join(decrypt(chunks(b'Something encrypted'))))

貌似Cryptography is/aims to be a thin layer over OpenSSL. So, you can decrypt AES in CTR mode with a little endian counter directly with multiple libcrypto (OpenSSL) contexts, as can be seen in this gist及以下

from contextlib import contextmanager
from ctypes import POINTER, cdll, c_char_p, c_void_p, c_int, create_string_buffer, byref
from sys import platform

def decrypt_aes_256_ctr_little_endian(
    key, ciphertext_chunks,
    get_libcrypto=lambda: cdll.LoadLibrary({'linux': 'libcrypto.so', 'darwin': 'libcrypto.dylib'}[platform])
):
    def non_null(result, func, args):
        if result == 0:
            raise Exception('Null value returned')
        return result

    def ensure_1(result, func, args):
        if result != 1:
            raise Exception(f'Result {result}')
        return result

    libcrypto = get_libcrypto()
    libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p
    libcrypto.EVP_CIPHER_CTX_new.errcheck = non_null
    libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,)

    libcrypto.EVP_DecryptInit_ex.argtypes = (c_void_p, c_void_p, c_void_p, c_char_p, c_char_p)
    libcrypto.EVP_DecryptInit_ex.errcheck = ensure_1
    libcrypto.EVP_DecryptUpdate.argtypes = (c_void_p, c_char_p, POINTER(c_int), c_char_p, c_int)
    libcrypto.EVP_DecryptUpdate.errcheck = ensure_1

    libcrypto.EVP_aes_256_ctr.restype = c_void_p

    @contextmanager
    def cipher_context():
        ctx = libcrypto.EVP_CIPHER_CTX_new()
        try:
            yield ctx
        finally:
            libcrypto.EVP_CIPHER_CTX_free(ctx)

    def in_fixed_size_chunks(chunks, size):
        chunk = b''
        offset = 0
        it = iter(chunks)

        def get(size):
            nonlocal chunk, offset

            while size:
                if not chunk:
                    try:
                        chunk = next(it)
                    except StopIteration:
                        return
                to_yield = min(size, len(chunk) - offset)
                yield chunk[offset:offset + to_yield]
                offset = (offset + to_yield) % len(chunk)
                chunk = chunk if offset else b''
                size -= to_yield

        while True:
            fixed_size_chunk = b''.join(get(size))
            if fixed_size_chunk:
                yield fixed_size_chunk
            else:
                break

    def decrypted_chunks(fixed_size_chunks):
        for j, chunk in enumerate(fixed_size_chunks):
            with cipher_context() as ctx:
                plaintext = create_string_buffer(16)
                plaintext_len = c_int()
                libcrypto.EVP_DecryptInit_ex(ctx, libcrypto.EVP_aes_256_ctr(), None, key, (j + 1).to_bytes(16, byteorder='little'))
                libcrypto.EVP_DecryptUpdate(ctx, plaintext, byref(plaintext_len), chunk, len(chunk))
                yield plaintext.raw[:plaintext_len.value]

    fixed_size_chunks = in_fixed_size_chunks(ciphertext_chunks, 16)
    decrypted_chunks = decrypted_chunks(fixed_size_chunks)
    yield from decrypted_chunks

上面的代码还有一个好处,就是在不知道每个块有多大的情况下使用可迭代的字节,这是我的特定用例:

ciphertext_chunks = [b'Something encrypted', b'even more encrypted']
key = b'\x12' * 32
for plaintext in decrypt_aes_256_ctr_little_endian(key, ciphertext_chunks):
   print(plaintext)