Search code examples
javascriptpythonaescryptojspycrypto

Encrypt with CryptoJS, decrypt with PyCrypto (adjusting CryptoJS to PyCrypto defaults)


I am trying to decrypt on CryptoJS and encrypt in PyCrypto.

I saw this excellent answer which works like charm, the only problem is that it adjusts PyCrypto to work with CryptoJS, and I prefer to define rules how I expect the input and do as little as possible adjustments on PyCrypto with its defaults.

I thought to ask to send the iv, and with Zero Padding.

I wrote the following in JS (ES6):

  const iv = CryptoJS.enc.Hex.parse("1234567889012345");
  const key = 'aR1h7EefwlPNVkvTHwfs6w=='
  const encrypted = AES.encrypt(
    password,
    key,
    {
      iv,
      padding: CryptoJS.pad.NoPadding
    }
  );

  const payload = {password: encrypted, iv: iv};
  // make HTTPS POST call

Python:

def decrypt_from_cryptoJS(encrypted, iv):
    key = "aR1h7EefwlPNVkvTHwfs6w==".encode()
    aes = AES.new(key.encode(), AES.MODE_CBC, iv)
    encrypted = aes.decrypt(base64.b64decode(encrypted)))

However, I get ValueError: raise TypeError("Object type %s cannot be passed to C code" % type(data))

If I try to create the VI via: CryptoJS.lib.WordArray.random(16) and send it via toString() method of JS, I get:

Incorrect IV length (it must be 16 bytes long)

for initiating the AES

How can I decrypt in CryptoJS with minimum code adjustments in PyCrypto? I am not sure I am even going the right way..


Solution

    • On the CryptoJS-side, key and IV must be passed as WordArray-objects [1]. CryptoJS provides encoders for the conversion of strings into WordArray-objects and vice versa [2]. If the key is passed as a string, it is treated as passphrase and the actual key and IV are derived from it (in the referenced answer, the algorithm used for this is implemented on the Python-side [3]).

    • The key is Base64-encoded and has a length of 16 bytes after Base64-decoding, so AES-128 is used. For the conversion into a WordArray the Base64-encoder on the CryptoJS-side has to be used. On the Python-side, the key must be Base64-decoded (which still needs to be added to the posted code). Note: In the posted code the key is treated as a Utf8-string, so the key has a length of 24 bytes and AES-192 is used. If this is intended, the Utf8-encoder has to be used on the CryptoJS-side instead of the Base64-encoder. On the Python-side, a Base64-decoding of the key is then not necessary.

    • The IV used for encryption must also be used for decryption. In general, the IV is generated on the encryption side as a random byte sequence. Since the IV isn't secret, it's usually placed in front of the ciphertext and the concatenated data are sent to the recipient who separates both parts. On the CryptoJS-side, concatenation is easily done using WordArray#concat. On the Python-side, the separation is done by slicing (which still needs to be added to the posted code).

    • AES/CBC expects a plaintext whose length is an integer multiple of the blocksize (16 bytes). If the plaintext has a different length, a padding must be used. CryptoJS uses CBC-mode and PKCS7-padding by default, so that neither of these need to be explicitly specified [4]. PKCS7-padding is more reliable than Zero-padding [5]. However, if Zero-padding must be used instead (which is not clear to me from the question), it has to be specified explicitly with padding: CryptoJS.pad.ZeroPadding. PyCrypto doesn't remove the padding automatically, i.e. this must be done manually (which still needs to be added to the posted code). Note, that in contrast to PyCrypto, PyCryptodome supports padding [6].

    A possible JavaScript-code is:

    var password = "The quick brown fox jumps over the lazy dog";
    var iv = CryptoJS.lib.WordArray.random(16);                         // Generate a random 16 bytes IV
    var key = CryptoJS.enc.Base64.parse('aR1h7EefwlPNVkvTHwfs6w==');    // Interpret key as Base64 encoded
    
    var encrypted = CryptoJS.AES.encrypt(password, key, {iv: iv});      // Use CBC-mode and PKCS7-padding
    var joinedData = iv.clone().concat(encrypted.ciphertext);           // Concat IV and Ciphertext    
    var joinedDataB64 = CryptoJS.enc.Base64.stringify(joinedData);
    console.log(joinedDataB64.replace(/(.{64})/g, "$1\n"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js"></script>

    The appropriate Python-code could be:

    from Crypto.Cipher import AES
    import base64
    
    def decrypt_from_cryptoJS(encrypted, iv):
      key = base64.b64decode("aR1h7EefwlPNVkvTHwfs6w==")                # Interpret key as Base64 encoded   
      aes = AES.new(key, AES.MODE_CBC, iv)                              # Use CBC-mode
      encrypted = aes.decrypt(encrypted)                                # Remove Base64 decoding
      return encrypted
    
    def unpadPkcs7(data):
      return data[:-ord(data[-1])]
      #return data[:-data[-1]] #Python 3
    
    joinedDataB64 = "sbYEr73hZVKviuQ2rt5RcJ5ugpn7XBLTtZIKKk5JjTXmGojFkAS+dK0D8NNAET6bC/Ai4sx+El5Bzu4igT1S9g=="
    joinedData = base64.b64decode(joinedDataB64)
    iv = joinedData[:16]                                                # Determine IV from concatenated data
    encrypted = joinedData[16:]                                         # Determine ciphertext from concatenated data
    
    decrypted = unpadPkcs7(decrypt_from_cryptoJS(encrypted, iv))        # Decrypt and remove PKCS7-padding manually
    print decrypted
    #print(decrypted) #Python 3