60

OpenSSL provides a popular (but insecure – see below!) command line interface for AES encryption:

openssl aes-256-cbc -salt -in filename -out filename.enc

Python has support for AES in the shape of the PyCrypto package, but it only provides the tools. How to use Python/PyCrypto to decrypt files that have been encrypted using OpenSSL?

Notice

This question 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.

Thijs van Dien
  • 6,516
  • 1
  • 29
  • 48
  • 3
    +1 for following up on yourself, but this wouldn't make a good standard, since password-based key derivation is based on a single iteration of MD5 (though with salt). At the very least, PBKDF2/scrypt should be used with a lot more iterations. – SquareRootOfTwentyThree May 27 '13 at 13:49
  • @SquareRootOfTwentyThree Thanks, I investigated that particular subject a little and good point. – Thijs van Dien May 30 '13 at 13:19
  • @SquareRootOfTwentyThree makes a very good point, `apps/enc.c` makes use of [EVP_BytesToKey](https://www.openssl.org/docs/man1.1.0/crypto/EVP_BytesToKey.html) with an iteration count of 1. For normal passwords, this is totally unsuitable since this can it can be trivially bruteforced. The manual page suggests use of PBKDF2 which is a more appropriate solution. Seeing that this code was [used in Ansible Vault](https://github.com/ansible/ansible/blob/v2.3.0.0-1/lib/ansible/parsing/vault/__init__.py#L575), what about starting with an explicit warning *not* to use this except for backwards compat? – Lekensteyn May 23 '17 at 23:45
  • @Lekensteyn Thank you for pointing out there's a reference to my answer in Ansible. I didn't expect that when writing it. :) There it actually seems to be used only for legacy purposes, but I get your point. I'll put in a stronger warning. – Thijs van Dien May 24 '17 at 13:00
  • @ThijsvanDien Now it is used for legacy purposes, but in the initial implementation in 2014 (where they had complete freedom on choosing primitives), they still managed to pick this one :-( Thanks for the edit! – Lekensteyn May 24 '17 at 19:11
  • 1
    @Lekensteyn I keep getting questions how to decrypt in other languages, suggesting people use the encryption code regardless. As of today, it can only be found in the edit history. – Thijs van Dien Dec 06 '17 at 15:48
  • cant we use https://www.pyopenssl.org/en/stable/api/ssl.html ? – bicepjai Oct 14 '19 at 22:18
  • @bicepjai I suppose you could, but then you'll still need OpenSSL itself. – Thijs van Dien Oct 17 '19 at 00:03

7 Answers7

97

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 md5
from Crypto.Cipher import AES
from Crypto import Random

def 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.

Thijs van Dien
  • 6,516
  • 1
  • 29
  • 48
  • 1
    How does this implementation compare to [this one](http://eli.thegreenplace.net/2010/06/25/aes-encryption-of-files-in-python-with-pycrypto/)? Are there any relative advantages or disadvantages? – rattray Nov 06 '13 at 03:00
  • 3
    @rattray The main difference is that your example is one like many others about general use of AES in Python. Mine is all about compatibility with the OpenSSL implementation, so that you can use a well-known command line tool for decryption of files encrypted with the Python code above, and the other way around. – Thijs van Dien Nov 06 '13 at 22:26
  • ThijsvanDien and Gregor, you guys, just made my day! Thank you so much! – Barmaley Oct 27 '14 at 19:26
  • How would you insert start offset of say...512 bytes for example, to leave decrypted? I'd like to preserve a file header for encrypt and decrypt operations. – Kenny Powers Sep 02 '15 at 00:39
  • 1
    @KennyPowers I don't think you can without breaking OpenSSL compatibility, which was the main goal of this question. If you don't need that, there are better ways to perform encryption that will also give you the flexibility you need. – Thijs van Dien Sep 02 '15 at 00:46
  • Would you mind pointing me in the right direction? I basically just need a simple AES ECB encrypt / decrypt that takes a password, and preserves file headers. I'm working through a simple version of this right now, but if I don't have to re-invent the wheel....well, you know what I mean. Thanks! – Kenny Powers Sep 02 '15 at 00:58
  • @KennyPowers All what fits in this comment is DON'T USE ECB. Other than that, I suggest you post a new question. I am not an authority on the subject anyway. – Thijs van Dien Sep 02 '15 at 11:31
  • Hi @ThijsvanDien. Forgive me, I tried to use your code, but I get this error: File "decrypt_platform_info.py", line 35, in decrypt chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs)) File "/Library/Python/2.7/site-packages/Crypto/Cipher/blockalgo.py", line 295, in decrypt return self._cipher.decrypt(ciphertext) **ValueError: Input strings must be a multiple of 16 in length** The command I'm using to encrypt is: ```openssl aes-256-cbc -a -salt -in file.json -out file.enc -pass pass:$password``` – Steve Walsh Feb 29 '16 at 21:48
  • 1
    @SteveWalsh My code expects binary whereas your `file.enc` is base64-encoded (given the `-a` parameter). Drop that parameter or decode the file before decrypting. For further support please start your own question. – Thijs van Dien Mar 03 '16 at 00:52
  • Thank you @ThijsvanDien! Really really helpful. Sorry for trailing my question on here. I just need to go to the master for help :) – Steve Walsh Mar 03 '16 at 11:17
  • @ThijsvanDien Can you give a brief explanation about what is weak about this encryption? Is the weakness in the encryption method itself, or does it come from the key length? Could I change the key length to 256 and have a stronger form of encryption? – tww0003 Aug 19 '17 at 18:54
  • @tww0003 See the comments on the question; it's mostly in the key derivation. Basically the process of turning a password into an encryption key is too simple/fast, making it easy to do many attempts. Completely random passwords of sufficient length (using a secure random generator) would probably still provide acceptable security, but anything user-entered rather not. Also it's unauthenticated encryption, which may or may not matter (explained in https://crypto.stackexchange.com/q/12178). If you have a choice, use libsodium; state of the art and foolproof. Crypto is just too easy to screw up. – Thijs van Dien Aug 19 '17 at 23:18
  • I am encrypting the file with this code, but i want to decrypt that file with the JAVA code is this possible? If yes then How? Thanks in advance. – Haroon Ahmed Dec 06 '17 at 11:43
  • @HaroonAhmed The only thing I can tell you is _don't_ encrypt with this code. Only use it to decrypt when you have no other choice because the file was (unfortunately) encrypted using OpenSSL in the past or by someone else. Instead, go for something like NaCl/libsodium on both sides. – Thijs van Dien Dec 06 '17 at 15:46
  • @ThijsvanDien I am encrypting a file in OpenSSL using just a secret key generated randomly as `openssl rand 256 > symmetric_keyfile.key` and then using the key to encrypt the file as so `openssl enc -in secrets.txt -out secrets.txt.enc -e -aes256 -k symmetric_keyfile.key`. What should be the AES MODE flag for this method of decryption? – Saket Kumar Singh Oct 07 '19 at 05:40
  • 1
    @SaketKumarSingh I don't think that command is doing what you think it's doing. It looks like you're encrypting the file with the password 'symmetric_keyfile.key', rather than what's in that file. – Thijs van Dien Oct 17 '19 at 18:29
22

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 md5
from Crypto.Cipher import AES
from Crypto import Random

def 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)
Gregor
  • 1,205
  • 11
  • 20
  • 4
    Please just edit my post. It is peer reviewed anyway. Generally I agree some error checking is good. Though 'missing pad' is kind of misleading when actually there's too much of it. Is that the same error OpenSSL gives? – Thijs van Dien Dec 08 '13 at 19:50
  • Corrected to more closely match openssl output from evp_enc.c which outputs the same "bad decrypt" message for both cases. – Gregor Dec 10 '13 at 13:17
  • Great! I want to decrypt in .NET too. Can anyone help me convert for this language? – Eduardo Jan 14 '14 at 18:54
  • 2
    I have removed the `encrypt` function from my answer and encourage you to do the same. – Thijs van Dien Dec 06 '17 at 15:39
