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
2
Answers
Based on @Topaco answer, below is the full working code.
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.
The PHP code has some vulnerabilities and inefficiencies: