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)
下面使用 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)