13

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 urandom
from hashlib import md5

from Crypto.Cipher import AES

def 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(...)
Stephen Fuhry
  • 12,624
  • 6
  • 56
  • 55
Johnny Booy
  • 139
  • 1
  • 2
  • This code was obviously untested and doesn't work as is. – Chris Arndt Sep 02 '14 at 17:01
  • 1
    @ChrisArndt Works fine for me on python 3. – Stephen Fuhry Mar 07 '15 at 17:15
  • 1
    Sorry, I don't recall anymore, what wasn't working for me. However, I implemented my own script to encrypt a file with AES: https://gist.github.com/SpotlightKid/53e1eb408267315de620 – Chris Arndt Mar 12 '15 at 12:55
  • @ChrisArndt Hello there, I like your script, what if there is no padding in encrypted file? (it throws exception). To generate **perfect** file use `dd if=/dev/zero of=file.txt count=1 bs=1048576` and try encrypting it + decrypting it you should get `ValueError: Bad decrypt pad (0)` – Kyslik Oct 18 '15 at 13:24
  • @Kyslik I don't quite follow. I answered your comment on my Gist – Chris Arndt Oct 19 '15 at 15:23
  • @ChrisArndt yes I made an error, your gist is perfect :) – Kyslik Oct 19 '15 at 15:38
  • 1
    @StephenFuhry I realize this is an old post, but there's a subtle bug in the code that you might want to fix - the line "out_file.write(bytes(x for x in chunk))" should be moved out one level otherwise you're only decrypting the last chunk. – Vineet Bansal May 26 '17 at 15:00
  • 1
    I have removed the `encrypt` function from my answer and encourage you to do the same. – Thijs van Dien Dec 06 '17 at 15:39
  • I was able to use your code with Python 3.7 to decrypt a file using a known KEY and IV, rather than a known password. Sharing details in case anyone else needs to do the same. The file I had to decrypt did not contain a salt header, so I removed the `salt =...` line. I also replaced the `key, iv =...` line with `key = bytes.fromhex(str_key)` and `iv = bytes.fromhex(str_iv)`, where `str_key` and `str_iv` contained 64-byte and 32-byte strings, respectively. – Jason Klein Sep 23 '19 at 14:41
  • This is fairly old now, but I couldn't get this answer to work (Python 3.9). I did encrypt with it, and it can also decrypt back to the original. But when I encrypt with the OpenSSL command in the original question (using v1.1.1f), I can't decrypt it with this. When I adjust the "Salted__" header to match, the output is blank. – Michael K Aug 24 '21 at 19:36
