5

I'm encrypting a string in a web application using CryptoJS (v 2.3), and I need to decrypt it on the server in Python, so I'm using PyCrypto. I feel like I'm missing something because I can't can it working.

Here's the JS:

Crypto.AES.encrypt('1234567890123456', '1234567890123456',
                   {mode: new Crypto.mode.CBC(Crypto.pad.ZeroPadding)})
// output: "wRbCMWcWbDTmgXKCjQ3Pd//aRasZ4mQr57DgTfIvRYE="

The python:

from Crypto.Cipher import AES
import base64
decryptor = AES.new('1234567890123456', AES.MODE_CBC)
decryptor.decrypt(base64.b64decode("wRbCMWcWbDTmgXKCjQ3Pd//aRasZ4mQr57DgTfIvRYE="))
# output: '\xd0\xc2\x1ew\xbb\xf1\xf2\x9a\xb9\xb6\xdc\x15l\xe7\xf3\xfa\xed\xe4\xf5j\x826\xde(m\xdf\xdc_\x9e\xd3\xb1'
Artjom B.
  • 61,146
  • 24
  • 125
  • 222
ian
  • 168
  • 1
  • 1
  • 7

3 Answers3

13

Here is a version with CryptoJS 3.1.2. Always beware of the following things (use the same in both languages):

  • Mode of operation (CBC in this case)
  • Padding (Zero Padding in this case; better use PKCS#7 padding)
  • Key (the same derivation function or clear key)
  • Encoding (same encoding for key, plaintext, ciphertext, ...)
  • IV (generated during encryption, passed for decryption)

If a string is passed as the key argument to the CryptoJS encrypt() function, the string is used to derive the actual key to be used for encryption. If you wish to use a key (valid sizes are 16, 24 and 32 byte), then you need to pass it as a WordArray.

The result of the CryptoJS encryption is an OpenSSL formatted ciphertext string. To get the actual ciphertext from it, you need to access the ciphertext property on it.

The IV must be random for each encryption so that it is semantically secure. That way attackers cannot say whether the same plaintext that was encrypted multiple times is actually the same plaintext when only looking at the ciphertext.

Below is an example that I have made.

JavaScript:

var key = CryptoJS.enc.Utf8.parse('1234567890123456'); // TODO change to something with more entropy

function encrypt(msgString, key) {
    // msgString is expected to be Utf8 encoded
    var iv = CryptoJS.lib.WordArray.random(16);
    var encrypted = CryptoJS.AES.encrypt(msgString, key, {
        iv: iv
    });
    return iv.concat(encrypted.ciphertext).toString(CryptoJS.enc.Base64);
}

function decrypt(ciphertextStr, key) {
    var ciphertext = CryptoJS.enc.Base64.parse(ciphertextStr);

    // 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;
    
    // decryption
    var decrypted = CryptoJS.AES.decrypt({ciphertext: ciphertext}, key, {
        iv: iv
    });
    return decrypted.toString(CryptoJS.enc.Utf8);
}

Python 2 code with pycrypto:

BLOCK_SIZE = 16
key = b"1234567890123456" # TODO change to something with more entropy

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

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

def encrypt(message, key):
    IV = Random.new().read(BLOCK_SIZE)
    aes = AES.new(key, AES.MODE_CBC, IV)
    return base64.b64encode(IV + aes.encrypt(pad(message)))

def decrypt(encrypted, key):
    encrypted = base64.b64decode(encrypted)
    IV = encrypted[:BLOCK_SIZE]
    aes = AES.new(key, AES.MODE_CBC, IV)
    return unpad(aes.decrypt(encrypted[BLOCK_SIZE:]))

Warning: Keep in mind that both python2 and pycrypto are obsolete, so the code has to be adjusted to fit python3 and pycryptodome.


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
  • I am not able to pass IV and passphrase in AES.new in python as it requires bytes instead of string. – Sanjay Sharma Nov 19 '19 at 15:37
  • I have a similar problem here https://stackoverflow.com/questions/65844902/cryptojs-decode-with-pycrypto But I have some additional settings in CryptoJS CryptoJS.algo.AES.keySize = 32 CryptoJS.algo.EvpKDF.cfg.iterations = 1e4 CryptoJS.algo.EvpKDF.cfg.keySize = 32; And I don't know how to realize them with python –  GhostKU Jan 24 '21 at 22:23
  • 1
    In case you face `TypeError` with above solution such as `TypeError: Object type cannot be passed to C code` or `TypeError: ord() expected string of length 1, but int found` , you can update your code as following: * In encrypt function: `return base64.b64encode(IV + aes.encrypt(pad(message).encode())).decode()` * In decrypt function: `return unpad(aes.decrypt(encrypted[BLOCK_SIZE:]).decode())` Thanks @Artjom B. ! your solution saved my day. – Rukamakama Jul 12 '21 at 15:45
  • @Rukamakama Yes, my answer is quite old and targeted Python 2 with PyCrypto. Both are not obsolete and so the code does not work, but should be easily adaptable to Python 3 and pycryptodome quite easily. – Artjom B. Jul 19 '21 at 14:26
0

I had to port a Javascript implementation of AES encryption/decryption which was using crypto-js library, to Python3.

Basically, my approach was to run the debugger on the existing JS code and look at variables getting filled in each step. I was able to figure out the equivalent methods to do the same in python as well.

Here is how I ported it using pycryptodome library which has some useful features.

  • AES.js
var CryptoJS = require("crypto-js");
var Base64 = require("js-base64");

function decrypt(str, secret) {
    try {
        var _strkey = Base64.decode(secret);
        var reb64 = CryptoJS.enc.Hex.parse(str);
        var text = reb64.toString(CryptoJS.enc.Base64);
        var Key = CryptoJS.enc.Base64.parse(_strkey.split(",")[1]); //secret key
        var IV = CryptoJS.enc.Base64.parse(_strkey.split(",")[0]); //16 digit
        var decryptedText = CryptoJS.AES.decrypt(text, Key, { keySize: 128 / 8, iv: IV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
        return decryptedText.toString(CryptoJS.enc.Utf8); //binascii.unhexlify(decryptedText)
    } catch (e) {
        console.log("Error", e)
    }
}

function encrypt(str, secret) {
    str = Math.random().toString(36).substring(2, 10) + str;
    var _strkey = Base64.decode(secret);
    _strkey.split(",");
    var text = CryptoJS.enc.Utf8.parse(str);
    var Key = CryptoJS.enc.Base64.parse(_strkey.split(",")[1]); //secret key
    var IV = CryptoJS.enc.Base64.parse(_strkey.split(",")[0]); //16 digit
    var encryptedText = CryptoJS.AES.encrypt(text, Key, { keySize: 128 / 8, iv: IV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
    var b64 = encryptedText.toString();
    var e64 = CryptoJS.enc.Base64.parse(b64);
    var eHex = e64.toLocaleString(CryptoJS.enc.Hex);
    return eHex.toUpperCase();
}

const secret = "V1VWTVRFOVhJRk5WUWsxQlVrbE9SUT09LFRrOUNUMFJaSUZkSlRFd2dTMDVQVnc9PQ=="
const data = "THIS IS MY SECRET MESSAGE!"
encData = EncryptText2(data, secret)
decData = DecryptText2(encData, secret)
console.log("encryptedData", encData)
console.log("decryptedData", decData)
  • AESify.py
import string
import random
import base64
import binascii
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

class AESify:
  def __init__(self, key=None, iv=None,secret = None, block_len=16, salt_len= 8):
    self.key = key
    self.iv = iv
    self.salt_len = salt_len
    self.block_len = block_len
    self.mode = AES.MODE_CBC
    if(secret):
      self.useSecret(secret)
    if(self.key is None and self.iv is None):
      raise Exception("No key , IV pair or secret provided")
      
  @staticmethod
  def makeSecret(key, iv):
    if(len(key) % 8 != 0):
      raise Exception("Key length must be a mutliple of 8")
    if(len(iv) % 8 != 0):
      raise Exception("Initial vector must be a multiple of 8")
    key64 = base64.b64encode(key.encode()).decode()
    iv64 = base64.b64encode(iv.encode()).decode()
    secret = iv64 + "," + key64
    secret64 = base64.b64encode(secret.encode()).decode()
    return secret64

  def useSecret(self, secret):
    iv64, key64 = base64.b64decode(secret).decode().split(",") # decode and convert to string
    self.iv = base64.b64decode(iv64)
    self.key = base64.b64decode(key64)
    return self

  def encrypt(self, text):
    text = self.add_salt(text, self.salt_len)
    cipher = AES.new(self.key, self.mode, self.iv)
    text = cipher.encrypt(pad(text.encode('utf-8'), self.block_len))
    return binascii.hexlify(text).decode()
  
  def decrypt(self, data):
    text = binascii.unhexlify(data) # UNHEX and convert the encrypted data to text
    cipher = AES.new(self.key, self.mode, self.iv)
    return unpad(cipher.decrypt(text), self.block_len).decode('utf-8')[self.salt_len:] 
  
  def add_salt(self, text, salt_len):
    # pre-pad with random salt
    salt = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(salt_len))
    text = salt + text
    return text
  • main.py
from AESify import AESify

key , iv = "NOBODY WILL KNOW", "YELLOW SUBMARINE"
# contains IV and key
secret = AESify.makeSecret(key, iv)
aes = AESify(secret= secret, block_len=16, salt_len=4)
msg = "THIS IS MY SECRET MESSAGE"
encrypted = aes.encrypt(msg)
decrypted = aes.decrypt(encrypted)
print(f"{secret=}")
print(f"{encrypted=}")
print(f"{decrypted=}")
Rayan
  • 11
  • 1
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/31375314) – Vaibhav Jadhav Mar 29 '22 at 05:54
-2

Note : salt , iv , padding should be same in js and python

generate salt and iv value and convert it into a byte string uisng CryptoJS.enc.Utf8.parse()

js file

var encrypted = CryptoJS.AES.encrypt(JSON.stringify(json_data), CryptoJS.enc.Utf8.parse(data['salt']) , { iv: CryptoJS.enc.Utf8.parse(data['iv']) , mode: CryptoJS.mode.CBC , padding: CryptoJS.pad.Pkcs7});
en_data = encrypted.ciphertext.toString(CryptoJS.enc.Base64)

send this encrypted data to the python file

python file

from Crypto.Util.Padding import pad, unpad

ct = request.POST['encrypted_data']
data = base64.b64decode(ct)
cipher1 = AES.new(salt, AES.MODE_CBC, iv)
pt = unpad(cipher2.decrypt(data), 16)
data = json.loads(pt.decode('utf-8'))
        

pad and upad in pycrypto by default uses pkcs#7

salt and iv value should in byte string