Search code examples
pythoncryptographyrandom-access

AES 128 CTR random access decryption with cryptography.hazmat in Python


In theory AES encryption in CTR mode allows for decrypting at any block index (that is you don't have to decrypt the whole encrypted data from the begining if you want to decrypt only from a certain position/block index). You just add to the initial value of the counter the desired block index and with this updated counter you can start decrypting. In practice I am trying to do this in Python with the cryptography.hazmat library but I am not able to do it.

I managed to do this using the pycryptodome module by specifying the initial_value=0 for the full file encryption and inital_value=desiredAESBlockIndex when constructing the cipher for decrypting at a specified AES block index.

Here is my code (using the cryptography module):

import os
import sys

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

def tstFullFileCryptography(aFilePath, aKey, aIV):
  fileRawData = b''
  with open(aFilePath, 'rb') as file:
    fileRawData = file.read()

  cipher = Cipher(algorithms.AES(aKey), modes.CTR(aIV))
  encryptor = cipher.encryptor()

  encryptedData = encryptor.update(fileRawData) + encryptor.finalize()
  
  with open(aFilePath + ".cry", 'wb') as file:
    file.write(encryptedData)

  ######### test #############
  decryptor = cipher.decryptor()
  decryptedData = decryptor.update(encryptedData) + decryptor.finalize()
  if decryptedData == fileRawData:
    print("OK - cryptography: full encryption")
  else:
    print("ERROR - cryptography: full encryption")

def tstCryptographyFromIndex(aFilePath, aKey, aIV):
  algAES = algorithms.AES(aKey)
  # the desired AES block index and size of data to decrypt
  tstAESBlockIndex = 1
  tstSizeInBytes = 16 * 13

  tstRaw = b''
  with open(aFilePath, 'rb') as file:
    file.seek(tstAESBlockIndex * algAES.block_size)
    tstRaw = file.read(tstSizeInBytes)

  tstEncryptedData = b''
  with open(aFilePath + ".cry", 'rb') as file:
    file.seek(tstAESBlockIndex * algAES.block_size)
    tstEncryptedData = file.read(tstSizeInBytes)
  print(f"\ntstEncryptedData: {tstEncryptedData}\n")
  
  ################ decrypt from desired block index; advance iv
  ivNum = int.from_bytes(aIV, byteorder=sys.byteorder)
  incrementedIVNum = ivNum + tstAESBlockIndex
  incrementedIV = incrementedIVNum.to_bytes(16, byteorder=sys.byteorder)
  decryptor = Cipher(algAES, modes.CTR(incrementedIV)).decryptor()
  
  tstDecryptedData = decryptor.update(tstEncryptedData) + decryptor.finalize()
  print(f"tstDecryptedData: {tstDecryptedData}\n")

  if tstRaw == tstDecryptedData:
    print("OK - cryptography: index decryption")
  else:
    print("ERROR - cryptography: index decryption")


def main():
  key = os.urandom(32)
  iv = os.urandom(16)
  
  tstFullFileCryptography("C:\\work\\python\\tstdata\\video.mp4", key, iv)
  tstCryptographyFromIndex("C:\\work\\python\\tstdata\\video.mp4", key, iv)
  
  return 0

if __name__ == "__main__":
  main()

Solution

  • Here is the code after fixing You had 2 errors in your code that cause your decryption and file seeking to be incorrect.

    import os
    import sys
    
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    
    def tstFullFileCryptography(aFilePath, aKey, aIV):
      fileRawData = b''
      with open(aFilePath, 'rb') as file:
        fileRawData = file.read()
    
      cipher = Cipher(algorithms.AES(aKey), modes.CTR(aIV))
      encryptor = cipher.encryptor()
    
      encryptedData = encryptor.update(fileRawData) + encryptor.finalize()
      
      with open(aFilePath + ".cry", 'wb') as file:
        file.write(encryptedData)
    
      ######### test #############
      decryptor = cipher.decryptor()
      decryptedData = decryptor.update(encryptedData) + decryptor.finalize()
      if decryptedData == fileRawData:
        print("OK - cryptography: full encryption")
      else:
        print("ERROR - cryptography: full encryption")
    
    def tstCryptographyFromIndex(aFilePath, aKey, aIV):
      algAES = algorithms.AES(aKey)
      # the desired AES block index and size of data to decrypt
      tstAESBlockIndex = 1
      tstSizeInBytes = 16 * 13
    
      tstRaw = b''
      with open(aFilePath, 'rb') as file:
        file.seek(tstAESBlockIndex * 16)
        tstRaw = file.read(tstSizeInBytes)
    
      tstEncryptedData = b''
      with open(aFilePath + ".cry", 'rb') as file:
        file.seek(tstAESBlockIndex * 16)
        tstEncryptedData = file.read(tstSizeInBytes)
      print(f"\ntstEncryptedData: {tstEncryptedData}\n")
      
      ################ decrypt from desired block index; advance iv
      ivNum = int.from_bytes(aIV, byteorder="big")
      incrementedIVNum = ivNum + tstAESBlockIndex
      incrementedIV = incrementedIVNum.to_bytes(16, byteorder="big")
      decryptor = Cipher(algAES, modes.CTR(incrementedIV)).decryptor()
      
      tstDecryptedData = decryptor.update(tstEncryptedData) + decryptor.finalize()
      print(f"tstDecryptedData: {tstDecryptedData}\n")
    
      if tstRaw == tstDecryptedData:
        print("OK - cryptography: index decryption")
      else:
        print("ERROR - cryptography: index decryption")
    
    
    def main():
      key = os.urandom(32)
      iv = os.urandom(16)
      
      tstFullFileCryptography("text", key, iv)
      tstCryptographyFromIndex("text", key, iv)
      
      return 0
    
    if __name__ == "__main__":
      main()
    

    First error is that you use incorrect block_size to seek since seeking is by byte level while block_size is at bit level = 128 in your case.

    Second error is sys.byte_order which is little in case you use x86 processor while all the encryption ops were done in big endianness so you are basically reversing bytes leading to incorrect decryption fixing both errors now let me decrypt successfully.