3

This answer is based on openssl v1.1.1, which supports a stronger key derivation process for AES encryption, than that of previous versions of openssl.

This answer is based on the following command:

echo -n 'Hello World!' | openssl aes-256-cbc -e -a -salt -pbkdf2 -iter 10000 

This command encrypts the plaintext 'Hello World!' using aes-256-cbc. The key is derived using pbkdf2 from the password and a random salt, with 10,000 iterations of sha256 hashing. When prompted for the password, I entered the password, 'p4$$w0rd'. The ciphertext output produced by the command was:

U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE=

The process for decrypting of the ciphertext above produced by openssl is as follows:

  1. base64-decode the output from openssl, and utf-8 decode the password, so that we have the underlying bytes for both of these.
  2. The salt is bytes 8-15 of the base64-decoded openssl output.
  3. Derive a 48-byte key using pbkdf2 given the password bytes and salt with 10,000 iterations of sha256 hashing.
  4. The key is bytes 0-31 of the derived key, the iv is bytes 32-47 of the derived key.
  5. The ciphertext is bytes 16 through the end of the base64-decoded openssl output.
  6. Decrypt the ciphertext using aes-256-cbc, given the key, iv, and ciphertext.
  7. Remove PKCS#7 padding from plaintext. The last byte of plaintext indicates the number of padding bytes appended to the end of the plaintext. This is the number of bytes to be removed.

Below is a python3 implementation of the above process:

import binascii
import base64
import hashlib
from Crypto.Cipher import AES       #requires pycrypto

#inputs
openssloutputb64='U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE='
password='p4$$w0rd'
pbkdf2iterations=10000

#convert inputs to bytes
openssloutputbytes=base64.b64decode(openssloutputb64)
passwordbytes=password.encode('utf-8')

#salt is bytes 8 through 15 of openssloutputbytes
salt=openssloutputbytes[8:16]

#derive a 48-byte key using pbkdf2 given the password and salt with 10,000 iterations of sha256 hashing
derivedkey=hashlib.pbkdf2_hmac('sha256', passwordbytes, salt, pbkdf2iterations, 48)

