How to decrypt password from JavaScript CryptoJS.AES.encrypt(password, passphrase) in Python

12,419

You will have to implement OpenSSL's EVP_BytesToKey, because that is what CryptoJS uses to derive the key and IV from the provided password, but pyCrypto only supports the key+IV type encryption. CryptoJS also generates a random salt which also must be send to the server. If the ciphertext object is converted to a string, then it uses automatically an OpenSSL-compatible format which includes the random salt.

var data = "Some semi-long text for testing";
var password = "some password";
var ctObj = CryptoJS.AES.encrypt(data, password);
var ctStr = ctObj.toString();

out.innerHTML = ctStr;
<script src="https://cdn.rawgit.com/CryptoStore/crypto-js/3.1.2/build/rollups/aes.js"></script>
<div id="out"></div>

Possible output:

U2FsdGVkX1+ATH716DgsfPGjzmvhr+7+pzYfUzR+25u0D7Z5Lw04IJ+LmvPXJMpz

CryptoJS defaults to 256 bit key size for AES, PKCS#7 padding and CBC mode. AES has a 128 bit block size which is also the IV size. This means that we have to request 32+16 = 48 byte from EVP_BytesToKey. I've found a semi-functional implementation here and extended it further.

Here is the full Python (tested with 2.7 and 3.4) code, which is compatible with CryptoJS:

from Cryptodome import Random
from Cryptodome.Cipher import AES
import base64
from hashlib import md5

BLOCK_SIZE = 16

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

def unpad(data):
    return data[:-(data[-1] if type(data[-1]) == int else ord(data[-1]))]

def bytes_to_key(data, salt, output=48):
    # extended from https://gist.github.com/gsakkis/4546068
    assert len(salt) == 8, len(salt)
    data += salt
    key = md5(data).digest()
    final_key = key
    while len(final_key) < output:
        key = md5(key + data).digest()
        final_key += key
    return final_key[:output]

def encrypt(message, passphrase):
    salt = Random.new().read(8)
    key_iv = bytes_to_key(passphrase, salt, 32+16)
    key = key_iv[:32]
    iv = key_iv[32:]
    aes = AES.new(key, AES.MODE_CBC, iv)
    return base64.b64encode(b"Salted__" + salt + aes.encrypt(pad(message)))

def decrypt(encrypted, passphrase):
    encrypted = base64.b64decode(encrypted)
    assert encrypted[0:8] == b"Salted__"
    salt = encrypted[8:16]
    key_iv = bytes_to_key(passphrase, salt, 32+16)
    key = key_iv[:32]
    iv = key_iv[32:]
    aes = AES.new(key, AES.MODE_CBC, iv)
    return unpad(aes.decrypt(encrypted[16:]))


password = "some password".encode()
ct_b64 = "U2FsdGVkX1+ATH716DgsfPGjzmvhr+7+pzYfUzR+25u0D7Z5Lw04IJ+LmvPXJMpz"

pt = decrypt(ct_b64, password)
print("pt", pt)

print("pt", decrypt(encrypt(pt, password), password))

Similar code can be found in my answers for Java and PHP.

JavaScript AES encryption in the browser without HTTPS is simple obfuscation and does not provide any real security, because the key must be transmitted alongside the ciphertext.

[UPDATE]:

You should use pycryptodome instead of pycrypto because pycrypto(latest pypi version is 2.6.1) no longer maintained and it has vulnerabilities CVE-2013-7459 and CVE-2018-6594 (CVE warning reported by github). I choose pycryptodomex package here(Cryptodome replace Crypto in code) instead of pycryptodome package to avoid conflict name with Crypto from pycrypto package.

Share:
12,419
Bing
Author by

Bing

A Network engineer who codes

Updated on June 16, 2022

