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



下面的代码片段使用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'' * 16)).decryptor()
print(decryptor.update(b'Something encrypted'))

为什么?我如何更改Python Cryptography版本以输出与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'' * 15)).decryptor()
print(decryptor.update(b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx').hex())

输出:

faebe2ab213094fcd5c9ec3dae32372b 13b3b971b7694faa5e15f5387ac5a67f bc5dbc82ce54cf1bbe2719488b322078
faebe2ab213094fcd5c9ec3dae32372b 6ddda72218780c287bc74956395bf7db 0603820b26889ec64e7f7964a14518c5

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

当PyCryptodome配置为小端序时,我目前不知道如何使用Cryptography库来生成PyCryptodome库结果。

(非常受@Topaco回答的启发)

有两个问题

  • 传递给CTR的值是计数器的初始值,必须为b'x01' + b'' * 15

  • 对于每个16个加密字节的块,它应该作为一个小的端序整数递增,但是Python加密只作为一个大的端序整数递增。

所以有可能让Python Cryptography做到这一点,但是你必须在之外对计数器进行递增,并为每个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'))))

看起来密码学的目标是成为OpenSSL之上的薄层。因此,您可以直接在多个libcrypto (OpenSSL)上下文中使用一个小的尾端计数器在CTR模式下解密AES,如本文要点和下面

所示。
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)

最新更新