skip to Main Content

I am trying to encrypt and decrypt a string in PHP as well as in JS.

PHP

function encrypt_decrypt($action, $string)
{
    $output = false;
    $encrypt_method = "AES-256-CBC";
    $secret_key = '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,';
    $secret_iv = '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,';

    $key = hash('sha256', $secret_key);

    //  echo $key.'  '; // bc11b452cdf3f8155b5fd3e3711b1d0712638c613f33f5551b96d5bb37b490ae

    // echo strlen($key); // 64

    // iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning
    $iv = substr(hash('sha256', $secret_iv), 0, 16);

    //   echo $iv.' '; // bc11b452cdf3f815

    if ($action == 'encrypt') {
        $output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
        $output = base64_encode($output); // output is base 64 encoded
    } else if ($action == 'decrypt') {
        $output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
    }
    return $output;
}

$encrypted = encrypt_decrypt('encrypt', 'alaks@123');

// echo "encrypted string is " . $encrypted; // Rko0Q0lsdnl3Ukp6RkI2bXA4STF6QT09

// echo "decrypted string is " . encrypt_decrypt('decrypt', $encrypted); // alaks@123

I am trying to apply the same secret_key and secret_iv and get the key and iv matching

JS

<script src="node_modules/crypto-js/crypto-js.js"></script>
<script>
  
    const secret_key = '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,';
    const secret_iv = '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,';

    const text = 'alaks@123';

    const key = CryptoJS.SHA256(secret_key).toString();
    const iv = CryptoJS.SHA256(secret_iv).toString().substring(0, 16);

    // console.log("key", key); // bc11b452cdf3f8155b5fd3e3711b1d0712638c613f33f5551b96d5bb37b490ae
   
    // console.log("iv", iv); // bc11b452cdf3f815
    
    function aesEncrypt(data) {
        let cipher = CryptoJS.AES.encrypt(data, key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return btoa(cipher.toString()); // convert the encrypted string to base64
    }

    function aesDecrypt(data) {
        let cipher = CryptoJS.AES.decrypt(atob(data), key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return cipher.toString(CryptoJS.enc.Utf8);
    }

    const encryptedText = aesEncrypt(text);
    const decryptedText = aesDecrypt(encryptedText);
    console.log('Encrypted Text - ' + encryptedText); // this encrypted text is different from php generated
    console.log('Decrypted Text - ' + decryptedText);

    // actual encrypted text = VTJGc2RHVmtYMSsvblljaUpsdElzQ2w4eVFsRnZHS01oR2g2WS8raTJLbz0=
    // expected encrypted text = Rko0Q0lsdnl3Ukp6RkI2bXA4STF6QT09

   
</script>

The generated encrypted text is also not consistent as in PHP. Below are the encrypted texts on every refresh

encrypted text generated = VTJGc2RHVmtYMTlISHlLdlBGdDB5Sitlb3pnY04wQjJtWWIwV0RCR1I1Yz0=
encrypted text generated = VTJGc2RHVmtYMS9XZXNyTzhMWkZqdEtpVkF1MURJeUdzUlhqUUlUcXRlTT0=
encrypted text generated = VTJGc2RHVmtYMTh3dHY0VDBBanhzbnltejV1K1MxV2FYb1ZXalJuTjJxZz0=
encrypted text generated = VTJGc2RHVmtYMTh3Z081YVViNnc3dCtZUlAvN05NZ09nRUh0blB0eTBpaz0=

My query is, what should be the CrptoJS setting, to get the same encoded output (Rko0Q0lsdnl3Ukp6RkI2bXA4STF6QT09), as in PHP

const secret_key = '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,';
    const secret_iv = '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,';

    const text = 'alaks@123';

    const key = CryptoJS.SHA256(secret_key).toString().substring(0,32);
  
    const iv = CryptoJS.SHA256(secret_iv).toString().substring(0, 16);

    console.log("key", key); // bc11b452cdf3f8155b5fd3e3711b1d0712638c613f33f5551b96d5bb37b490ae
   
    // console.log("iv", iv); // bc11b452cdf3f815
    
    function aesEncrypt(data) {
        let cipher = CryptoJS.AES.encrypt(data, key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return btoa(cipher.toString()); // convert the encrypted string to base64
    }

    function aesDecrypt(data) {
        let cipher = CryptoJS.AES.decrypt(atob(data), key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return cipher.toString(CryptoJS.enc.Utf8);
    }

//**Edit 1: Added Code Snippet**
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

Combined PHP and JS code

    <?php
$text = 'alaks@123';

define('SECRET_KEY', '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,');
define('SECRET_IV', '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,');

function encrypt_decrypt($action, $string)
{
    $collectionInPhp = [];
    $encrypt_method = "AES-256-CBC";
    $output = false;
    $key = hash('sha256', SECRET_KEY);
    $iv = substr(hash('sha256', SECRET_IV), 0, 16);

    $collectionInPhp['secret_key'] = SECRET_KEY;
    $collectionInPhp['secret_iv'] = SECRET_IV;
    $collectionInPhp['key'] = $key;
    $collectionInPhp['iv'] = $iv;

    if ($action == 'encrypt') {
        $output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
        $collectionInPhp['before_base64_encode'] = $output;
         $output = base64_encode($output);
         $collectionInPhp['after_base64_encode'] = $output;

    } else if ($action == 'decrypt') {
        $output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
    }
    return [$output, $collectionInPhp];
}

$result = encrypt_decrypt('encrypt', $text);

$encrypted = $result[0];
$collectionInPhp = $result[1];
?>

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pure/3.0.0/pure-min.css"
        integrity="sha512-X2yGIVwg8zeG8N4xfsidr9MqIfIE8Yz1It+w2rhUJMqxUwvbVqC5OPcyRlPKYOw/bsdJut91//NO9rSbQZIPRQ=="
        crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>

<body>
    <table class="pure-table pure-table-bordered">
        <thead>
            <tr>
                <th>#</th>
                <th>PHP</th>
                <th>JS</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>Secret Key</td>
                <td class="php secret-key"></td>
                <td class="js secret-key"></td>
            </tr>
            <tr>
                <td>Secret IV</td>
                <td class="php secret-iv"></td>
                <td class="js secret-iv"></td>
            </tr>
            <tr>
                <td>Key</td>
                <td class="php key"></td>
                <td class="js key"></td>
            </tr>
            <tr>
                <td>IV</td>
                <td class="php iv"></td>
                <td class="js iv"></td>
            </tr>
            <tr>
                <td>Before Base64Encode</td>
                <td class="php beforeBase64Encode"></td>
                <td class="js"></td>
            </tr>
            <tr>
                <td>Encrypted</td>
                <td class="php encrypted"></td>
                <td class="js"></td>
            </tr>
            <tr>
                <td>Received in JS</td>
                <td class="php"></td>
                <td class="js encrypted"></td>
            </tr>
            <tr>
                <td>After Base64Decode</td>
                <td class="php"></td>
                <td class="js afterBase64Decode"></td>
            </tr>

            <tr>
                <td>Decrypted</td>
                <td class="php"></td>
                <td class="js decrypted"></td>
            </tr>
        </tbody>
    </table>
    <script src="node_modules/crypto-js/crypto-js.js"></script>
    <script>
    const collectionInPhp = <?php echo json_encode($collectionInPhp); ?>;
    const encrypted = "<?php echo $encrypted; ?>";
    const base64Decoded = atob("<?php echo $encrypted; ?>");

    // const secret_key = CryptoJS.enc.Utf8.parse("<?php // echo SECRET_KEY; ?>");
    // const secret_iv = CryptoJS.enc.Utf8.parse("<?php // echo SECRET_IV; ?>");
    // const key = CryptoJS.SHA256(secret_key).toString().substring(0,32); 
    // const iv = CryptoJS.SHA256(secret_iv).toString().substring(0, 16);

    const secret_key = "<?php echo SECRET_KEY; ?>";
    const secret_iv = "<?php  echo SECRET_IV; ?>";
    const key = CryptoJS.enc.Utf8.parse(CryptoJS.SHA256(secret_key)).toString().substring(0, 32);
    const iv = CryptoJS.enc.Utf8.parse(CryptoJS.SHA256(secret_iv)).toString().substring(0, 16);

    function aesDecrypt(data) {
        let cipher = CryptoJS.AES.decrypt(data, key, { // test case 1
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });

        // return cipher.toString(CryptoJS.enc.Utf8);
        return cipher.toString();
    }
    const decrypted = aesDecrypt(base64Decoded);
    const collectionInJS = {
        base64Decoded: base64Decoded,
        encrypted: encrypted,
        key: key,
        iv: iv,
        secret_key: secret_key,
        secret_iv: secret_iv,
        decrypted: decrypted
    }

    document.querySelector('.php.secret-key').innerHTML = collectionInPhp.secret_key;
    document.querySelector('.js.secret-key').innerHTML = collectionInJS.secret_key;

    document.querySelector('.php.secret-iv').innerHTML = collectionInPhp.secret_iv;
    document.querySelector('.js.secret-iv').innerHTML = collectionInJS.secret_iv;

    document.querySelector('.php.key').innerHTML = collectionInPhp.key;
    document.querySelector('.js.key').innerHTML = collectionInJS.key;

    document.querySelector('.php.iv').innerHTML = collectionInPhp.iv;
    document.querySelector('.js.iv').innerHTML = collectionInJS.iv;

    document.querySelector('.php.beforeBase64Encode').innerHTML = collectionInPhp.before_base64_encode;
    document.querySelector('.js.afterBase64Decode').innerHTML = collectionInJS.base64Decoded;


    document.querySelector('.php.encrypted').innerHTML = collectionInPhp.after_base64_encode;
    document.querySelector('.js.encrypted').innerHTML = collectionInJS.encrypted;

    document.querySelector('.js.decrypted').innerHTML = collectionInJS.decrypted;
    </script>

</body>

</html>

Value Comparison on PHP & JS

enter image description here

2

Answers


  1. Chosen as BEST ANSWER

    Based on @Topaco answer, below is the full working code.

        <?php
        $text = 'alaks@123';
    
        define('SECRET_KEY', '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,');
        define('SECRET_IV', '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,');
    
        function encrypt_decrypt($action, $string)
        {
            $collectionInPhp = [];
            $encrypt_method = "AES-256-CBC";
            $output = false;
            $key = hash('sha256', SECRET_KEY);
            $iv = substr(hash('sha256', SECRET_IV), 0, 16);
    
            $collectionInPhp['secret_key'] = SECRET_KEY;
            $collectionInPhp['secret_iv'] = SECRET_IV;
            $collectionInPhp['key'] = $key;
            $collectionInPhp['iv'] = $iv;
    
            if ($action == 'encrypt') {
                $output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
                $collectionInPhp['before_base64_encode'] = $output;
                $output = base64_encode($output);
                $collectionInPhp['after_base64_encode'] = $output;
    
            } else if ($action == 'decrypt') {
                $output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
            }
            return [$output, $collectionInPhp];
        }
    
        $result = encrypt_decrypt('encrypt', $text);
    
        $encrypted = $result[0];
        $collectionInPhp = $result[1];
        ?>
    
        <!DOCTYPE html>
        <html lang="en">
    
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Document</title>
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pure/3.0.0/pure-min.css"
                integrity="sha512-X2yGIVwg8zeG8N4xfsidr9MqIfIE8Yz1It+w2rhUJMqxUwvbVqC5OPcyRlPKYOw/bsdJut91//NO9rSbQZIPRQ=="
                crossorigin="anonymous" referrerpolicy="no-referrer" />
        </head>
    
        <body>
            <table class="pure-table pure-table-bordered">
                <thead>
                    <tr>
                        <th>#</th>
                        <th>PHP</th>
                        <th>JS</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>Secret Key</td>
                        <td class="php secret-key"></td>
                        <td class="js secret-key"></td>
                    </tr>
                    <tr>
                        <td>Secret IV</td>
                        <td class="php secret-iv"></td>
                        <td class="js secret-iv"></td>
                    </tr>
                    <tr>
                        <td>Key</td>
                        <td class="php key"></td>
                        <td class="js key"></td>
                    </tr>
                    <tr>
                        <td>IV</td>
                        <td class="php iv"></td>
                        <td class="js iv"></td>
                    </tr>
                    <tr>
                        <td>Before Base64Encode</td>
                        <td class="php beforeBase64Encode"></td>
                        <td class="js"></td>
                    </tr>
                    <tr>
                        <td>Encrypted</td>
                        <td class="php encrypted"></td>
                        <td class="js"></td>
                    </tr>
                    <tr>
                        <td>Received in JS</td>
                        <td class="php"></td>
                        <td class="js encrypted"></td>
                    </tr>
                    <tr>
                        <td>After Base64Decode</td>
                        <td class="php"></td>
                        <td class="js afterBase64Decode"></td>
                    </tr>
    
                    <tr>
                        <td>Decrypted</td>
                        <td class="php"></td>
                        <td class="js decrypted"></td>
                    </tr>
                </tbody>
            </table>
            <script src="node_modules/crypto-js/crypto-js.js"></script>
            <script>
            const collectionInPhp = <?php echo json_encode($collectionInPhp); ?>;
            const encrypted = "<?php echo $encrypted; ?>";
            const base64Decoded = atob("<?php echo $encrypted; ?>");
            const secret_key = "<?php echo SECRET_KEY; ?>";
            const secret_iv = "<?php  echo SECRET_IV; ?>";
            const key = CryptoJS.enc.Utf8.parse(CryptoJS.SHA256(secret_key).toString().substring(0, 32));
            const iv = CryptoJS.enc.Utf8.parse(CryptoJS.SHA256(secret_iv).toString().substring(0, 16));
    
            function aesDecrypt(data) {
                let cipher = CryptoJS.AES.decrypt(data, key, { // test case 1
                    iv: iv,
                    mode: CryptoJS.mode.CBC,
                    padding: CryptoJS.pad.Pkcs7
                });
    
                return cipher.toString(CryptoJS.enc.Utf8);
                // return cipher.toString();
            }
            const decrypted = aesDecrypt(base64Decoded);
            const collectionInJS = {
                base64Decoded: base64Decoded,
                encrypted: encrypted,
                key: key,
                iv: iv,
                secret_key: secret_key,
                secret_iv: secret_iv,
                decrypted: decrypted
            }
    
            document.querySelector('.php.secret-key').innerHTML = collectionInPhp.secret_key;
            document.querySelector('.js.secret-key').innerHTML = collectionInJS.secret_key;
    
            document.querySelector('.php.secret-iv').innerHTML = collectionInPhp.secret_iv;
            document.querySelector('.js.secret-iv').innerHTML = collectionInJS.secret_iv;
    
            document.querySelector('.php.key').innerHTML = collectionInPhp.key;
            document.querySelector('.js.key').innerHTML = collectionInJS.key;
    
            document.querySelector('.php.iv').innerHTML = collectionInPhp.iv;
            document.querySelector('.js.iv').innerHTML = collectionInJS.iv;
    
            document.querySelector('.php.beforeBase64Encode').innerHTML = collectionInPhp.before_base64_encode;
            document.querySelector('.js.afterBase64Decode').innerHTML = collectionInJS.base64Decoded;
    
    
            document.querySelector('.php.encrypted').innerHTML = collectionInPhp.after_base64_encode;
            document.querySelector('.js.encrypted').innerHTML = collectionInJS.encrypted;
    
            document.querySelector('.js.decrypted').innerHTML = collectionInJS.decrypted;
            </script>
    
        </body>
    
        </html>
    

    enter image description here


  2. In the PHP code, the key is generated as SHA256 hash of the password $secret_key. hash() returns the result hex encoded by default, which is why the result consists of 64 hex digits, i.e. 64 bytes.
    PHP/OpenSSL implicitly truncates the key length to 32 bytes according to the specified digest AES-256-CBC. CryptoJS does not automatically shorten, i.e. the shortening must be done explicitly.

    The IV is generated as SHA256 hash of the password $secret_iv. Unlike the key, it is correctly truncated to the right size.

    In order for CryptoJS to interpret the key material as key, it must be passed as WordArray. For this, the key must be converted using the UTF-8 encoder. The same applies to the IV.

    const secret_key = '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,';
    const secret_iv = '$6Za+?k}^`5q:f^@TSxy69gf7JcKuF!,';
    
    const text = 'alaks@123';
    
    const keyHex = CryptoJS.SHA256(secret_key).toString().substring(0,32); // Fix 1: Truncate the key to 32 bytes
    const ivHex = CryptoJS.SHA256(secret_iv).toString().substring(0, 16);
    const key = CryptoJS.enc.Utf8.parse(keyHex); // Fix 2: Convert key and IV to a WordArray using the UTF8 encoder
    const iv = CryptoJS.enc.Utf8.parse(ivHex); 
    
    function aesEncrypt(data) {
        let cipher = CryptoJS.AES.encrypt(data, key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return btoa(cipher.toString()); 
    }
    
    function aesDecrypt(data) {
        let cipher = CryptoJS.AES.decrypt(atob(data), key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return cipher.toString(CryptoJS.enc.Utf8);
    }
    const ciphertext = aesEncrypt('alaks@123');
    const decrypted = aesDecrypt(ciphertext);
    console.log(ciphertext); // Rko0Q0lsdnl3Ukp6RkI2bXA4STF6QT09
    console.log(decrypted);  // alaks@123
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

    The PHP code has some vulnerabilities and inefficiencies:

    • No hex encoding should be used as key, but the actual value. Analogous for the IV.
    • Instead of a static IV, a random IV should be applied.
    • Instead of a fast digest, a proven key derivation should be used (at least PBKDF2).
    • No double Base64 encoding/decoding should be applied.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search