Encrypt with PHP, Decrypt with Javascript (cryptojs)

61,462

Solution 1

I've required the same thing and i wrote a short library that works for CryptoJS 3.x and PHP with openssl support. Hope this helps, source plus example files here https://github.com/brainfoolong/cryptojs-aes-php

PHP Lib

/**
* Decrypt data from a CryptoJS json encoding string
*
* @param mixed $passphrase
* @param mixed $jsonString
* @return mixed
*/
function cryptoJsAesDecrypt($passphrase, $jsonString){
    $jsondata = json_decode($jsonString, true);
    $salt = hex2bin($jsondata["s"]);
    $ct = base64_decode($jsondata["ct"]);
    $iv  = hex2bin($jsondata["iv"]);
    $concatedPassphrase = $passphrase.$salt;
    $md5 = array();
    $md5[0] = md5($concatedPassphrase, true);
    $result = $md5[0];
    for ($i = 1; $i < 3; $i++) {
        $md5[$i] = md5($md5[$i - 1].$concatedPassphrase, true);
        $result .= $md5[$i];
    }
    $key = substr($result, 0, 32);
    $data = openssl_decrypt($ct, 'aes-256-cbc', $key, true, $iv);
    return json_decode($data, true);
}

/**
* Encrypt value to a cryptojs compatiable json encoding string
*
* @param mixed $passphrase
* @param mixed $value
* @return string
*/
function cryptoJsAesEncrypt($passphrase, $value){
    $salt = openssl_random_pseudo_bytes(8);
    $salted = '';
    $dx = '';
    while (strlen($salted) < 48) {
        $dx = md5($dx.$passphrase.$salt, true);
        $salted .= $dx;
    }
    $key = substr($salted, 0, 32);
    $iv  = substr($salted, 32,16);
    $encrypted_data = openssl_encrypt(json_encode($value), 'aes-256-cbc', $key, true, $iv);
    $data = array("ct" => base64_encode($encrypted_data), "iv" => bin2hex($iv), "s" => bin2hex($salt));
    return json_encode($data);
}

Javascript Lib

var CryptoJSAesJson = {
    stringify: function (cipherParams) {
        var j = {ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64)};
        if (cipherParams.iv) j.iv = cipherParams.iv.toString();
        if (cipherParams.salt) j.s = cipherParams.salt.toString();
        return JSON.stringify(j);
    },
    parse: function (jsonStr) {
        var j = JSON.parse(jsonStr);
        var cipherParams = CryptoJS.lib.CipherParams.create({ciphertext: CryptoJS.enc.Base64.parse(j.ct)});
        if (j.iv) cipherParams.iv = CryptoJS.enc.Hex.parse(j.iv)
        if (j.s) cipherParams.salt = CryptoJS.enc.Hex.parse(j.s)
        return cipherParams;
    }
}

Example Javascript

var encrypted = CryptoJS.AES.encrypt(JSON.stringify("value to encrypt"), "my passphrase", {format: CryptoJSAesJson}).toString();
var decrypted = JSON.parse(CryptoJS.AES.decrypt(encrypted, "my passphrase", {format: CryptoJSAesJson}).toString(CryptoJS.enc.Utf8));

Example PHP

$encrypted = cryptoJsAesEncrypt("my passphrase", "value to encrypt");
$decrypted = cryptoJsAesDecrypt("my passphrase", $encrypted);

Solution 2

Security notice: The code on this answer is vulnerable to chosen-ciphertext attacks. See this answer instead for secure encryption.

Here is a working example of encrypting your string with PHP and decrypting it with CryptoJS.

On the PHP side:

Use MCRYPT_RIJNDAEL_128 (not 256) to pair with AES. The 128 here is the blocksize, not the keysize.

Send the IV, too. You need the IV to decrypt.

$text = "this is the text here";
$key = "encryptionkey";

// Note: MCRYPT_RIJNDAEL_128 is compatible with AES (all key sizes)
$iv = random_bytes(16);

$ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $text, MCRYPT_MODE_CBC, $iv);

echo "iv:".base64_encode($iv)."\n";
echo "ciphertext:".base64_encode($ciphertext)."\n";

