(Very much inspired by @Topaco's answer)
There are 2 issues
The value passed to CTR
is the initial value of the counter, and has to be b'\x01' + b'\0' * 15
This then should be incremented as a little endian integer for each block of 16 encrypted bytes, but Python Cryptography only increments it as a big endian integer.
So it is to possible get Python Cryptography to do this, but you have to do the incrementing of the counter outside of it and re-create a decryption context for each block of 16 bytes:
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'))))
It looks like 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 and below
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
The above code also has the benefit of working with an iterable of bytes where it's not known how big each chunk is, which is my particular use case:
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)