I'm trying to implemented AES-256 encryption by JavaScript, CryptoJS.AES.encrypt, and expected to get the same result as the code realized by the php one. However, I not only got the wrong answer, and the inconsistent string length.
Here is my JavaScript code with the parameter, key, and iv I used.
let parameter = {
MerchantID: "3430112",
RespondType: "JSON",
TimeStamp: "1485232229",
Version: "1.4",
MerchantOrderNo: "S_1485232229",
Amt: 40,
ItemDesc: "UnitTest",
}
const Hashkey = "12345678901234567890123456789012";
const HashIV = "1234567890123456";
const CryptoJS = require('crypto-js');
// ------------------INFO----------------------------------------
const key = CryptoJS.enc.Utf8.parse(Hashkey);
const iv = CryptoJS.enc.Utf8.parse(HashIV);
const encrypt_mode = {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7 || CryptoJS.pad.ZeroPadding
};
let result = create_mpg_aes_encrypt(parameter, key, encrypt_mode);
//console.log(result.length);
//console.log(result);
function create_mpg_aes_encrypt(parameter, key, encrypt_mode){
//// 將參數經過 URL ENCODED QUERY STRING
let params = new URLSearchParams(parameter);
let str = params.toString();
str = addpadding(str);
let encryptedData = CryptoJS.AES.encrypt(str, key, encrypt_mode);
encryptedData = encryptedData.toString();
//console.log(encryptedData.length);
//console.log(encryptedData);
str = encryptedData;
str = str.trim();
return str;
}
function addpadding(str){
const blocksize = 32;
let len = str.length;
let pad = blocksize - (len % blocksize);
let string = str;
for(let i = 0; i < pad; i++) {
string += String.fromCharCode(pad);
}
return string;
}
// The result I really get.
/*
/5HIqgE3nk3mIaROXxH3Lk0lvbGhgkLbbO+e8H2AsBZeR2/R2ayqUxcCcsgtEilh4aBwCnQnz6HPkNt/bWWTu8kxAqTUubZtmXTBPDGnq0u6HU4HkPDLu9etZMbTyAEqYBzqqAi/9w+UqO+lpPmEudQTBP/YeWEhd8Yi919CFPr5gMs1sJrNNskJ560tkazr
*/
Here is the php code I wanted to fulfill.
function create_mpg_aes_encrypt($parameter = "", $key = "", $iv = ""){
$return_str = '';
if(!empty($parameter)) {
// URL ENCODED QUERY STRING
$return_str = http_build_query($parameter);
//echo $return_str;
}
return trim(bin2hex(openssl_encrypt(addpadding($return_str), 'aes-256-cbc',
$key, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv)));
}
function addpadding($string, $blocksize = 32) {
$len = strlen($string);
$pad = $blocksize - ($len % $blocksize);
$string .= str_repeat(chr($pad), $pad);
return $string;
}
$trade_info_arr = array(
'MerchantID' => 3430112,
'RespondType' => 'JSON',
'TimeStamp' => 1485232229,
'Version' => 1.4,
'MerchantOrderNo' => 'S_1485232229',
'Amt' => 40,
'ItemDesc' => 'UnitTest'
);
$mer_key ='12345678901234567890123456789012';
$mer_iv = '1234567890123456';
// Result I expected to get
/*ff91c8aa01379e4de621a44e5f11f72e4d25bdb1a18242db6cef9ef07d80b0165e476fd1d9acaa53170272c82d122961e1a0700a7427cfa1cf90db7f6d6593bbc93102a4d4b9b66d9974c13c31a7ab4bba1d4e0790f0cbbbd7ad64c6d3c8012a601ceaa808bff70f94a8efa5a4f984b9d41304ffd879612177c622f75f4214fa*/
enter code here
There are only two minor bugs in the CryptoJS code:
The default padding (PKCS7 with the AES block size of 16 bytes) must be disabled, because a custom padding (PKCS7 with a block size of 32 bytes) is implemented in addpadding()
:
padding: CryptoJS.pad.ZeroPadding
The ciphertext must be returned hex encoded and not Base64 encoded:
encryptedData = encryptedData.ciphertext.toString(CryptoJS.enc.Hex);
With these two changes, the CryptoJS code works and provides the desired result:
let parameter = {
MerchantID: "3430112",
RespondType: "JSON",
TimeStamp: "1485232229",
Version: "1.4",
MerchantOrderNo: "S_1485232229",
Amt: 40,
ItemDesc: "UnitTest",
}
const Hashkey = "12345678901234567890123456789012";
const HashIV = "1234567890123456";
//const CryptoJS = require('crypto-js');
// ------------------INFO----------------------------------------
const key = CryptoJS.enc.Utf8.parse(Hashkey);
const iv = CryptoJS.enc.Utf8.parse(HashIV);
const encrypt_mode = {
iv: iv,
mode: CryptoJS.mode.CBC,
//padding: CryptoJS.pad.Pkcs7 || CryptoJS.pad.ZeroPadding // Fix 1: Disable padding
padding: CryptoJS.pad.ZeroPadding
};
let result = create_mpg_aes_encrypt(parameter, key, encrypt_mode);
//console.log(result.length);
console.log(result.replace(/(.{48})/g,'$1\n'));
function create_mpg_aes_encrypt(parameter, key, encrypt_mode){
//// 將參數經過 URL ENCODED QUERY STRING
let params = new URLSearchParams(parameter);
let str = params.toString();
str = addpadding(str);
let encryptedData = CryptoJS.AES.encrypt(str, key, encrypt_mode);
//encryptedData = encryptedData.toString(); // Fix 2: Hex encode ciphertext
encryptedData = encryptedData.ciphertext.toString(CryptoJS.enc.Hex);
//console.log(encryptedData.length);
//console.log(encryptedData);
str = encryptedData;
str = str.trim();
return str;
}
function addpadding(str){
const blocksize = 32;
let len = str.length;
let pad = blocksize - (len % blocksize);
let string = str;
for(let i = 0; i < pad; i++) {
string += String.fromCharCode(pad);
}
return string;
}
// The result I really get.
/*
/5HIqgE3nk3mIaROXxH3Lk0lvbGhgkLbbO+e8H2AsBZeR2/R2ayqUxcCcsgtEilh4aBwCnQnz6HPkNt/bWWTu8kxAqTUubZtmXTBPDGnq0u6HU4HkPDLu9etZMbTyAEqYBzqqAi/9w+UqO+lpPmEudQTBP/YeWEhd8Yi919CFPr5gMs1sJrNNskJ560tkazr
*/
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
Note that the PHP reference code unnecessarily applies a block size of 32 bytes for padding. Usually padding is done to the block size of the algorithm used, which is 16 bytes for AES. Padding to 32 bytes is not a bug, but it unnecessarily increases the size of the ciphertext.
Also, a static IV is insecure (the IV must be randomly generated for each encryption) and similarly a password as the key (in the case of a password, use a password-based key derivation function such as Argon2 or PBKDF2), though the posted key and IV may only be used for testing purposes, which is of course OK.