Here is sample output from a test run:

iv:BMcOODpuQurUYGICmOqqbQ==
ciphertext:ZJAab8YtkRq5TL7uyIR7frM2b3krftJzn1pTqRTAda4=

IMPORTANT: Because we are not authenticating our ciphertext, decryption becomes vulnerable to padding oracle attacks. See also: authenticated encryption in PHP.

On the CryptoJS side:

Your key is only 13 ASCII printable characters which is very weak. Mcrypt padded the key to a valid keysize using ZERO bytes.

Convert the key and IV to word arrays.

I did not have much luck decrypting with ciphertext as a word array, so I've left it in Base64 format.

CryptoJS = require("crypto-js")

// Mcrypt pads a short key with zero bytes
key = CryptoJS.enc.Utf8.parse('encryptionkey\u0000\u0000\u0000')

iv = CryptoJS.enc.Base64.parse('BMcOODpuQurUYGICmOqqbQ==')

// Keep the ciphertext in Base64 form
ciphertext = 'ZJAab8YtkRq5TL7uyIR7frM2b3krftJzn1pTqRTAda4='

/**
 * DANGER DANGER WILL ROBINSON! <== Stop editing my answer or I will delete it.
 *
 * This example code doesn't demonstrate AUTHENTICATED ENCRYPTION
 * and is therefore vulnerable to chosen-ciphertext attacks.
 *
 * NEVER USE THIS CODE TO PROTECT SENSITIVE DATA!
 */

// Mcrypt uses ZERO padding
plaintext = CryptoJS.AES.decrypt(ciphertext, key, { iv: iv, padding: CryptoJS.pad.ZeroPadding })

// I ran this in nodejs
process.stdout.write(CryptoJS.enc.Utf8.stringify(plaintext))

Solution 3

Don't go too dept with coding, just use base64 decoder

on php code:

$encrypt_val=base64_encode("value");

and on js just:

var my_orignal_val = window.atob(passed_val);

This will enough for your requirement.

Solution 4

You are using two libraries that try to accommodate input that is - strictly speaking - not valid. Rijndael requires keys that 16, 24 or 32 bytes long random byte strings. You provide a 13 character string. Mcrypt, the PHP library, uses the string (presumable utf8 encoded) directly as binary input and zero pads it to the required 32 bytes for MCRYPT_RIJNDAEL_256. CryptoJS on the other hand decides that you have entered something like a passphrase and instead uses a key derivation function to generate a 32 byte key.

Furthermore the encryption algorithms used don't even match. Mcrypt uses a seldom implemented variant of the original Rijndael for the 256 bit version, while CryptoJS implements the widely known variant AES256 of the Rijndael proposal. The 128 bit version of both (MCRYPT_RIJNDAEL_128 and AES128) are identical though.

The third problem you are about to face later is that Mcrypt also uses a crazy padding scheme for the data being encrypted. As Rijndael is a block cipher, it can only encrypt blocks of 16, 24 or 32 bytes (depending on the variant - AES always uses 16 byte blocks). As such data has to be padded. Mcrypt does this in a non-injective way by just appending zeros. If you are only encoding strings this will not be so much of a problem for you as utf8 encoded strings never contain zero bytes, so you can just strip them off (CryptoJS even supports that natively).

The easiest fix to all these problems is to avoid having to implement any cryptography yourself (it is strongly discouraged anyway without a wide knowledge of the subject). Can you instead transmit your sensitive information over https which will use TLS (formerly called SSL) to encrypt and authenticate the channel?

Share:
61,462
user2769
Author by

user2769

Updated on June 29, 2020

