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)
In theory AES encryption in CTR mode allows for decrypting at any block index (that is you don't have to decrypt the whole encrypted data from the begining if you want to decrypt only from a certain position/block index). You just add to the initial value of the counter the desired block index and with this updated counter you can start decrypting.
In practice I am trying to do this in Python with the cryptography.hazmat library but I am not able to do it.
I managed to do this using the pycryptodome module by specifying the initial_value=0 for the full file encryption and inital_value=desiredAESBlockIndex when constructing the cipher for decrypting at a specified AES block index.
Here is my code (using the cryptography module):
import os
import sys
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def tstFullFileCryptography(aFilePath, aKey, aIV):
fileRawData = b''
with open(aFilePath, 'rb') as file:
fileRawData = file.read()
cipher = Cipher(algorithms.AES(aKey), modes.CTR(aIV))
encryptor = cipher.encryptor()
encryptedData = encryptor.update(fileRawData) + encryptor.finalize()
with open(aFilePath + ".cry", 'wb') as file:
file.write(encryptedData)
######### test #############
decryptor = cipher.decryptor()
decryptedData = decryptor.update(encryptedData) + decryptor.finalize()
if decryptedData == fileRawData:
print("OK - cryptography: full encryption")
else:
print("ERROR - cryptography: full encryption")
def tstCryptographyFromIndex(aFilePath, aKey, aIV):
algAES = algorithms.AES(aKey)
# the desired AES block index and size of data to decrypt
tstAESBlockIndex = 1
tstSizeInBytes = 16 * 13
tstRaw = b''
with open(aFilePath, 'rb') as file:
file.seek(tstAESBlockIndex * algAES.block_size)
tstRaw = file.read(tstSizeInBytes)
tstEncryptedData = b''
with open(aFilePath + ".cry", 'rb') as file:
file.seek(tstAESBlockIndex * algAES.block_size)
tstEncryptedData = file.read(tstSizeInBytes)
print(f"\ntstEncryptedData: {tstEncryptedData}\n")
################ decrypt from desired block index; advance iv
ivNum = int.from_bytes(aIV, byteorder=sys.byteorder)
incrementedIVNum = ivNum + tstAESBlockIndex
incrementedIV = incrementedIVNum.to_bytes(16, byteorder=sys.byteorder)
decryptor = Cipher(algAES, modes.CTR(incrementedIV)).decryptor()
tstDecryptedData = decryptor.update(tstEncryptedData) + decryptor.finalize()
print(f"tstDecryptedData: {tstDecryptedData}\n")
if tstRaw == tstDecryptedData:
print("OK - cryptography: index decryption")
else:
print("ERROR - cryptography: index decryption")
def main():
key = os.urandom(32)
iv = os.urandom(16)
tstFullFileCryptography("C:\\work\\python\\tstdata\\video.mp4", key, iv)
tstCryptographyFromIndex("C:\\work\\python\\tstdata\\video.mp4", key, iv)
return 0
if __name__ == "__main__":
main()
Here is the code after fixing You had 2 errors in your code that cause your decryption and file seeking to be incorrect.
import os
import sys
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def tstFullFileCryptography(aFilePath, aKey, aIV):
fileRawData = b''
with open(aFilePath, 'rb') as file:
fileRawData = file.read()
cipher = Cipher(algorithms.AES(aKey), modes.CTR(aIV))
encryptor = cipher.encryptor()
encryptedData = encryptor.update(fileRawData) + encryptor.finalize()
with open(aFilePath + ".cry", 'wb') as file:
file.write(encryptedData)
######### test #############
decryptor = cipher.decryptor()
decryptedData = decryptor.update(encryptedData) + decryptor.finalize()
if decryptedData == fileRawData:
print("OK - cryptography: full encryption")
else:
print("ERROR - cryptography: full encryption")
def tstCryptographyFromIndex(aFilePath, aKey, aIV):
algAES = algorithms.AES(aKey)
# the desired AES block index and size of data to decrypt
tstAESBlockIndex = 1
tstSizeInBytes = 16 * 13
tstRaw = b''
with open(aFilePath, 'rb') as file:
file.seek(tstAESBlockIndex * 16)
tstRaw = file.read(tstSizeInBytes)
tstEncryptedData = b''
with open(aFilePath + ".cry", 'rb') as file:
file.seek(tstAESBlockIndex * 16)
tstEncryptedData = file.read(tstSizeInBytes)
print(f"\ntstEncryptedData: {tstEncryptedData}\n")
################ decrypt from desired block index; advance iv
ivNum = int.from_bytes(aIV, byteorder="big")
incrementedIVNum = ivNum + tstAESBlockIndex
incrementedIV = incrementedIVNum.to_bytes(16, byteorder="big")
decryptor = Cipher(algAES, modes.CTR(incrementedIV)).decryptor()
tstDecryptedData = decryptor.update(tstEncryptedData) + decryptor.finalize()
print(f"tstDecryptedData: {tstDecryptedData}\n")
if tstRaw == tstDecryptedData:
print("OK - cryptography: index decryption")
else:
print("ERROR - cryptography: index decryption")
def main():
key = os.urandom(32)
iv = os.urandom(16)
tstFullFileCryptography("text", key, iv)
tstCryptographyFromIndex("text", key, iv)
return 0
if __name__ == "__main__":
main()
First error is that you use incorrect block_size to seek since seeking is by byte level while block_size is at bit level = 128 in your case.
Second error is sys.byte_order which is little in case you use x86 processor while all the encryption ops were done in big endianness so you are basically reversing bytes leading to incorrect decryption fixing both errors now let me decrypt successfully.
so I have been trying to build an AES encryption program based off of the github pycrypto guide link to github however when I go to decode an error shows up:
Traceback (most recent call last):
File "/home/pi/Desktop/aes/newAES.py", line 24, in <module>
print(decrypt(key,msg,iv))
File "/home/pi/Desktop/aes/newAES.py", line 13, in decrypt
cipher = AES.new(key,AES.MODE_CFB)
File "/usr/lib/python3/dist-packages/Crypto/Cipher/AES.py", line 94, in new
return AESCipher(key, *args, **kwargs)
File "/usr/lib/python3/dist-packages/Crypto/Cipher/AES.py", line 59, in __init__
blockalgo.BlockAlgo.__init__(self, _AES, key, *args, **kwargs)
File "/usr/lib/python3/dist-packages/Crypto/Cipher/blockalgo.py", line 141, in __init__
self._cipher = factory.new(key, *args, **kwargs)
ValueError: IV must be 16 bytes long
my code is:
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
def encrypt(key,msg):
if key == 0:
key=get_random_bytes(16)
print("key: "+key)
iv = get_random_bytes(16)
print('iv: '+str(iv))
cipher = AES.new(key,AES.MODE_CFB,iv)
ciphertext= cipher.decrypt(msg)
return("your encrypted message: "+str(ciphertext))
def decrypt(key,ciphertext,iv):
cipher = AES.new(key,AES.MODE_CFB)
msg = cipher.decrypt(ciphertext)
ed = input('(e)ncrypt or (d)ecrypt: ')
if ed=='e':
key = input('16 digit key: ')
msg = input('message: ')
print(encrypt(key,msg))
elif ed =='d':
key = input('16 digit key: ')
iv = bytes(input('iv: '),'utf-8')
msg = bytes(input('encrypted message:'),'utf-8')
print(decrypt(key,msg,iv))
I would appreciate any help offered on solving this issue, hopefully it isn't some stupid error
The problem with iv is that it consists of random bytes, but it is being read into your program as a string. Calling bytes on that string does not do what you expect.
>>> iv = b'\xba\x0eyO8\x17\xcf\x97=\xf2&l34#('
>>> siv = str(iv)
>>> siv
"b'\\xba\\x0eyO8\\x17\\xcf\\x97=\\xf2&l34#('" # Note 'b' is part of the string
>>> biv = bytes(siv, 'utf-8')
>>> biv
b"b'\\xba\\x0eyO8\\x17\\xcf\\x97=\\xf2&l34#('" # Now there are two 'b's!
You can resolve this by using ast.literal_eval:
>>> ast.literal_eval(siv)
b'\xba\x0eyO8\x17\xcf\x97=\xf2&l34#('
Here's a working version of your code - I removed the need to copy/paste iv, but the same observations about inputting bytes applies to the ciphertext.
import ast
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
def encrypt(key, msg):
iv = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CFB, iv)
ciphertext = cipher.encrypt(msg) # Use the right method here
return iv + ciphertext
def decrypt(key, ciphertext):
iv = ciphertext[:16]
ciphertext = ciphertext[16:]
cipher = AES.new(key, AES.MODE_CFB, iv)
msg = cipher.decrypt(ciphertext)
return msg.decode("utf-8")
if __name__ == "__main__":
ed = input("(e)ncrypt or (d)ecrypt: ")
if ed == "e":
key = input("16 digit key: ")
msg = input("message: ")
print("Encrypted message: ", encrypt(key, msg))
elif ed == "d":
key = input("16 digit key: ")
smsg = input("encrypted message: ")
msg = ast.literal_eval(smsg)
print("Decrypted message: ", decrypt(key, msg))
The code in action:
(e)ncrypt or (d)ecrypt: e
16 digit key: abcdabcdabcdabcd
message: Spam, spam, spam
Encrypted message: b'\xa4?\xa9RI>\x1f\xb5*\xb2,NWN\x0c\xfd"yB|\x1f\x82\x96\xd5\xb4\xd4\x1d&\x8bM\xdb\x07'
(e)ncrypt or (d)ecrypt: d
16 digit key: abcdabcdabcdabcd
encrypted message: b'\xa4?\xa9RI>\x1f\xb5*\xb2,NWN\x0c\xfd"yB|\x1f\x82\x96\xd5\xb4\xd4\x1d&\x8bM\xdb\x07'
Decrypted message: Spam, spam, spam
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.