8

I have need to simply encrypt some text in python and being able to decrypt in JavaScrypt.

So far I have in python:

from Crypto import Random
from Crypto.Cipher import AES
import base64

BLOCK_SIZE = 16
key = "1234567890123456" # want to be 16 chars
textToEncrypt = "This is text to encrypt"

def encrypt(message, passphrase):
    # passphrase MUST be 16, 24 or 32 bytes long, how can I do that ?
    IV = Random.new().read(BLOCK_SIZE)
    aes = AES.new(passphrase, AES.MODE_CFB, IV)
    return base64.b64encode(aes.encrypt(message))

def decrypt(encrypted, passphrase):
    IV = Random.new().read(BLOCK_SIZE)
    aes = AES.new(passphrase, AES.MODE_CFB, IV)
    return aes.decrypt(base64.b64decode(encrypted))

print encrypt( textToEncrypt, key )

this is producing text: ZF9as5JII5TlqcB5tAd4sxPuBXd5TrgE

in JavaScript:

<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>
<script>
    var decrypted = CryptoJS.AES.decrypt( "ZF9as5JII5TlqcB5tAd4sxPuBXd5TrgE", "1234567890123456");
    console.log ( decrypted.toString( CryptoJS.enc.Utf8 ) );
</script>

however it does not produce original string (empty string instead). What I am doing wrong ?

Is it focusing on AES is a best idea - I will be happy if I have some kind of encryption that will blur data.

bensiu
  • 24,660
  • 56
  • 77
  • 117
  • I notice that the string being produced doesn't match the string you are trying to decrypt in JS. Is that a typo? I'm wondering if perhaps CryptoJS.AES.decrypt might be throwing an exception - is your console clean? – PhysicalEd Jun 22 '15 at 21:36
  • You are Base64 encoding. This looks like what you need: https://gist.github.com/joecliff/10948117. Actually, that doesn't show how to decrypt with a key, but you most likely still have to decode the Base64 string first. – Davin Tryon Jun 22 '15 at 21:41
  • function `encrypt` returns base64 encoded data. see to [decode](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob) in js. – Nizam Mohamed Jun 22 '15 at 21:42
  • string not matched was type in question code only - fixed – bensiu Jun 22 '15 at 21:46
  • Could you elaborate more about Base64 decoding... – bensiu Jun 22 '15 at 21:47
  • 1
    When you encrypt you Base64 encode `base64.b64encode(aes.encrypt(message))`. When you are decrypting in CryptoJS the decrypt method does *not* take a Base64 encoded value. You need to decode the value first. – Davin Tryon Jun 22 '15 at 21:55

3 Answers3

18

There are many problems with your Python code and CryptoJS code:

  • You use a random IV to encrypt some plaintext in Python. If you want to retrieve that plaintext, you need to use the same IV during decryption. The plaintext cannot be recovered without the IV. Usually the IV is simply prepended to the ciphertext, because it doesn't have to be secret. So you need to read the IV during decryption and not generate a new one.

  • You use CBC mode in CryptoJS (default) instead of CFB mode. The mode has to be the same. The other tricky part is that CFB mode is parametrized with a segment size. PyCrypto uses by default 8-bit segments (CFB8), but CryptoJS is only implemented for fixed segments of 128-bit (CFB128). Since the PyCrypto version is variable, you need to change that.

  • The CryptoJS decrypt() function expects as ciphertext either an OpenSSL formatted string or a CipherParams object. Since you don't have an OpenSSL formatted string, you have to convert the ciphertext into an object.

  • The key for CryptoJS is expected to be a WordArray and not a string.

  • Use the same padding. PyCrypto doesn't pad the plaintext if CFB8 is used, but padding is needed when CFB128 is used. CryptoJS uses PKCS#7 padding by default, so you only need to implement that padding in python.

Python code (for version 2):

def pad(data):
    length = 16 - (len(data) % 16)
    return data + chr(length)*length

def unpad(data):
    return data[:-ord(data[-1])]

def encrypt(message, passphrase):
    IV = Random.new().read(BLOCK_SIZE)
    aes = AES.new(passphrase, AES.MODE_CFB, IV, segment_size=128)
    return base64.b64encode(IV + aes.encrypt(pad(message)))

def decrypt(encrypted, passphrase):
    encrypted = base64.b64decode(encrypted)
    IV = encrypted[:BLOCK_SIZE]
    aes = AES.new(passphrase, AES.MODE_CFB, IV, segment_size=128)
    return unpad(aes.decrypt(encrypted[BLOCK_SIZE:]))

JavaScript code:

<script src="https://cdn.rawgit.com/CryptoStore/crypto-js/3.1.2/build/rollups/aes.js"></script>
<script src="https://cdn.rawgit.com/CryptoStore/crypto-js/3.1.2/build/components/mode-cfb-min.js"></script>
<script>
    var base64ciphertextFromPython = "...";
    var ciphertext = CryptoJS.enc.Base64.parse(base64ciphertextFromPython);

    // split iv and ciphertext
    var iv = ciphertext.clone();
    iv.sigBytes = 16;
    iv.clamp();
    ciphertext.words.splice(0, 4); // delete 4 words = 16 bytes
    ciphertext.sigBytes -= 16;

    var key = CryptoJS.enc.Utf8.parse("1234567890123456");

    // decryption
    var decrypted = CryptoJS.AES.decrypt({ciphertext: ciphertext}, key, {
      iv: iv,
      mode: CryptoJS.mode.CFB
    });
    console.log ( decrypted.toString(CryptoJS.enc.Utf8));