Comments

  • user2769
    user2769 almost 4 years

    I'm having trouble with basic encryption/decryption. I've looked all around for a working example but haven't quite found a working example.

    -I will be encrypting in php, decrypting with cryptojs for a small layer of security

    <script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js">
    <?
    $text = "this is the text here";
    $key = "encryptionkey";
    
    $msgEncrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $text, MCRYPT_MODE_CBC, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND));
    $msgBase64 = trim(base64_encode($msgEncrypted));
    
    echo "<h2>PHP</h2>";
    echo "<p>Encrypted:</p>";
    echo $msgEncrypted;
    echo "<p>Base64:</p>";
    echo $msgBase64;
     ?>
    
    <p>AES Decrypt</p>
    <script> 
        var key = 'encryptionkey';
        var encrypted = "<?php echo $msgBase64 ?>";
        //tried  var base64decode = CryptoJS.enc.Base64.parse(encrypted); 
        var decrypted = CryptoJS.AES.decrypt(encrypted, key);
        console.log( decrypted.toString(CryptoJS.enc.Utf8) );
    </script>
    

    Which step am I missing?

  • user2769
    user2769 almost 10 years
    Thanks! I wasn't able to get your example working unfortunately. When I removed the "padding: CryptoJS.pad.ZeroPadding", It would sortof work -- but adds "���" to the end of string. Also, I was wondering how to change the encrpytion key in your example. What does "\u0000\u0000\u0000" mean and how do I determine how much I need?
  • Jim Flood
    Jim Flood almost 10 years
    "this is the text here" encoded in UTF-16 would be 42 bytes, which would be padded to 48 for encryption. Those extra 6 bytes would appear as three UTF-16 chars at the end, which would explain three funny chars. Try CryptoJS.pad.NoPadding, and look at the binary value of those last bytes. Are they all 0x00, or all 0x06?
  • Jim Flood
    Jim Flood almost 10 years
    Each \u0000 is adding a zero byte 0x00, or two zero bytes if the string is UTF-16. Mcrypt must be padding the key to 16, 24, or 32 bytes. If this is ASCII or UTF-8, it would be 16 bytes. If UTF-16, then 32 bytes. You should be using a stronger key. Pick a key size of 16, 24, or 32 bytes and make it from cryptographically strong random data.
  • JSON
    JSON over 9 years
    Looks pretty good. But why did you choose to not use IV/Salt?
  • JSON
    JSON over 9 years
    BTW, just want to add, it's amazing that you just posted this... exactly what I was looking for because Mcrypt is GPL, where OpenSSL is Apache/BSD
  • Brain Foo Long
    Brain Foo Long over 9 years
    @JSON I wanted to keep the example as short as possible - It should show how the basic flow works. I'm also not a crypto expert and i'm not sure what a custom IV and custom salt should help. Salt and IV exist and is always different because salt is randomly generated and IV results from the passphrase and random salt. But when you can improve that code please do it on my GIT repos.
  • Scott Arciszewski
    Scott Arciszewski about 9 years
    MCRYPT_RAND is not a CSPRNG. You want a CSPRNG for IVs in CBC mode. Also, tonyarcieri.com/…
  • php_coder_3809625
    php_coder_3809625 about 9 years
    @BrainFooLong Is there any licenses on the code above? Just wanna make sure in case I wanna distribute the code with my other stuff.
  • Brain Foo Long
    Brain Foo Long about 9 years
    @php_coder_3809625 GPL v3 - I'll add it to the GitHub repos readme.
  • php_coder_3809625
    php_coder_3809625 about 9 years
    @BrainFooLong Cheers mate.
  • Marki555
    Marki555 almost 9 years
    Didn't you mean MCRYPT_RIJNDAEL_ 128 in the 2nd paragraph?
  • ReNiSh AR
    ReNiSh AR over 8 years
    can any way to get static encrypted string instead of random encrypted string by using this method..?
  • User
    User almost 8 years
    @BrainFooLong After trying to get my own code to work all day, finding that's you'd spent time writing this really helped. And then it actually worked and now i'm speechless XD
  • Braiam
    Braiam almost 6 years
    The updates to your answer are visible from the revisions link.
  • eyedmax
    eyedmax about 5 years
    actually, var my_orignal_val = window.btoa(passed_val);
  • Gaurang Sondagar
    Gaurang Sondagar almost 5 years
    Hii, Above php code is not working for encrypt and decrypt from received data through cryptojs
  • Jim Flood
    Jim Flood almost 5 years
    Stop editing my answer for frivolous reasons or I will delete it. Sign your graffiti.