AES CTR Mode Encryption with HMAC - python

I am trying to implement AES CTR encryption mode with HMAC authentication for messages.
It's encrypting and decrypting fine as long as the key length is 64 bytes, since AES key and HMAC key are being derived from this key.
Questions
Is it safe to append IV or nonce to the encrypted messages?
Is it safe to append HMAC digest to append to the messages?
Using Pycryptodome
Code
import os
import zlib
from Crypto.Hash import HMAC
from Crypto.Hash import SHA256
from Crypto.Cipher import AES
from Crypto.Util import Counter
import zlib
def encrypt(full_key, plaintext):
if len(full_key) != 64:
raise Exception("FULL key length shall be equal to 64")
key = full_key[:len(full_key) //2]
# Use the last half as the HMAC key
hmac_key = full_key[len(full_key) // 2:]
if isinstance(plaintext, str):
plaintext = plaintext.encode()
compressed = zlib.compress(plaintext, 5)
print (f"compressed plaintext {compressed}")
# Choose a random, 16-byte IV.
iv = os.urandom(16)
# Convert the IV to a Python integer.
iv_int = int(binascii.hexlify(iv), 16)
# Create a new Counter object with IV = iv_int.
ctr = Counter.new(128, initial_value=iv_int)
# Create AES-CTR cipher.
aes = AES.new(key, AES.MODE_CTR, counter=ctr)
# Encrypt and return IV and ciphertext.
ciphertext = aes.encrypt(compressed)
hmac_obj = HMAC.new(hmac_key, compressed, SHA256)
mac = hmac_obj.digest()
return iv+ciphertext+mac
def decrypt(key, ciphertext):
# Initialize counter for decryption. iv should be the same as the output of
# encrypt().
if len(full_key) != 64:
raise Exception("FULL key length shall be equal to 64")
key = full_key[:len(full_key) //2]
# Use the last half as the HMAC key
hmac_key = full_key[len(full_key) // 2:]
mac_length = 32
iv_length = 16
iv = ciphertext[:16]
mac = ciphertext[-mac_length:]
_ciphertext = ciphertext[iv_length:-mac_length]
iv_int = int(iv.hex(), 16)
ctr = Counter.new(128, initial_value=iv_int)
# Create AES-CTR cipher.
aes = AES.new(key, AES.MODE_CTR, counter=ctr)
ciphertext = aes.decrypt(_ciphertext)
# Extract the MAC from the end of the file
hmac_obj = HMAC.new(hmac_key, ciphertext, SHA256)
computed_mac = hmac_obj.digest()
if computed_mac != mac:
raise Exception("Messege integrity violated")
plaintext= zlib.decompress(ciphertext)
# Decrypt and return the plaintext.
return plaintext

Related

pycryptodome & AES CBC : failing to decrypt a string

I'm trying to implement a simple encryption-decryption script with pycryptodome and AES-CBC, that is:
no iv,
no padding, therefore the string to encrypt is stripped do 16 characters
key is not random and is a fixed string.
However I fail by decrypting the message.
Here is the script:
from Crypto.Cipher import AES
from Crypto import Random
#import itertools
plain_text = "This is the text to encrypt"
key = "0361231230000000"
def encrypt(plain_text, key):
key = bytes(key, "UTF-8")
cipher = AES.new(key, AES.MODE_CBC)
print("Encryption Cipher: ", cipher)
# When there is no padding, the block size must equal the cipher length
# Padding is necessary for texts with length different from 16 bytes
cbytes = cipher.encrypt(bytes(plain_text[:16], "UTF-8"))
return cbytes
def decrypt(enc_text):
k = bytes(key, "UTF-8")
cipher = AES.new(k, AES.MODE_CBC)
print("Decryption Cipher: ")
return cipher.decrypt(enc_text).decode("UTF-8")
if __name__ == "__main__":
enc_text = encrypt(plain_text, key)
print(enc_text)
dec_text = decrypt(enc_text)
print(dec_text)
The error message is the following one:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte
If in the decryption process I replace "UTF-8" by "Latin-1", the output does not match:
Decryption Cipher:
Ï(¨e¨^î
What did I miss ?
Unlike ECB, CBC does require an initialization vector. As the documentation says:
If [iv argument] not provided, a random byte string is generated (you must then read its value with the iv attribute).
To apply that to your code:
from Crypto.Cipher import AES
from Crypto import Random
plain_text = "This is the text to encrypt"
key = b"0361231230000000"
def encrypt(plain_text, key):
cipher = AES.new(key, AES.MODE_CBC)
b = plain_text.encode("UTF-8")
return cipher.iv, cipher.encrypt(b)
def decrypt(iv, enc_text):
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
return cipher.decrypt(enc_text).decode("UTF-8")
if __name__ == "__main__":
iv, enc_text = encrypt(plain_text[:16], key)
dec_text = decrypt(iv, enc_text)
print(dec_text)

PyCrypto Not Printing Properly

Hi so before I was getting the IV in front (decrypted value of IV I believe it was) of the decrypted string. Now I don't get the string... How do I make this work, been trying for hours now...
My code:
from Crypto.Cipher import AES
import hashlib
import base64
import os
import string
iv = os.urandom(16)
key = hashlib.sha256(b'mypassword123').digest()
plaintext = (b'the password is totally not secure')
cipher = AES.new(key, AES.MODE_CFB, iv)
ciphertext = iv + cipher.encrypt(plaintext)
print (ciphertext)
print ("IV = ",iv)
ciphertext = ciphertext.split(iv)
ciphertext = str(ciphertext)[1].strip()
plaintext = cipher.decrypt(ciphertext)
print (plaintext)
encrypt will change cipher, you have to make a new one;
and str will change byte to repr(byte), like below:
a=b'xxx'
str(a) # "b'xxx'"
from Crypto.Cipher import AES
import hashlib
import base64
import os
import string
iv = os.urandom(16)
key = hashlib.sha256(b'mypassword123').digest()
plaintext = (b'the password is totally not secure')
cipher = AES.new(key, AES.MODE_CFB, iv)
ciphertext = iv + cipher.encrypt(plaintext)
print (ciphertext)
print ("IV = ",iv)
ciphertext = ciphertext.split(iv)
ciphertext = ciphertext[1]
cipher2 = AES.new(key, AES.MODE_CFB, iv)
plaintext = cipher2.decrypt(ciphertext)
print (plaintext)
detail see pycrypto

Python transfer encrypted image using CRYPTO AES [duplicate]

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.
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.
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)
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(...)
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:
base64-decode the output from openssl, and utf-8 decode the
password, so that we have the underlying bytes for both of these.
The salt is bytes 8-15 of the base64-decoded openssl output.
Derive a 48-byte key using pbkdf2 given the password bytes and salt with
10,000 iterations of sha256 hashing.
The key is bytes 0-31 of the derived key, the iv is bytes 32-47 of the derived key.
The ciphertext is bytes 16 through the end of the base64-decoded openssl
output.
Decrypt the ciphertext using aes-256-cbc, given the key, iv, and
ciphertext.
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.
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
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)
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.

How to decrypt OpenSSL AES-encrypted files in Python?

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.
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.
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)
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(...)
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:
base64-decode the output from openssl, and utf-8 decode the
password, so that we have the underlying bytes for both of these.
The salt is bytes 8-15 of the base64-decoded openssl output.
Derive a 48-byte key using pbkdf2 given the password bytes and salt with
10,000 iterations of sha256 hashing.
The key is bytes 0-31 of the derived key, the iv is bytes 32-47 of the derived key.
The ciphertext is bytes 16 through the end of the base64-decoded openssl
output.
Decrypt the ciphertext using aes-256-cbc, given the key, iv, and
ciphertext.
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.
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
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)
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.

Encrypt and decrypt using PyCrypto AES-256

I'm trying to build two functions using PyCrypto that accept two parameters: the message and the key, and then encrypt/decrypt the message.
I found several links on the web to help me out, but each one of them has flaws:
This one at codekoala uses os.urandom, which is discouraged by PyCrypto.
Moreover, the key I give to the function is not guaranteed to have the exact length expected. What can I do to make that happen?
Also, there are several modes, which one is recommended? I don't know what to use :/
Finally, what exactly is the IV? Can I provide a different IV for encrypting and decrypting, or will this return in a different result?
Here is my implementation, and it works for me with some fixes. It enhances the alignment of the key and secret phrase with 32 bytes and IV to 16 bytes:
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
class AESCipher(object):
def __init__(self, key):
self.bs = AES.block_size
self.key = hashlib.sha256(key.encode()).digest()
def encrypt(self, raw):
raw = self._pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return base64.b64encode(iv + cipher.encrypt(raw.encode()))
def decrypt(self, enc):
enc = base64.b64decode(enc)
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')
def _pad(self, s):
return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)
#staticmethod
def _unpad(s):
return s[:-ord(s[len(s)-1:])]
You may need the following two functions: pad- to pad (when doing encryption) and unpad- to unpad (when doing decryption) when the length of input is not a multiple of BLOCK_SIZE.
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[:-ord(s[len(s)-1:])]
So you're asking the length of key? You can use the MD5 hash of the key rather than use it directly.
More, according to my little experience of using PyCrypto, the IV is used to mix up the output of a encryption when input is same, so the IV is chosen as a random string, and use it as part of the encryption output, and then use it to decrypt the message.
And here's my implementation:
import base64
from Crypto.Cipher import AES
from Crypto import Random
class AESCipher:
def __init__( self, key ):
self.key = key
def encrypt( self, raw ):
raw = pad(raw)
iv = Random.new().read( AES.block_size )
cipher = AES.new( self.key, AES.MODE_CBC, iv )
return base64.b64encode( iv + cipher.encrypt( raw ) )
def decrypt( self, enc ):
enc = base64.b64decode(enc)
iv = enc[:16]
cipher = AES.new(self.key, AES.MODE_CBC, iv )
return unpad(cipher.decrypt( enc[16:] ))
Let me address your question about "modes." AES-256 is a kind of block cipher. It takes as input a 32-byte key and a 16-byte string, called the block and outputs a block. We use AES in a mode of operation in order to encrypt. The solutions above suggest using CBC, which is one example. Another is called CTR, and it's somewhat easier to use:
from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto import Random
# AES supports multiple key sizes: 16 (AES128), 24 (AES192), or 32 (AES256).
key_bytes = 32
# Takes as input a 32-byte key and an arbitrary-length plaintext and returns a
# pair (iv, ciphtertext). "iv" stands for initialization vector.
def encrypt(key, plaintext):
assert len(key) == key_bytes
# Choose a random, 16-byte IV.
iv = Random.new().read(AES.block_size)
# Convert the IV to a Python integer.
iv_int = int(binascii.hexlify(iv), 16)
# Create a new Counter object with IV = iv_int.
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
# Create AES-CTR cipher.
aes = AES.new(key, AES.MODE_CTR, counter=ctr)
# Encrypt and return IV and ciphertext.
ciphertext = aes.encrypt(plaintext)
return (iv, ciphertext)
# Takes as input a 32-byte key, a 16-byte IV, and a ciphertext, and outputs the
# corresponding plaintext.
def decrypt(key, iv, ciphertext):
assert len(key) == key_bytes
# Initialize counter for decryption. iv should be the same as the output of
# encrypt().
iv_int = int(iv.encode('hex'), 16)
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
# Create AES-CTR cipher.
aes = AES.new(key, AES.MODE_CTR, counter=ctr)
# Decrypt and return the plaintext.
plaintext = aes.decrypt(ciphertext)
return plaintext
(iv, ciphertext) = encrypt(key, 'hella')
print decrypt(key, iv, ciphertext)
This is often referred to as AES-CTR. I would advise caution in using AES-CBC with PyCrypto. The reason is that it requires you to specify the padding scheme, as exemplified by the other solutions given. In general, if you're not very careful about the padding, there are attacks that completely break encryption!
Now, it's important to note that the key must be a random, 32-byte string; a password does not suffice. Normally, the key is generated like so:
# Nominal way to generate a fresh key. This calls the system's random number
# generator (RNG).
key1 = Random.new().read(key_bytes)
A key may be derived from a password, too:
# It's also possible to derive a key from a password, but it's important that
# the password have high entropy, meaning difficult to predict.
password = "This is a rather weak password."
# For added # security, we add a "salt", which increases the entropy.
#
# In this example, we use the same RNG to produce the salt that we used to
# produce key1.
salt_bytes = 8
salt = Random.new().read(salt_bytes)
# Stands for "Password-based key derivation function 2"
key2 = PBKDF2(password, salt, key_bytes)
Some solutions above suggest using SHA-256 for deriving the key, but this is generally considered bad cryptographic practice.
Check out Wikipedia for more on modes of operation.
I am grateful for the other answers which inspired me, but it didn't work for me.
After spending hours trying to figure out how it works, I came up with the implementation below with the newest PyCryptodomex library (it is another story how I managed to set it up behind proxy, on Windows, in a virtualenv... phew)
It is working on your implementation. Remember to write down padding, encoding, and encrypting steps (and vice versa). You have to pack and unpack, keeping in mind the order.
import base64
import hashlib
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes
__key__ = hashlib.sha256(b'16-character key').digest()
def encrypt(raw):
BS = AES.block_size
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
raw = base64.b64encode(pad(raw).encode('utf8'))
iv = get_random_bytes(AES.block_size)
cipher = AES.new(key= __key__, mode= AES.MODE_CFB,iv= iv)
return base64.b64encode(iv + cipher.encrypt(raw))
def decrypt(enc):
unpad = lambda s: s[:-ord(s[-1:])]
enc = base64.b64decode(enc)
iv = enc[:AES.block_size]
cipher = AES.new(__key__, AES.MODE_CFB, iv)
return unpad(base64.b64decode(cipher.decrypt(enc[AES.block_size:])).decode('utf8'))
For someone who would like to use urlsafe_b64encode and urlsafe_b64decode, here are the version that're working for me (after spending some time with the unicode issue)
BS = 16
key = hashlib.md5(settings.SECRET_KEY).hexdigest()[:BS]
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[:-ord(s[len(s)-1:])]
class AESCipher:
def __init__(self, key):
self.key = key
def encrypt(self, raw):
raw = pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return base64.urlsafe_b64encode(iv + cipher.encrypt(raw))
def decrypt(self, enc):
enc = base64.urlsafe_b64decode(enc.encode('utf-8'))
iv = enc[:BS]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(enc[BS:]))
You can get a passphrase out of an arbitrary password by using a cryptographic hash function (NOT Python's builtin hash) like SHA-1 or SHA-256. Python includes support for both in its standard library:
import hashlib
hashlib.sha1("this is my awesome password").digest() # => a 20 byte string
hashlib.sha256("another awesome password").digest() # => a 32 byte string
You can truncate a cryptographic hash value just by using [:16] or [:24] and it will retain its security up to the length you specify.
Another take on this (heavily derived from solutions above) but
uses null for padding
does not use lambda (never been a fan)
tested with python 2.7 and 3.6.5
#!/usr/bin/python2.7
# you'll have to adjust for your setup, e.g., #!/usr/bin/python3
import base64, re
from Crypto.Cipher import AES
from Crypto import Random
from django.conf import settings
class AESCipher:
"""
Usage:
aes = AESCipher( settings.SECRET_KEY[:16], 32)
encryp_msg = aes.encrypt( 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppp' )
msg = aes.decrypt( encryp_msg )
print("'{}'".format(msg))
"""
def __init__(self, key, blk_sz):
self.key = key
self.blk_sz = blk_sz
def encrypt( self, raw ):
if raw is None or len(raw) == 0:
raise NameError("No value given to encrypt")
raw = raw + '\0' * (self.blk_sz - len(raw) % self.blk_sz)
raw = raw.encode('utf-8')
iv = Random.new().read( AES.block_size )
cipher = AES.new( self.key.encode('utf-8'), AES.MODE_CBC, iv )
return base64.b64encode( iv + cipher.encrypt( raw ) ).decode('utf-8')
def decrypt( self, enc ):
if enc is None or len(enc) == 0:
raise NameError("No value given to decrypt")
enc = base64.b64decode(enc)
iv = enc[:16]
cipher = AES.new(self.key.encode('utf-8'), AES.MODE_CBC, iv )
return re.sub(b'\x00*$', b'', cipher.decrypt( enc[16:])).decode('utf-8')
For the benefit of others, here is my decryption implementation which I got to by combining the answers of #Cyril and #Marcus. This assumes that this coming in via HTTP Request with the encryptedText quoted and base64 encoded.
import base64
import urllib2
from Crypto.Cipher import AES
def decrypt(quotedEncodedEncrypted):
key = 'SecretKey'
encodedEncrypted = urllib2.unquote(quotedEncodedEncrypted)
cipher = AES.new(key)
decrypted = cipher.decrypt(base64.b64decode(encodedEncrypted))[:16]
for i in range(1, len(base64.b64decode(encodedEncrypted))/16):
cipher = AES.new(key, AES.MODE_CBC, base64.b64decode(encodedEncrypted)[(i-1)*16:i*16])
decrypted += cipher.decrypt(base64.b64decode(encodedEncrypted)[i*16:])[:16]
return decrypted.strip()
I have used both Crypto and PyCryptodomex library and it is blazing fast...
import base64
import hashlib
from Cryptodome.Cipher import AES as domeAES
from Cryptodome.Random import get_random_bytes
from Crypto import Random
from Crypto.Cipher import AES as cryptoAES
BLOCK_SIZE = AES.block_size
key = "my_secret_key".encode()
__key__ = hashlib.sha256(key).digest()
print(__key__)
def encrypt(raw):
BS = cryptoAES.block_size
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
raw = base64.b64encode(pad(raw).encode('utf8'))
iv = get_random_bytes(cryptoAES.block_size)
cipher = cryptoAES.new(key= __key__, mode= cryptoAES.MODE_CFB,iv= iv)
a= base64.b64encode(iv + cipher.encrypt(raw))
IV = Random.new().read(BLOCK_SIZE)
aes = domeAES.new(__key__, domeAES.MODE_CFB, IV)
b = base64.b64encode(IV + aes.encrypt(a))
return b
def decrypt(enc):
passphrase = __key__
encrypted = base64.b64decode(enc)
IV = encrypted[:BLOCK_SIZE]
aes = domeAES.new(passphrase, domeAES.MODE_CFB, IV)
enc = aes.decrypt(encrypted[BLOCK_SIZE:])
unpad = lambda s: s[:-ord(s[-1:])]
enc = base64.b64decode(enc)
iv = enc[:cryptoAES.block_size]
cipher = cryptoAES.new(__key__, cryptoAES.MODE_CFB, iv)
b= unpad(base64.b64decode(cipher.decrypt(enc[cryptoAES.block_size:])).decode('utf8'))
return b
encrypted_data =encrypt("Hi Steven!!!!!")
print(encrypted_data)
print("=======")
decrypted_data = decrypt(encrypted_data)
print(decrypted_data)
You can use a scheme like PKCS#7 padding. You can use it instead the previous functions to pad (when doing encryption) and unpad (when doing decryption). I will provide the full source code below.
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
import pkcs7
class Encryption:
def __init__(self):
pass
def Encrypt(self, PlainText, SecurePassword):
pw_encode = SecurePassword.encode('utf-8')
text_encode = PlainText.encode('utf-8')
key = hashlib.sha256(pw_encode).digest()
iv = Random.new().read(AES.block_size)
cipher = AES.new(key, AES.MODE_CBC, iv)
pad_text = pkcs7.encode(text_encode)
msg = iv + cipher.encrypt(pad_text)
EncodeMsg = base64.b64encode(msg)
return EncodeMsg
def Decrypt(self, Encrypted, SecurePassword):
decodbase64 = base64.b64decode(Encrypted.decode("utf-8"))
pw_encode = SecurePassword.decode('utf-8')
iv = decodbase64[:AES.block_size]
key = hashlib.sha256(pw_encode).digest()
cipher = AES.new(key, AES.MODE_CBC, iv)
msg = cipher.decrypt(decodbase64[AES.block_size:])
pad_text = pkcs7.decode(msg)
decryptedString = pad_text.decode('utf-8')
return decryptedString
import StringIO
import binascii
def decode(text, k=16):
nl = len(text)
val = int(binascii.hexlify(text[-1]), 16)
if val > k:
raise ValueError('Input is not padded or padding is corrupt')
l = nl - val
return text[:l]
def encode(text, k=16):
l = len(text)
output = StringIO.StringIO()
val = k - (l % k)
for _ in xrange(val):
output.write('%02x' % val)
return text + binascii.unhexlify(output.getvalue())
See mnothic's answer.
Compatible UTF-8 encoding:
def _pad(self, s):
s = s.encode()
res = s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs).encode()
return res
PyCrypto is old and busted.
The cryptography has better support these days.
Here's another implementation. Note that this returns bytes, you'd need to use base64 to convert them to a string for transmission.
import os
import hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
_BLOCK_SIZE = 16
class AesStringCipher:
def __init__(self, key):
self._key = hashlib.sha256(key.encode()).digest()
def encrypt_str(self, raw:str) -> bytes:
iv = os.urandom(_BLOCK_SIZE)
cipher = Cipher(algorithms.AES(self._key), modes.CBC(iv), default_backend())
encryptor = cipher.encryptor()
raw = _pad(raw)
return iv + encryptor.update(raw.encode('utf-8')) + encryptor.finalize()
def decrypt_str(self, enc:bytes) -> str:
iv = enc[:_BLOCK_SIZE]
enc = enc[_BLOCK_SIZE:]
cipher = Cipher(algorithms.AES(self._key), modes.CBC(iv), default_backend())
decryptor = cipher.decryptor()
raw = decryptor.update(enc) + decryptor.finalize()
raw = raw.decode('utf-8')
return _unpad(raw)
def _pad(s:str) -> str:
padding = (_BLOCK_SIZE - (len(s) % _BLOCK_SIZE))
return s + padding * chr(padding)
def _unpad(s:str) -> str:
return s[:-ord(s[len(s)-1:])]
if __name__ == '__main__':
cipher = AesStringCipher('my secret password')
secret_msg = 'this is a super secret msg ...'
enc_msg = cipher.encrypt_str(secret_msg)
dec_msg = cipher.decrypt_str(enc_msg)
assert secret_msg == dec_msg
Encryption and decryption of Latin and special characters (Chinese) using AES-256 with utf8mb4:
For those who need to encrypt and decrypt Latin and special values, such as Chinese, here is a modification of the #MIkee code to do this task.
Remembering that UTF-8 alone does not handle this type of encoding.
import base64, re
from Crypto.Cipher import AES
from Crypto import Random
from django.conf import settings
import codecs
# Make utf8mb4 recognizable.
codecs.register(lambda name: codecs.lookup('utf8') if name == 'utf8mb4' else None)
class AESCipher:
def __init__(self, key, blk_sz):
self.key = key
self.blk_sz = blk_sz
def encrypt( self, raw ):
# raw is the main value
if raw is None or len(raw) == 0:
raise NameError("No value given to encrypt")
raw = raw + '\0' * (self.blk_sz - len(raw) % self.blk_sz)
raw = raw.encode('utf8mb4')
# Initialization vector to avoid same encrypt for same strings.
iv = Random.new().read( AES.block_size )
cipher = AES.new( self.key.encode('utf8mb4'), AES.MODE_CFB, iv )
return base64.b64encode( iv + cipher.encrypt( raw ) ).decode('utf8mb4')
def decrypt( self, enc ):
# enc is the encrypted value
if enc is None or len(enc) == 0:
raise NameError("No value given to decrypt")
enc = base64.b64decode(enc)
iv = enc[:16]
# AES.MODE_CFB that allows bigger length or Latin values
cipher = AES.new(self.key.encode('utf8mb4'), AES.MODE_CFB, iv )
return re.sub(b'\x00*$', b'', cipher.decrypt( enc[16:])).decode('utf8mb4')
Usage:
>>> from django.conf import settings
>>> from aesencryption import AESCipher
>>>
>>> aes = AESCipher(settings.SECRET_KEY[:16], 32)
>>>
>>> value = aes.encrypt('漢字')
>>>
>>> value
'hnuRwBjwAHDp5X0DmMF3lWzbjR0r81WlW9MRrWukgQwTL0ZI88oQaWvMfBM+W87w9JtSTw=='
>>> dec_value = aes.decrypt(value)
>>> dec_value
'漢字'
>>>
The same for Latin letters, such as ã, á, à, â, ã, ç, etc.
Attention point
Bear in mind that if you will store Latin values to your database, you need to set it to allow such type of data. Therefore, if your database is set as utf-8 it will not accept such type of data. You will need to change there as well.
from Crypto import Random
from Crypto.Cipher import AES
import base64
BLOCK_SIZE=16
def trans(key):
return md5.new(key).digest()
def encrypt(message, passphrase):
passphrase = trans(passphrase)
IV = Random.new().read(BLOCK_SIZE)
aes = AES.new(passphrase, AES.MODE_CFB, IV)
return base64.b64encode(IV + aes.encrypt(message))
def decrypt(encrypted, passphrase):
passphrase = trans(passphrase)
encrypted = base64.b64decode(encrypted)
IV = encrypted[:BLOCK_SIZE]
aes = AES.new(passphrase, AES.MODE_CFB, IV)
return aes.decrypt(encrypted[BLOCK_SIZE:])
You can use the new django-mirage-field package.

Categories

Resources