</script>

Other considerations:

It seems that you want to use a passphrase as a key. Passphrases are usually human readable, but keys are not. You can derive a key from a passphrase with functions such as PBKDF2, bcrypt or scrypt.

The code above is not fully secure, because it lacks authentication. Unauthenticated ciphertexts may lead to viable attacks and unnoticed data manipulation. Usually the an encrypt-then-MAC scheme is employed with a good MAC function such as HMAC-SHA256.

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
  • Thank you for response but it does not work - you mentioned missing iv - I do not see usage in code - where need to be used ? – bensiu Jun 22 '15 at 22:29
  • I've extended the example – Artjom B. Jun 22 '15 at 22:36
  • One more time - thank you - however I still have "Uncaught TypeError: Cannot read property '0' of undefined" when executing CryptoJS.AES.decrypt({ciphertext: ciphertext}, key); line and I am not sure where problem is.... – bensiu Jun 23 '15 at 13:25
  • It should work now. Turns out, there were more things that were different. – Artjom B. Jun 23 '15 at 18:28
  • Any chance you could include an encrypt function for JS? This answer has been extremely helpful for me. – Fmstrat Jul 30 '18 at 14:38
  • I started this, and it seems to work, but would love to know if this is the best way: https://pastebin.com/raw/XLwsfJRi – Fmstrat Jul 30 '18 at 15:37
  • @Fmstrat It looks correct, but not very secure. This does provide confidentiality, but you need a MAC to achieve authenticity of messages. – Artjom B. Aug 01 '18 at 17:44
15

(1 Year later but I hope this works for someone)

First of all, thanks Artjom B. your post helps me a lot. And Like OP, I have the same same problem Python server endonding and Javascript client decoding. This was my solution:


Python 3.x (Server)

I used an excplicit PKCS7 encode for padding, why? because I want to be sure Im using the same padding enconding and decoding, this is the link where I found it http://programmerin.blogspot.com.co/2011/08/python-padding-with-pkcs7.html .

Then, like Artjom B. said, be sure about your segment size, IV size and AES mode (CBC for me),

This is the code:

def encrypt_val(clear_text):
    master_key = '1234567890123456' 
    encoder = PKCS7Encoder()
    raw = encoder.encode(clear_text)
    iv = Random.new().read( 16 )
    cipher = AES.new( master_key, AES.MODE_CBC, iv, segment_size=128 )
    return base64.b64encode( iv + cipher.encrypt( raw ) ) 

Note than your are enconding on base64 the concatenation of IV and encryption data.


Javascript (client)

function decryptMsg (data) {
    master_key = '1234567890123456';

    // Decode the base64 data so we can separate iv and crypt text.
    var rawData = atob(data);
    // Split by 16 because my IV size
    var iv = rawData.substring(0, 16);
    var crypttext = rawData.substring(16);

    //Parsers
    crypttext = CryptoJS.enc.Latin1.parse(crypttext);
    iv = CryptoJS.enc.Latin1.parse(iv); 
    key = CryptoJS.enc.Utf8.parse(master_key);

    // Decrypt
    var plaintextArray = CryptoJS.AES.decrypt(
      { ciphertext:  crypttext},
      key,
      {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7}
    );

    // Can be Utf8 too
    output_plaintext = CryptoJS.enc.Latin1.stringify(plaintextArray);
    console.log("plain text : " + output_plaintext);
}

One of my main problem was keep in mind all kind of encoding and decoding data, for example, I didn't know that the master_key on client side was to be parse with Utf8.

Ander2
  • 5,569
  • 2
  • 23
  • 42
LKS
  • 151
  • 1
  • 3
0

//First pip install pycryptodome -- (pycrypto is obsolete and gives issues) // pip install pkcs7

from Crypto import Random
from Crypto.Cipher import AES
import base64
from pkcs7 import PKCS7Encoder
from app_settings.views import retrieve_settings # my custom settings

app_secrets = retrieve_settings(file_name='secrets');


def encrypt_data(text_data):
                    #limit to 16 bytes because my encryption key was too long
                    #yours could just be 'abcdefghwhatever' 
    encryption_key = app_secrets['ENCRYPTION_KEY'][:16]; 

    #convert to bytes. same as bytes(encryption_key, 'utf-8')
    encryption_key = str.encode(encryption_key); 
    
    #pad
    encoder = PKCS7Encoder();
    raw = encoder.encode(text_data) # Padding
    iv = Random.new().read(AES.block_size ) #AES.block_size defaults to 16

                                 # no need to set segment_size=BLAH
    cipher = AES.new( encryption_key, AES.MODE_CBC, iv ) 
    encrypted_text = base64.b64encode( iv + cipher.encrypt( str.encode(raw) ) ) 
    return encrypted_text;
Growyn
  • 31
  • 3