#key is bytes 0-31 of derivedkey, iv is bytes 32-47 of derivedkey 
key=derivedkey[0:32]
iv=derivedkey[32:48]

#ciphertext is bytes 16-end of openssloutputbytes
ciphertext=openssloutputbytes[16:]

#decrypt ciphertext using aes-cbc, given key, iv, and ciphertext
decryptor=AES.new(key, AES.MODE_CBC, iv)
plaintext=decryptor.decrypt(ciphertext)

#remove PKCS#7 padding. 
#Last byte of plaintext indicates the number of padding bytes appended to end of plaintext.  This is the number of bytes to be removed.
plaintext = plaintext[:-plaintext[-1]]

#output results
print('openssloutputb64:', openssloutputb64)
print('password:', password)
print('salt:', salt.hex())
print('key: ', key.hex())
print('iv: ', iv.hex())
print('ciphertext: ', ciphertext.hex())
print('plaintext: ', plaintext.decode('utf-8'))

As expected, the above python3 script produces the following:

openssloutputb64: U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE=
password: p4$$w0rd
salt: ca7fc628e898c187
key:  444ab886d5721fc87e58f86f3e7734659007bea7fbe790541d9e73c481d9d983
iv:  7f4597a18096715d7f9830f0125be8fd
ciphertext:  ea842d6862ac05ebefcf9b6cf4239711
plaintext:  Hello World!

Note: An equivalent/compatible implementation in javascript (using the web crypto api) can be found at https://github.com/meixler/web-browser-based-file-encryption-decryption.

mti2935
  • 11,465
  • 3
  • 29
  • 33
0

I know this is a bit late but here is a solution that I blogged in 2013 about how to use the python pycrypto package to encrypt/decrypt in an openssl compatible way. It has been tested on python2.7 and python3.x. The source code and a test script can be found here.

One of the key differences between this solution and the excellent solutions presented above is that it differentiates between pipe and file I/O which can cause problems in some applications.

The key functions from that blog are shown below.

# ================================================================
# get_key_and_iv
# ================================================================
def get_key_and_iv(password, salt, klen=32, ilen=16, msgdgst='md5'):
    '''
    Derive the key and the IV from the given password and salt.

    This is a niftier implementation than my direct transliteration of
    the C++ code although I modified to support different digests.

    CITATION: http://stackoverflow.com/questions/13907841/implement-openssl-aes-encryption-in-python

    @param password  The password to use as the seed.
    @param salt      The salt.
    @param klen      The key length.
    @param ilen      The initialization vector length.
    @param msgdgst   The message digest algorithm to use.
    '''
    # equivalent to:
    #   from hashlib import <mdi> as mdf
    #   from hashlib import md5 as mdf
    #   from hashlib import sha512 as mdf
    mdf = getattr(__import__('hashlib', fromlist=[msgdgst]), msgdgst)
    password = password.encode('ascii', 'ignore')  # convert to ASCII

    try:
        maxlen = klen + ilen
        keyiv = mdf(password + salt).digest()
        tmp = [keyiv]
        while len(tmp) < maxlen:
            tmp.append( mdf(tmp[-1] + password + salt).digest() )
            keyiv += tmp[-1]  # append the last byte
        key = keyiv[:klen]
        iv = keyiv[klen:klen+ilen]
        return key, iv
    except UnicodeDecodeError:
        return None, None


