Search code examples
encryptioncryptographyaescryptojspycrypto

Encryption with Crypto.js and pycrypto generates different encryption cipher text


I am trying to generate encrypted text in Node using Crypto.js using its AES algorithm. It works in Js for encryption and decryption both. Similarly I tried implementing same in Python using pycrypto and it does encryption and decryption both in python. But the problem arises when I want to use encrypted cipher text from JS to decrypt in Python. The problem is that the encrypted text generated in JS is different from what is generated in Python.

Here is the JS based usage of AES:

import CryptoJS from "crypto-js";

let str = "lol";
let salt = "ABCDEFGHIJKLMNOP";
let key = "ABCDEFGHIJKLMNOPABCDEFGHIJKLMNOP";

salt = CryptoJS.enc.Hex.parse(salt);
key = CryptoJS.enc.Hex.parse(key);

const options = {
  iv: salt,
  padding: CryptoJS.pad.ZeroPadding,
  mode: CryptoJS.mode.CFB,
};

// function to encrypt the string
function encrypt(str, salt, key) {
  const cipher = CryptoJS.AES.encrypt(str, key, options);
  console.log("cipher", CryptoJS.enc.Hex.stringify(cipher.ciphertext));
  return cipher.ciphertext.toString(CryptoJS.enc.Base64);
}

// function to decrypt the string
function decrypt(str, salt, key) {
  const cipher = CryptoJS.AES.decrypt(str, key, options);
  return cipher.toString(CryptoJS.enc.Utf8);
}

const encrypted = encrypt(str, salt, key);
console.log("encrypted", encrypted);


const decrypted = decrypt(encrypted, salt, key);
console.log("decrypted", decrypted);

// OUTPUT
// encrypted sd4Xpz/ws8x2j+cgF17t6A==
// decrypted lol

Here is the Python based usage of AES:

from Crypto.Cipher import AES
from base64 import b64encode, b64decode

str = "lol";
salt = b"ABCDEFGHIJKLMNOP";
key = "ABCDEFGHIJKLMNOPABCDEFGHIJKLMNOP";
enc_dec_method = 'utf-8'


def encrypt(str_to_enc, str_key, salt):
    aes_obj = AES.new(str_key.encode('utf-8'), AES.MODE_CFB, salt)
    hx_enc = aes_obj.encrypt(str_to_enc.encode('utf8'))
    df = b64encode(hx_enc)
    mret = b64encode(hx_enc).decode(enc_dec_method)
    return mret

def decrypt(str_to_dec, str_key, salt):
    aes_obj = AES.new(str_key.encode('utf-8'), AES.MODE_CFB, salt)
    str_tmp = b64decode(str_to_dec.encode(enc_dec_method))
    str_dec = aes_obj.decrypt(str_tmp)
    mret = str_dec.decode(enc_dec_method)
    return mret

test_enc_text = encrypt(str, key, salt)
test_dec_text = decrypt(test_enc_text, key, salt)

print(f"Encrypted Text: {test_enc_text}")
print(f"Decrypted Text: {test_dec_text}")

# OUTPUT
# Encrypted Text: o9XB
# Decrypted Text: lol
  • I tired encrypting and decrypting in both the languages and check the the documentation and source code on both the libraries.
  • Did also trying checking if I am missing encoding or decoding, but had no luck.

What key concept am I missing here which can help bridge the gap of compatibility here?


Solution

  • The codes are incompatible because:

    • Key and IV are not hex encoded, so the hex encoder must not be used in the CryptoJS code. If Utf-8 encoding is to be applied as in the Python code, the Utf-8 encoder must be used instead.

      salt = CryptoJS.enc.Utf8.parse(salt);
      key = CryptoJS.enc.Utf8.parse(key);
      
    • CFB is a stream cipher mode that does not require padding. However, in the current CryptoJS code, Zero padding is used (CryptoJS.pad.ZeroPadding). Instead, the padding has to be disabled (which does not happen implicitly, unlike in the Python code):

      padding: CryptoJS.pad.NoPadding,
      
    • CFB is configured with an additional parameter, the segment size, which specifies the number of bits encrypted per encryption step. The CryptoJS code supports only one segment size, 128 bits. With PyCryptodome the segment size can be configured, by default 8 bits are used. For compatibility it must be changed to 128 bits:

      aes_obj = AES.new(str_key.encode('utf-8'), AES.MODE_CFB, salt, segment_size=128)
      

    With these changes both codes are compatible.

    Test: Both codes provide the following ciphertext for the following plaintext:

    plaintext:  The quick brown fox jumps over the lazy dog
    ciphertext: m0T/7e04eV49RTcgd7KtwHxSOavNzHNwlrvjt1YmmHidBy4rHS0oovclKQ==
    

    Note that what you call salt is actually the initialization vector (IV). The term salt is more commonly used in the context of key derivation functions (like PBKDF2).

    Regarding security: The values used for salt and IV are OK for testing, but in a real world scenario a random byte sequence has to be applied for the IV for each encryption, which has to be passed along with the ciphertext to the decrypting side (generally concatenated).
    Also, as key no passphrase has to be used, but a random byte sequence. A passphrase (a strong one of course) may only be applied in combination with a key derivation function.