6

I'm encrypting my user password in JavaScript like this:

 var encryptedPassword = CryptoJS.AES.encrypt(password, "Secret Passphrase");

It works fine but now I'm trying to decrypt in PHP on the server side like this:

 $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_RAND);
 $decryptPassword = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, "Secret Passphrase", base64_decode($password), MCRYPT_MODE_CBC, $iv);

it doesn't works at all, the decrypted password string looks very strange:

 string(64) ">�OX2MS��댗v�<$�ʕ��i�̄��_��P���\�կ=�_6(�m����,4WT7��a"

Here is the current state of my code in JavaScript after the helpful comments:

    var encryptedPassword = CryptoJS.AES.encrypt(password, "Secret Passphrase");
    var ivHex = encryptedPassword.iv.toString();
    var ivSize = encryptedPassword.algorithm.ivSize; // same as blockSize
    var keySize = encryptedPassword.algorithm.keySize;
    var keyHex = encryptedPassword.key.toString();
    var saltHex = encryptedPassword.salt.toString(); // must be sent
    var openSslFormattedCipherTextString = encryptedPassword.toString(); // not used
    var cipherTextHex = encryptedPassword.ciphertext.toString(); // must be sent

I am sending saltHex and CipherTextHex to the PHP server and I'm using mcrypt_decrypt() like this:

 $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), $saltHex);
 $decryptPassword = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, "Secret Passphrase", base64_decode($cipherTextHex), MCRYPT_MODE_CBC, $iv);

It still does't work with this updated code.

Can someone help me to decrypt properly with mcrypt_decrypt() PHP function for a simple AES encryption method ? I'm sure I am doing something wrong with the cipher, mcrypt mode and the IV parameters inside my mcrypt_decrypt() method. Thanks if you know.

Nizar B.
  • 3,098
  • 9
  • 38
  • 56
  • @ArtjomB. Can you please, give more precision ? – Nizar B. Dec 28 '14 at 14:21
  • What's advantage of encrypting in `JavaScript` and decrypt in `PHP`, Because you can do encrypt and decrypt in `PHP` –  Dec 28 '14 at 14:21
  • @lawrenceoverflow The encryption in the client side with JavaScript is used to not POST the login form in clear plain text to my PHP server. – Nizar B. Dec 28 '14 at 14:22
  • @lawrenceoverflow `javascript` is not `java` – Francis.TM Dec 28 '14 at 14:23
  • @Francis.TM True story ... – Nizar B. Dec 28 '14 at 14:23
  • 1
    See here: http://stackoverflow.com/a/27250883/1816580 and convert the code to php if you want – Artjom B. Dec 28 '14 at 14:24
  • @ArtjomB. Would be nice if i only have the right CIPHER, MCRYPT_MODE used and the IV in PHP to decrypt this JavaScript line: var encryptedPassword = CryptoJS.AES.encrypt(password, "Secret Passphrase"); – Nizar B. Dec 28 '14 at 14:28
  • 1
    If you don't have the IV and salt then you're out of luck. You didn't show how `encryptedPassword` is transmitted, because it may be encoded ito it. CryptoJS uses an OpenSSLFormatter. You should look into the code of CryptoJS. – Artjom B. Dec 28 '14 at 14:32
  • @ArtjomB. Witch means cipherTextHex in the JavaScript is the CIPHER for the mcrypt_decrypt() PHP function, but what's saltText variable ? – Nizar B. Dec 28 '14 at 14:35
  • no, $cipher is still `MCRYPT_RIJNDAEL_128`. – Artjom B. Dec 28 '14 at 14:40
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/67793/discussion-between-katcha-and-artjom-b). – Nizar B. Dec 28 '14 at 14:45
  • I have done something that you may use: http://stackoverflow.com/questions/24337317/encrypt-with-php-decrypt-with-javascript-cryptojs/27582253#27582253 – Brain Foo Long Dec 28 '14 at 18:03

3 Answers3

16

The problem is that in the CryptoJS code a password is used to derive the key and the IV to be used for AES encryption, but mcrypt only uses the key to encrypt/decrypt. This information needs to be passed to php. Since you don't want to transmit the password, you have to derive the key and IV in the same way in php.

The following code derives the key and IV from a password and salt. It is modeled after the code in my answer here (for more information).

function evpKDF($password, $salt, $keySize = 8, $ivSize = 4, $iterations = 1, $hashAlgorithm = "md5") {
    $targetKeySize = $keySize + $ivSize;
    $derivedBytes = "";
    $numberOfDerivedWords = 0;
    $block = NULL;
    $hasher = hash_init($hashAlgorithm);
    while ($numberOfDerivedWords < $targetKeySize) {
        if ($block != NULL) {
            hash_update($hasher, $block);
        }
        hash_update($hasher, $password);
        hash_update($hasher, $salt);
        $block = hash_final($hasher, TRUE);
        $hasher = hash_init($hashAlgorithm);

        // Iterations
        for ($i = 1; $i < $iterations; $i++) {
            hash_update($hasher, $block);
            $block = hash_final($hasher, TRUE);
            $hasher = hash_init($hashAlgorithm);
        }

        $derivedBytes .= substr($block, 0, min(strlen($block), ($targetKeySize - $numberOfDerivedWords) * 4));

        $numberOfDerivedWords += strlen($block)/4;
    }

    return array(
        "key" => substr($derivedBytes, 0, $keySize * 4),
        "iv"  => substr($derivedBytes, $keySize * 4, $ivSize * 4)
    );
}