Comments

  • Bing
    Bing almost 2 years

    I have a password which is encrypt from JavaScript via

      var password = 'sample'
      var passphrase ='sample_passphrase'
      CryptoJS.AES.encrypt(password, passphrase)
    

    Then I tried to decrypt the password comes from JavaScript in Python:

      from Crypto.Cipher import AES
      import base64
    
      PADDING = '\0'
    
      pad_it = lambda s: s+(16 - len(s)%16)*PADDING
      key = 'sample_passphrase'
      iv='11.0.0.101'        #------> here is my question, how can I get this iv to restore password, what should I put here?
      key=pad_it(key)        #------> should I add padding to keys and iv?
      iv=pad_it(iv)          ##
      source = 'sample'
      generator = AES.new(key, AES.MODE_CFB,iv)
      crypt = generator.encrypt(pad_it(source))
      cryptedStr = base64.b64encode(crypt)
      print cryptedStr
      generator = AES.new(key, AES.MODE_CBC,iv)
      recovery = generator.decrypt(crypt)
      print recovery.rstrip(PADDING)
    

    I checked JS from browser console, it shows IV in CryptoJS.AES.encrypt(password, passphrase) is a object with some attributes( like sigBytes:16, words: [-44073646, -1300128421, 1939444916, 881316061]). It seems generated randomly.

    From one web page, it tells me that JS has two way to encrypt password (reference link ):

    • a. crypto.createCipher(algorithm, password)
    • b. crypto.createCipheriv(algorithm, key, iv)

    What I saw in JavaScript should be option a. However, only option b is equivalent to AES.new() in python.

    The questions are:

    1. How can I restore this password in Python without changing JavaScript code?

    2. If I need IV in Python, how can I get it from the password that is used in JavaScript?

  • Bing
    Bing about 8 years
    Have tried this one, it works perfectly. Thanks a lot. I don't know much on how encrypt and decrypt work with these options, if you could mark some reference links I can get these info, that will be very appreciated.
  • Artjom B.
    Artjom B. about 8 years
    I'm not sure I understand which references you're looking for. If you're talking about how I know all this, then the easiest way would be to simply take the unminified source code and read it.
  • Bing
    Bing about 8 years
    Yes, I mean how to get these info. I will check some code. thanks.
  • dark knight
    dark knight about 7 years
    Awesome code. Thank you. Spend a lot of time in cryptojs code trying to figure this out
  • ikel
    ikel almost 7 years
    great work, it is exactly what i have been searching for in past few hours, thanks for the help
  • emre can
    emre can over 6 years
    Thank you Artjom, It really works for most of the cases, however sometimes, I get an AssertionError for assert encrypted[0:8] == b"Salted__" line that runs on decrypt function. Currently, I'm looking for the reason of that by repating the erroneous case.
  • Artjom B.
    Artjom B. over 6 years
    @EmreCanKucukoglu Are you sending the value through HTTP? If so, are you encoding it correctly? This might be an issue, because Base64 is not URL-safe in its default form. If this is not it, you need to ask a proper question with some examples.
  • emre can
    emre can over 6 years
    Yes, I am sending the values through HTTP. I use it like that: body = json.loads(request.body.decode('utf-8')) encrypted_data = body['encrypted_data'] pw = body['pw'].encode() result = crypto.decrypt(encrypted_data, pw).decode() Then, I understand that I have to encode also encrypted_data, right?
  • emre can
    emre can over 6 years
    It occurs actually because of some other problems, I suppose. I dig into logs and realize that request body has some erroneous parts for my case. Sorry for that and thanks again Artjom.
  • Artjom B.
    Artjom B. almost 4 years
    @Fruit Thank you for the edit. Do you know what the incompatibilities between pycrypto and pycryptodome are when switching to pycryptodome?
  • 林果皞
    林果皞 almost 4 years
    @ArtjomB. It has been documented at pycryptodome.org/en/latest/src/vs_pycrypto.html
  • FightWithCode
    FightWithCode over 3 years
    You life saviour
  • Douglas Figueroa
    Douglas Figueroa about 3 years
    After a lot of tries with other solutions, this works perfectly fine. Thank so much!
  • Muhammad Haseeb
    Muhammad Haseeb over 2 years
    Don't forget to decode your string.return unpad(aes.decrypt(encrypted[16:])).decode()
  • Shubham
    Shubham almost 2 years
    @ArtjomB. I am using this script in my app to encrypt some content. (embed.plnkr.co/0VPU1zmmWC5wmTKPKnhg). Password => PBKDF2 key derivation => AES encryption I tried to encrypt/decrypt the content through python script you suggested above but it fails. Can you help here please?