# ================================================================
# encrypt
# ================================================================
def encrypt(password, plaintext, chunkit=True, msgdgst='md5'):
    '''
    Encrypt the plaintext using the password using an openssl
    compatible encryption algorithm. It is the same as creating a file
    with plaintext contents and running openssl like this:

    $ cat plaintext
    <plaintext>
    $ openssl enc -e -aes-256-cbc -base64 -salt \\
        -pass pass:<password> -n plaintext

    @param password  The password.
    @param plaintext The plaintext to encrypt.
    @param chunkit   Flag that tells encrypt to split the ciphertext
                     into 64 character (MIME encoded) lines.
                     This does not affect the decrypt operation.
    @param msgdgst   The message digest algorithm.
    '''
    salt = os.urandom(8)
    key, iv = get_key_and_iv(password, salt, msgdgst=msgdgst)
    if key is None:
        return None

    # PKCS#7 padding
    padding_len = 16 - (len(plaintext) % 16)
    if isinstance(plaintext, str):
        padded_plaintext = plaintext + (chr(padding_len) * padding_len)
    else: # assume bytes
        padded_plaintext = plaintext + (bytearray([padding_len] * padding_len))

    # Encrypt
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ciphertext = cipher.encrypt(padded_plaintext)

    # Make openssl compatible.
    # I first discovered this when I wrote the C++ Cipher class.
    # CITATION: http://projects.joelinoff.com/cipher-1.1/doxydocs/html/
    openssl_ciphertext = b'Salted__' + salt + ciphertext
    b64 = base64.b64encode(openssl_ciphertext)
    if not chunkit:
        return b64

    LINELEN = 64
    chunk = lambda s: b'\n'.join(s[i:min(i+LINELEN, len(s))]
                                for i in range(0, len(s), LINELEN))
    return chunk(b64)


# ================================================================
# decrypt
# ================================================================
def decrypt(password, ciphertext, msgdgst='md5'):
    '''
    Decrypt the ciphertext using the password using an openssl
    compatible decryption algorithm. It is the same as creating a file
    with ciphertext contents and running openssl like this:

    $ cat ciphertext
    # ENCRYPTED
    <ciphertext>
    $ egrep -v '^#|^$' | \\
        openssl enc -d -aes-256-cbc -base64 -salt -pass pass:<password> -in ciphertext
    @param password   The password.
    @param ciphertext The ciphertext to decrypt.
    @param msgdgst    The message digest algorithm.
    @returns the decrypted data.
    '''

    # unfilter -- ignore blank lines and comments
    if isinstance(ciphertext, str):
        filtered = ''
        nl = '\n'
        re1 = r'^\s*$'
        re2 = r'^\s*#'
    else:
        filtered = b''
        nl = b'\n'
        re1 = b'^\\s*$'
        re2 = b'^\\s*#'

    for line in ciphertext.split(nl):
        line = line.strip()
        if re.search(re1,line) or re.search(re2, line):
            continue
        filtered += line + nl

    # Base64 decode
    raw = base64.b64decode(filtered)
    assert(raw[:8] == b'Salted__' )
    salt = raw[8:16]  # get the salt

    # Now create the key and iv.
    key, iv = get_key_and_iv(password, salt, msgdgst=msgdgst)
    if key is None:
        return None

    # The original ciphertext
    ciphertext = raw[16:]

    # Decrypt
    cipher = AES.new(key, AES.MODE_CBC, iv)
    padded_plaintext = cipher.decrypt(ciphertext)

    if isinstance(padded_plaintext, str):
        padding_len = ord(padded_plaintext[-1])
    else:
        padding_len = padded_plaintext[-1]
    plaintext = padded_plaintext[:-padding_len]
    return plaintext
Joe Linoff
  • 761
  • 9
  • 13
  • I couldn't get this solution to work in Python 3.9. When I put these functions into my code, I get the TypeError: Object type cannot be passed to C code. The blog link is broken. And I couldn't get the github-linked script to work. That seems to stall on most things. It does output "b'mcrypt.py' 1.2" when I use the -V option. It's definitely possible I'm not doing something right. – Michael K Aug 23 '21 at 20:25
  • Wow, i am sorry that you are having issues, i haven't looked at this in quite awhile, i will take a look, meanwhile, you might try https://github.com/jlinoff/lock_files it should still be working. Is this the blog URL that is failing for you: https://joelinoff.com/blog/?p=885 ? – Joe Linoff Aug 26 '21 at 20:06
  • it looks like something changed in the pycrypto package. I was able to workaround it by changing the install package name from `crypto` to `Crypto` but that is way too hacky. I am removing the gist to avoid confusing others. This might be helpful: https://crypto.stackexchange.com/questions/3298/is-there-a-standard-for-openssl-interoperable-aes-encryption. – Joe Linoff Aug 28 '21 at 17:45
  • I decided to keep the gist and update it to reflect this conversation along with a detailed description of the workaround needed to get it to work. Thank you for reporting this. GIST: https://gist.github.com/jlinoff/412752f1ecb6b27762539c0f6b6d667b – Joe Linoff Aug 28 '21 at 18:12
  • No worries. I knew this was from 2017 and oddly I have also had plenty of other issues trying to get OpenSSL-compatible decryption to work in Python. I ended up having my code run OpenSSL with subprocess. By the way the blog link isn't actually broken, but there isn't anything there besides "Simple python functions that provide openssl -aes-256-cbc compatible encrypt/decrypt" (looks like title and sidebar only). I read a little on your lock_files project, very neat. – Michael K Aug 30 '21 at 19:45
  • Thank you for the clarification, i will address the blog issue. – Joe Linoff Aug 31 '21 at 20:51
