My goal is to setup simple encryption with python 3.x, so I searched the web this weekend to get informations about RSA / AES and so on ... Actually, things that may look like a possibility to encrypt text data for transmission in a reasonable secure way .. without paranoia either, I'm not an expert just want to make sure that the stuff is pretty hard to read without the key !
Honestly, I do not know much about cryptography. After several hours of searching and collecting information and source code, my attempts failed because of invalid length problems or other conversion errors due to the examples provided in Python 2.7 . I found very few examples in python 3 and encryption methods used seemed to be not really appropriate or serious me.
I was finally was able to run the following code that accepts ISO 8859-1 coded characters. I actually encapsulates everything in UTF-8 encoding to avoid language issues .. I hope so ....
I would like to know if I'm on the right way of design and especially if data security is acceptable, again I'm not looking for the Great security solution, just want to protect my own personnal data and not to protect a military defense secret Lol !
Feel free to forward me your comments or suggestions and especially things I could have missed !
Thanks very much per advance.
Emmanuel (France)
Note: next step I'll try to send an RSA encrypted AES password to the recipient along with the text stream. As the AES password is different for each message the client needs to automatically translate it to be able to decode the cipher message. The AES password will be transmitted in RSA asymmetric encryption with the strongest possible key without performance breakdown. The aim is to transmit simple messages ( w/o base64 encoding) or large volumes of data in a reasonable timeframe.
@+ see you.
To execute the code bellow, you should have PyCrypto (python 3.2) installed
import os, base64, hashlib
from Crypto.Cipher import AES
class Aes(object):
# Crypte / décrypte un texte donné en AES mode CBC. Accepte l'encodage base64.
# Encrypts input text string & decrypts bytes encoded string with or without base64 encoding
# Author: emmanuel.brunet@live.fr - 12/2013
SALT_LENGTH = 64
DERIVATION_ROUNDS=10000
BLOCK_SIZE = 16
KEY_SIZE = 256
MODE = AES.MODE_CBC
def encrypt(self, source, aes_key, outfile=None, base64_encode=False):
'''
Crypte l'entrée source en AES mode CBC avec sortie encodage base64 / fichier facultative
@param str source: text to encode or text file path
@param bytes aes_key: password
@parm str outfile: disk file to write encoded text to. defaults to None
@param bool base64_encode: returns base64 encoded string if True (for emails) or bytes if False
@return bytes ciphertext: the bytes encoded string.
'''
'''
----------------------------
Inputs management
----------------------------
'''
if os.path.exists(source):
fp = open(source, 'rb')
input_text = fp.read()
fp.close()
else:
input_text = bytes(source, 'UTF-8')
if input_text == b'':
print('No data to encrypt')
return
padding_len = 16 - (len(input_text) % 16)
padded_text = str(input_text, 'UTF-8') + chr(padding_len) * padding_len
'''
---------------------------------------------------------
Computes the derived key (derived_key).
---------------------------------------------------------
Elle permet d'utiliser la clé initiale (aes_key) plusieurs
fois, une pour chaque bloc à encrypter.
---------------------------------------------------------
'''
salt = os.urandom(self.SALT_LENGTH)
derived_key = bytes(aes_key, 'UTF-8')
for unused in range(0,self.DERIVATION_ROUNDS):
derived_key = hashlib.sha256(derived_key + salt).digest()
derived_key = derived_key[:self.KEY_SIZE]
'''
----------------
Encrypt
----------------
'''
# The initialization vector should be random
iv = os.urandom(self.BLOCK_SIZE)
cipherSpec = AES.new(derived_key, self.MODE, iv)
cipher_text = cipherSpec.encrypt(padded_text)
cipher_text = cipher_text + iv + salt
'''
-------------------------
Output management
-------------------------
'''
if outfile is None:
'''
Returns cipher in base64 encoding. Useful for email management for instance
'''
if base64_encode:
return(base64.b64encode(cipher_text))
else:
return(cipher_text)
else:
'''
Writes result to disk
'''
fp = open(outfile, 'w')
if base64_encode:
fp.write(base64.b64encode(cipher_text))
else:
fp.write(cipher_text)
fp.close()
print('Cipher text saved in', outfile)
def decrypt(self, source, aes_key, outfile=None, base64_encode=False):
'''
Decrypts encoded string or data file
@param bytes or str source: encrypted bytes string to decode or file path
@param bytes aes_key: password
@parm str outfile: disk file to write encoded text to. defaults to None
@param bool base64_encode: cipher text is given base64 encoded (for mails content for examples)
@returns str secret_text: the decoding text string or None if invalid key given
'''
'''
---------------------------
Input management
---------------------------
'''
if type(source) == str and os.path.exists(source):
fp = open(source, 'rb')
ciphertext = fp.read()
fp.close()
elif type(source) == bytes:
ciphertext = source
else:
print('Invalid data source')
return
if base64_encode:
encoded_text = base64.b64decode(ciphertext)
else:
# decodedCiphertext = ciphertext.decode("hex")
encoded_text = ciphertext
'''
-------------------------
Computes derived key
-------------------------
'''
iv_start = len(encoded_text) - self.BLOCK_SIZE - self.SALT_LENGTH
salt_start = len(encoded_text) - self.SALT_LENGTH
data, iv, salt = encoded_text[:iv_start], encoded_text[iv_start:salt_start], encoded_text[salt_start:]
derived_key = bytes(aes_key, 'utf-8')
for unused in range(0, self.DERIVATION_ROUNDS):
derived_key = hashlib.sha256(derived_key + salt).digest()
derived_key = derived_key[:self.KEY_SIZE]
'''
-------------------------
Decrypt
-------------------------
'''
Cipher = AES.new(derived_key, self.MODE, iv)
padded_text = Cipher.decrypt(data)
padding_length = padded_text[-1]
secret_text = padded_text[:-padding_length]
'''
Si le flux n'est pas décodé (mot de passe invalide), la conversion UTF-8 plante ou au mieux on obtient un texte illisible
'''
try:
secret_text = str(secret_text, 'utf-8')
except:
return
if outfile is None:
return(secret_text)
else:
'''
Writes result to disk
'''
fp = open(outfile, 'w')
fp.write(secret_text)
fp.close()
final stuff
I've made the following changes:
- uses PBKDF2 as KDF with HMAC-sha512
- Fixed the constant issue
- mandatory packages are now : PyCypto & pbkdf2-1.3
I've tried many time to insert the new code block ... but it doesn't work. Very strange behaviour of the text editor.
I've fixed the AES.block_size constant issue.
I'm going to change the cipertext structure by placing the salt in front. Thanks very much.
Is Crypto.Random more appropriate than os.urandom – Emmanuel BRUNET Dec 10 '13 at 19:58