22

I'd like to know basically how can I encrypt data with a generated salt key and then decrypt it using python ?

i've gone trough a lot of websites and modules, and they all look great at encrpytion part, but none can decrypt as it seems.

My main concern is to have strong salt key, that'd be probably generated over few hunderd times, then use that key to encrypt data - in particular I'm looking into encrypting JSON encoded data with the salt key, sending the encrypted data to the other side ( listening client ) and then decrypt the data there based on the algorithm that is used to generate the salt key.

I've found that mcrypt module would work best with this, but there isn't much documentation for python-mcrypt module ( that's currently being outdated and not maintained).

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
Tabiko
  • 353
  • 1
  • 3
  • 8
  • 2
    you can have a look at pycrypto: https://www.dlitz.net/software/pycrypto/ – Gabi Purcaru Jun 21 '11 at 12:23
  • 1
    Yeah, keyboard not looking too great. :| Also to contribute, http://www.codekoala.com/blog/2009/aes-encryption-python-using-pycrypto/ nice introduction into pycrypto – Tabiko Jun 21 '11 at 12:29

2 Answers2

49

The short answer to your question is that you combine the password and the salt and hash them repeatedly to create your key. Then you append the salt onto the ciphertext so that you can generate the key for decryption. To ensure that I had the right answer, I made a few functions to do the work. They are given below.

In my answer, I've made use of pycrypto, so we need to import a few of those libraries.

import Crypto.Random
from Crypto.Cipher import AES
import hashlib

To aid readability, I've defined a few constants that I'll use later on.

# salt size in bytes
SALT_SIZE = 16

# number of iterations in the key generation
NUMBER_OF_ITERATIONS = 20

# the size multiple required for AES
AES_MULTIPLE = 16

To use a salt, I've done a password-based encryption scheme. I've used the RSA PKCS #5 standard for password-based encryption key generation and padding, adapted for the AES encryption algorithm.

To generate the key, the password and the salt are concatenated. This combination is hashed as many times as requested.

def generate_key(password, salt, iterations):
    assert iterations > 0

    key = password + salt

    for i in range(iterations):
        key = hashlib.sha256(key).digest()  

    return key

To pad the text, you figure out how many extra bytes you have beyond an even multiple of 16. If it is 0, you add 16 bytes of padding, if it is 1, you add 15, etc. This way you always add padding. The character you pad with is the character with the same value as the number of padding bytes (chr(padding_size)), to aid the removal of the padding at the end (ord(padded_text[-1])).

def pad_text(text, multiple):
    extra_bytes = len(text) % multiple

    padding_size = multiple - extra_bytes

    padding = chr(padding_size) * padding_size

    padded_text = text + padding

    return padded_text

def unpad_text(padded_text):
    padding_size = ord(padded_text[-1])

    text = padded_text[:-padding_size]

    return text

Encryption requires generating a random salt and using that along with the password to generate the encryption key. The text is padded using the above pad_text function and then encrypted with a cipher object. The ciphertext and salt are concatenated and returned as a result. If you wanted to send this as plaintext, you would need to encode it with base64.

def encrypt(plaintext, password):
    salt = Crypto.Random.get_random_bytes(SALT_SIZE)

    key = generate_key(password, salt, NUMBER_OF_ITERATIONS)

    cipher = AES.new(key, AES.MODE_ECB)

    padded_plaintext = pad_text(plaintext, AES_MULTIPLE)

    ciphertext = cipher.encrypt(padded_plaintext)

    ciphertext_with_salt = salt + ciphertext

    return ciphertext_with_salt

Decryption proceeds backwards, pulling the salt off of the ciphertext and using that to decrypt the remainder of the ciphertext. Then the plaintext is unpadded using unpad_text.

def decrypt(ciphertext, password):
    salt = ciphertext[0:SALT_SIZE]

    ciphertext_sans_salt = ciphertext[SALT_SIZE:]

    key = generate_key(password, salt, NUMBER_OF_ITERATIONS)

    cipher = AES.new(key, AES.MODE_ECB)

    padded_plaintext = cipher.decrypt(ciphertext_sans_salt)

    plaintext = unpad_text(padded_plaintext)

    return plaintext

Let me know if you have any other questions/clarifications.

101100
  • 2,666
  • 23
  • 28
  • 1
    Thanks for the detailed answer, I've already got the code running, however the generated key isn't gotten of the random generator. Thing with the random key is that it needs to be replicable on the other side, the listening script were the encrypted data is set, managed to get a working 5 minute valid key, which is generated with SHA256 from 15000 iterations. – Tabiko Jun 24 '11 at 05:46
  • The key is random, but generated from a known password, so you'd need to have the password on both the client and the server. The salt is used to make the key random and that means that anyone intercepting messages is much less likely to know if you ever repeat yourself. Are you trying to skip using the password? – 101100 Jun 24 '11 at 06:48
  • Actually yes, since this will be data sent from one server to another, with no complicated authentication, just interested to secure the sending data, as it may contain sensitive information ( no user details or anything, however more server related things as open ports, rootkit scans etc ). – Tabiko Jun 26 '11 at 17:49
  • There has to be some form of shared information to use encryption in this scenario. If the data is passing only one way, then I think the solution you are looking for is public-key or asymmetric encryption. In that case, only the receiver has the private key and can unlock the data, while the servers each have the same public key, which can only be used to encrypt, not decrypt. Does that sound like a good solution? If so, I can give you more details. – 101100 Jun 27 '11 at 18:55
  • 1
    The idea is good but this answer has many many short comings: 1) Too few iterations for key derivation from password. 2) naive key derivation function which is not very secure. PBKDF2 is better in that regard. 3) ECB mode is really insecure, because it is not randomized and each block is separately encrypted in exactly the same way ([scroll down to the penguin](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_.28ECB.29)). 4) There is no integrity checking for ciphertext manipulation or correct password. – Artjom B. Sep 06 '16 at 19:53
  • as of 2017, this solution appears not to work in Python 3 using pycryptodome. It is still helpful though. – Jeff Nov 10 '17 at 17:28
8

You don't need anything else than RNCryptor:

import rncryptor

data = '...'
password = '...'

# rncryptor.RNCryptor's methods
cryptor = rncryptor.RNCryptor()
encrypted_data = cryptor.encrypt(data, password)
decrypted_data = cryptor.decrypt(encrypted_data, password)
assert data == decrypted_data

# rncryptor's functions
encrypted_data = rncryptor.encrypt(data, password)
decrypted_data = rncryptor.decrypt(encrypted_data, password)
assert data == decrypted_data

It provides semantically secure (random salt and IV for each encryption) encryption and includes secure integrity checking (ciphertext cannot be manipulated without noticing) through HMAC.

RNCryptor also has a specific data format so you don't have to think about that and implementations in many languages.

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
  • 1
    This answer was underrated - thanks for this answer - that library - even if not updated for 2 years - simplifies and does under the covers what is stated in the accepted answer - and does it even better using PBKDF2 – João Antunes Aug 30 '18 at 08:18