0

Tried everything above and some more from other threads, this is what has worked for me, equivalent of this in openssl:

Not the best encrpython but those were requirements

Decryption: openssl enc -d -aes256 -md md5 -in {->path_in} -out {->path_out} -pass pass:{->pass}

Encryption: openssl enc -e -aes256 -md md5 -in {->path_in} -out {->path_out} -pass pass:{->pass}

Python:

from os import urandom
from hashlib import md5
from Crypto.Cipher import AES
import typer

def filecrypto(in_file, out_file, password, decrypt: bool = True):
    salt_header = 'Salted__'

    def 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_f(in_file, out_file, password, salt_header=salt_header, key_length=32):
        bs = AES.block_size
        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)
        with open(out_file, 'wb') as f_out:
            # write the first line or the salted header
            f_out.write(str.encode(salt_header) + salt)
            with open(in_file, 'rb') as f_in:
                f_out.write(cipher.encrypt(f_in.read()))

    def decrypt_f(in_file, out_file, password, salt_header=salt_header, key_length=32):
        bs = AES.block_size
        with open(in_file, 'rb') as f_in:
            # retrieve the salted header
            salt = f_in.read(bs)[len(salt_header):]
            key, iv = derive_key_and_iv(password, salt, key_length, bs)
            cipher = AES.new(key, AES.MODE_CBC, iv)
            with open(out_file, 'wb') as f_out:
                f_out.write(cipher.decrypt(f_in.read()))

    return decrypt_f(in_file, out_file, password) if decrypt else encrypt_f(in_file, out_file, password)

if __name__ == "__filecrypto__":
    typer.run(filecrypto)
-1

Note: this method is not OpenSSL compatible

But it is suitable if all you want to do is encrypt and decrypt files.

A self-answer I copied from here. I think this is, perhaps, a simpler and more secure option. Although I would be interested in some expert opinion on how secure it is.

I used Python 3.6 and SimpleCrypt to encrypt the file and then uploaded it.

I think this is the code I used to encrypt the file:

from simplecrypt import encrypt, decrypt
f = open('file.csv','r').read()
ciphertext = encrypt('USERPASSWORD',f.encode('utf8')) # I am not certain of whether I used the .encode('utf8')
e = open('file.enc','wb') # file.enc doesn't need to exist, python will create it
e.write(ciphertext)
e.close

This is the code I use to decrypt at runtime, I run getpass("password: ") as an argument so I don't have to store a password variable in memory

from simplecrypt import encrypt, decrypt
from getpass import getpass

# opens the file
f = open('file.enc','rb').read()

print('Please enter the password and press the enter key \n Decryption may take some time')

# Decrypts the data, requires a user-input password
plaintext = decrypt(getpass("password: "), f).decode('utf8')
print('Data have been Decrypted')

Note, the UTF-8 encoding behaviour is different in python 2.7 so the code will be slightly different.

Harvs
  • 503
  • 1
  • 6
  • 18
  • Please note that this question is specifically about achieving compatibility with OpenSSL; not about good ways to perform encryption in Python (the OpenSSL way certainly isn't). As such, your answer does not fit the question and therefore I'm downvoting. – Thijs van Dien Aug 04 '17 at 21:41
  • @ThijsvanDien Thank you for pointing that out. I did not realise as my post [Import encrypted csv into Python 3](https://stackoverflow.com/questions/45358388/import-encrypted-csv-into-python-3) was marked as a potential duplicate of this post. I have edited the post to clarify. – Harvs Aug 05 '17 at 10:21