How to decrypt OpenSSL AES-encrypted files in Python? How to decrypt OpenSSL AES-encrypted files in Python? python python

How to decrypt OpenSSL AES-encrypted files in Python?


Given the popularity of Python, at first I was disappointed that there was no complete answer to this question to be found. It took me a fair amount of reading different answers on this board, as well as other resources, to get it right. I thought I might share the result for future reference and perhaps review; I'm by no means a cryptography expert! However, the code below appears to work seamlessly:

from hashlib import md5from Crypto.Cipher import AESfrom Crypto import Randomdef derive_key_and_iv(password, salt, key_length, iv_length):    d = d_i = ''    while len(d) < key_length + iv_length:        d_i = md5(d_i + password + salt).digest()        d += d_i    return d[:key_length], d[key_length:key_length+iv_length]def decrypt(in_file, out_file, password, key_length=32):    bs = AES.block_size    salt = in_file.read(bs)[len('Salted__'):]    key, iv = derive_key_and_iv(password, salt, key_length, bs)    cipher = AES.new(key, AES.MODE_CBC, iv)    next_chunk = ''    finished = False    while not finished:        chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))        if len(next_chunk) == 0:            padding_length = ord(chunk[-1])            chunk = chunk[:-padding_length]            finished = True        out_file.write(chunk)

Usage:

with open(in_filename, 'rb') as in_file, open(out_filename, 'wb') as out_file:    decrypt(in_file, out_file, password)

If you see a chance to improve on this or extend it to be more flexible (e.g. make it work without salt, or provide Python 3 compatibility), please feel free to do so.

Notice

This answer used to also concern encryption in Python using the same scheme. I have since removed that part to discourage anyone from using it. Do NOT encrypt any more data in this way, because it is NOT secure by today's standards. You should ONLY use decryption, for no other reasons than BACKWARD COMPATIBILITY, i.e. when you have no other choice. Want to encrypt? Use NaCl/libsodium if you possibly can.


I am re-posting your code with a couple of corrections (I didn't want to obscure your version). While your code works, it does not detect some errors around padding. In particular, if the decryption key provided is incorrect, your padding logic may do something odd. If you agree with my change, you may update your solution.

from hashlib import md5from Crypto.Cipher import AESfrom Crypto import Randomdef derive_key_and_iv(password, salt, key_length, iv_length):    d = d_i = ''    while len(d) < key_length + iv_length:        d_i = md5(d_i + password + salt).digest()        d += d_i    return d[:key_length], d[key_length:key_length+iv_length]# This encryption mode is no longer secure by today's standards.# See note in original question above.def obsolete_encrypt(in_file, out_file, password, key_length=32):    bs = AES.block_size    salt = Random.new().read(bs - len('Salted__'))    key, iv = derive_key_and_iv(password, salt, key_length, bs)    cipher = AES.new(key, AES.MODE_CBC, iv)    out_file.write('Salted__' + salt)    finished = False    while not finished:        chunk = in_file.read(1024 * bs)        if len(chunk) == 0 or len(chunk) % bs != 0:            padding_length = bs - (len(chunk) % bs)            chunk += padding_length * chr(padding_length)            finished = True        out_file.write(cipher.encrypt(chunk))def decrypt(in_file, out_file, password, key_length=32):    bs = AES.block_size    salt = in_file.read(bs)[len('Salted__'):]    key, iv = derive_key_and_iv(password, salt, key_length, bs)    cipher = AES.new(key, AES.MODE_CBC, iv)    next_chunk = ''    finished = False    while not finished:        chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))        if len(next_chunk) == 0:            padding_length = ord(chunk[-1])            if padding_length < 1 or padding_length > bs:               raise ValueError("bad decrypt pad (%d)" % padding_length)            # all the pad-bytes must be the same            if chunk[-padding_length:] != (padding_length * chr(padding_length)):               # this is similar to the bad decrypt:evp_enc.c from openssl program               raise ValueError("bad decrypt")            chunk = chunk[:-padding_length]            finished = True        out_file.write(chunk)


The code below should be Python 3 compatible with the small changes documented in the code. Also wanted to use os.urandom instead of Crypto.Random. 'Salted__' is replaced with salt_header that can be tailored or left empty if needed.

from os import urandomfrom hashlib import md5from Crypto.Cipher import AESdef derive_key_and_iv(password, salt, key_length, iv_length):    d = d_i = b''  # changed '' to b''    while len(d) < key_length + iv_length:        # changed password to str.encode(password)        d_i = md5(d_i + str.encode(password) + salt).digest()        d += d_i    return d[:key_length], d[key_length:key_length+iv_length]def encrypt(in_file, out_file, password, salt_header='', key_length=32):    # added salt_header=''    bs = AES.block_size    # replaced Crypt.Random with os.urandom    salt = urandom(bs - len(salt_header))    key, iv = derive_key_and_iv(password, salt, key_length, bs)    cipher = AES.new(key, AES.MODE_CBC, iv)    # changed 'Salted__' to str.encode(salt_header)    out_file.write(str.encode(salt_header) + salt)    finished = False    while not finished:        chunk = in_file.read(1024 * bs)         if len(chunk) == 0 or len(chunk) % bs != 0:            padding_length = (bs - len(chunk) % bs) or bs            # changed right side to str.encode(...)            chunk += str.encode(                padding_length * chr(padding_length))            finished = True        out_file.write(cipher.encrypt(chunk))def decrypt(in_file, out_file, password, salt_header='', key_length=32):    # added salt_header=''    bs = AES.block_size    # changed 'Salted__' to salt_header    salt = in_file.read(bs)[len(salt_header):]    key, iv = derive_key_and_iv(password, salt, key_length, bs)    cipher = AES.new(key, AES.MODE_CBC, iv)    next_chunk = ''    finished = False    while not finished:        chunk, next_chunk = next_chunk, cipher.decrypt(            in_file.read(1024 * bs))        if len(next_chunk) == 0:            padding_length = chunk[-1]  # removed ord(...) as unnecessary            chunk = chunk[:-padding_length]            finished = True         out_file.write(bytes(x for x in chunk))  # changed chunk to bytes(...)