The salt is generated during encryption in CryptoJS and needs to be sent to php with the ciphertext. Before invoking evpKDF the salt has to be converted to a binary string from hex.

$keyAndIV = evpKDF("Secret Passphrase", hex2bin($saltHex));
$decryptPassword = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, 
        $keyAndIV["key"], 
        hex2bin($cipherTextHex), 
        MCRYPT_MODE_CBC, 
        $keyAndIV["iv"]);

If only encryptedPassword.toString() was sent to the server, then it is necessary to split the salt and actual ciphertext before use. The format is a proprietary OpenSSL-compatible format with the first 8 bytes being "Salted__", the next 8 bytes being the random salt and the rest is the actual ciphertext. Everything together is Base64-encoded.

function decrypt($ciphertext, $password) {
    $ciphertext = base64_decode($ciphertext);
    if (substr($ciphertext, 0, 8) != "Salted__") {
        return false;
    }
    $salt = substr($ciphertext, 8, 8);
    $keyAndIV = evpKDF($password, $salt);
    $decryptPassword = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, 
            $keyAndIV["key"], 
            substr($ciphertext, 16), 
            MCRYPT_MODE_CBC, 
            $keyAndIV["iv"]);

    // unpad (PKCS#7)
    return substr($decryptPassword, 0, strlen($decryptPassword) - ord($decryptPassword[strlen($decryptPassword)-1]));
}

The same can be achieved with the OpenSSL extension instead of Mcrypt:

function decrypt($ciphertext, $password) {
    $ciphertext = base64_decode($ciphertext);
    if (substr($ciphertext, 0, 8) != "Salted__") {
        return false;
    }
    $salt = substr($ciphertext, 8, 8);
    $keyAndIV = evpKDF($password, $salt);
    $decryptPassword = openssl_decrypt(
            substr($ciphertext, 16), 
            "aes-256-cbc",
            $keyAndIV["key"], 
            OPENSSL_RAW_DATA, // base64 was already decoded
            $keyAndIV["iv"]);

    return $decryptPassword;
}
Community
  • 1
  • 1
Artjom B.
  • 61,146
  • 24
  • 125
  • 222
  • 1
    Similar code can be found in my answers for [Java](https://stackoverflow.com/a/27250883/1816580) and [Python](https://stackoverflow.com/a/36780727/1816580). – Artjom B. Aug 11 '17 at 17:12
  • **Security notice:** This code is provided for achieve compatibility between different programming languages. It is not necessarily fully secure. Its security depends on the complexity and length of the password, because of only one iteration and the use of MD5. I would recommend to use at least a 20 character password with alphanumeric characters which is ideally randomly generated. – Artjom B. Jan 29 '18 at 18:48
  • Thank you SO MUCH @Artjom B.! I wish I could give you +100. This was so helpful. – Ryan Aug 10 '18 at 22:32
  • I`m also want to say thank you @ArtjomB. 2 days of searching.... – V-E-Y Feb 18 '23 at 16:47
  • @ArtjomB. Can you help pleas? How encrypt in php if in CryptoJs side they decrypt like this - let bytes = AES.decrypt(user.password.toString(), secretKey); compareResult = password === JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); secretKey I have. – V-E-Y Feb 18 '23 at 19:38
5
/** 
 *-------------PHP code example-----------------
 */
/**
 * 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);
}
$encrypted = '{"ct":"nPfd1U0y9o2hRCdwJK6XkM1E01wa1ZjMu3eAzGjUD60=","iv":"2abda27fc571cf74e6efc1ba564801f9","s":"813a340e805f54ae"}';
$key = "123456";
$decrypted = cryptoJsAesDecrypt($key, $encrypted);

/* -------------Javascript code example-----------------*/
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;
    }
}
var key = "123456";
var encrypted = CryptoJS.AES.encrypt(JSON.stringify("value to encrypt"), key, {format: CryptoJSAesJson}).toString();
console.log(encrypted);
var decrypted = JSON.parse(CryptoJS.AES.decrypt(encrypted, key, {format: CryptoJSAesJson}).toString(CryptoJS.enc.Utf8));
console.log("decryyepted: "+decrypted);
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
0

You can't decrypt with a random initialisation vector - you need to use the same IV the data was encrypted with. Also, IIRC, AES defaults to an 8 bit representation of the encrypted data which will need to be handled carefully in transfer over HTTP.

symcbean
  • 47,736
  • 6
  • 59
  • 94
  • One thing, I got a problem with md5. When I pass the clear password to check the login of the user, the md5 hash is equal to the md5 hash from the MySQL database. But when I'm using the decrypted password, the md5 is different. This is weird because the clear password string is the same than the decrypted string, and the md5 hash is different. – Nizar B. Dec 28 '14 at 21:22