Implement OpenSSL AES Encryption in Python
Asked Answered
S

2

5

I'm trying to implement the following in Python: openssl enc -e -aes-256-cbc -base64 -k "Secret Passphrase" -in plaintext.txt -out ciphertext.txt

openssl enc -d -aes-256-cbc -base64 -k "Secret Passphrase" -in ciphertext.txt -out verification.txt

I've tried several different modules, PyCrypto, M2Crypto, etc but can't seem to get the correct combination of changing the password to the right size key and encoding everything correctly. I've found https://github.com/nvie/SimpleAES but that basically runs OpenSSL on the command line, which I'd rather avoid.

Sestet answered 17/12, 2012 at 3:17 Comment(2)
I have the same question, did you end up using the answer below as your solution? (It hasn't been accepted as the answer, so I was wondering if you solved this a different way)Withhold
If I remember right I don't think that worked either. I think the problem I was trying to solve was using the same encryption algorithm in Python and JS so I pivoted a bit. Instead I used cryptography and the Fernet implementation. cryptography.io/en/latest/fernet and then something like this in JS: github.com/csquared/fernet.jsSestet
O
6

Base 64 encoding and decoding can be easily handled via the standard base64 module.

AES-256 decryption and encryption in CBC mode are supported by both PyCrypto and M2Crypto.

The only non-standard (and most difficult) part is the derivation of the IV and the key from the password. OpenSSL does it via its own EVP_BytesToKey function, which is described in this man page.

The Python equivalent is:

def EVP_BytesToKey(password, salt, key_len, iv_len):
    """
    Derive the key and the IV from the given password and salt.
    """
    from hashlib import md5
    dtot =  md5(password + salt).digest()
    d = [ dtot ]
    while len(dtot)<(iv_len+key_len):
        d.append( md5(d[-1] + password + salt).digest() )
        dtot += d[-1]
    return dtot[:key_len], dtot[key_len:key_len+iv_len]

where key_len is 32 and iv_len is 16 for AES-256. The function returns the key and the IV which you can use to decrypt the payload.

OpenSSL puts and expects the salt in the first 8 bytes of the encrypted payload.

Finally, AES in CBC mode can only work with data aligned to the 16 byte boundary. The default padding used is PKCS#7.

The steps for encrypting are therefore:

  1. Generate 8 bytes of random data as salt.
  2. Derive AES key and IV from password using the salt from step 1.
  3. Pad the input data with PKCS#7.
  4. Encrypt the padded using AES-256 in CBC mode with the key and the IV from step 2.
  5. Encode in Base64 and output the salt from step 1.
  6. Encode in Base64 and output the encrypted data from step 4.

The steps from decrypting are the reverse:

  1. Decode the input data from Base64 into a binary string.
  2. Treat the first 8 bytes of the decoded data as salt.
  3. Derive AES key and IV from password using the salt from step 1.
  4. Decrypt the remaining decoded data using the AES key and the IV from step 3.
  5. Verify and remove the PKCS#7 padding from the result.
Obumbrate answered 17/12, 2012 at 22:12 Comment(1)
Almost agree. The openssl enc format is 8 bytes literal "Salted__", 8 bytes salt value, then ciphertext, all base64'ed as a unit. Combine encrypt #5 and #6 plus that constant, and change decrypt #2 to [8:16] and #4 to [16:].Rohde
K
3

Because nowadays the base64-standard is deprecated and pbkdf2-hashing is state of the art, the answer is correct, but outdated. I'm using this post, because it pops up as first result on DDG. To spare you, like me, to spend multiple days to figure this one out, here is my working code to encrypt data like openssl.

The following code is analog to echo <in> | openssl aes-256-cbc -pbkdf2 -k <key> -out <out>:

def encrypt(self, password, input):
    bs = AES.block_size
    salt = urandom(bs - len(b'Salted__'))
    pbk = pbkdf2_hmac('sha256', password.encode('utf8'), salt, 10000, 48)
    key = pbk[:32]
    iv = pbk[32:48]
    cipher = AES.new(key, AES.MODE_CBC, iv)
    result = (b'Salted__' + salt)
    finished = False
    while not finished:
        chunk = input.read(1024 * bs).encode()
        if len(chunk) == 0 or len(chunk) % bs != 0:
            padding_length = (bs - len(chunk) % bs) or bs
            chunk += (padding_length * chr(padding_length)).encode()
            finished = True
        result += cipher.encrypt(chunk)
    return result

To mention/clarify a few things:

  • you need to add the bytes 'Salted__' at the beginning of your hash (the salt is therfore only 8 and not 16 bytes)
  • the iv is derived from the pbkdf2 hash (You often find people adding the iv to the result. This leads to random, cryptic chars at the beginning of your decrypted result - the encrypted iv. It's human readable, but remember they are random bytes and can break everything! With the derivation from the passwordhash everything works.)
  • The Standard-Values at the moment for openssl are: md=sha256, iter=10000
  • You need to pad everything, so the encryption works (while loop)
  • If you want to, you can ecrypt everything also in base64. For that, don't return result, return base64.b64enode(result). It equals the openssl command from above with the addition of the -base64 flag.

To decrypt everything with openssl, use the following command: openssl aes-256-cbc -pbkdf2 -d -k <key> -in <in> -out <out>

Kohl answered 2/9, 2021 at 8:6 Comment(2)
It works after chunk = input[:1024*bs].encode() changed to chunk = input.read(1024 * bs)Candelabra
@Candelabra you're absolutely right. I realized this error before, but forgot to change it here. It is now corrected. Thank youKohl

© 2022 - 2024 — McMap. All rights reserved.