Search code examples
javascriptphpcryptojstripledes

Getting substring of a CryptoJS's TripleDES encrypted string


I've a legacy php code that encrypts with TripleDES method as a part of a bank flow to encrypt purchase operations. This is the code:

/******  3DES Function  ******/
function encrypt_3DES($message, $key){
    $l = ceil(strlen($message) / 8) * 8;
    return substr(openssl_encrypt($message . str_repeat("\0", $l - strlen($message)), 'des-ede3-cbc', $key, OPENSSL_RAW_DATA, "\0\0\0\0\0\0\0\0"), 0, $l);
}

I want to reproduce the same code in javascript using CriptoJS's TripleDES. This is my trial so far:

<script src=https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/tripledes.js></script>
<script>

    function encrypt_3DES(message, key) {
        // Parse input parameters
        message = CryptoJS.enc.Utf8.parse(message);
        key = CryptoJS.enc.Utf8.parse(key);
        // Build an iv with zero values
        let iv = CryptoJS.lib.WordArray.create(64/8);
        let encrypted = CryptoJS.TripleDES.encrypt(message, key, {iv: iv});
        // Encrypt with TripleDES
        return encrypted.toString();
    }

    let message = "testMessage";
    let key = "testKey";

    let encrypted = encrypt_3DES(message, key);
    // encrypted: "HLu4p18KtFfy5VjwjFkDHA=="
    
</script>

Taking the example values, the result is the string "HLu4p18KtFfy5VjwjFkDHA==".

The problem is that the results between js and php are slightly different, and my suspictions are pointing to the substr operation done on the php side, because when I'm converting the result of the open_ssl operation (without the substring), my javascript code is returning the same base64 string:

<?php

/******  3DES Function  ******/
function encrypt_3DES($message, $key)
{
    $l = ceil(strlen($message) / 8) * 8;
    return substr(openssl_encrypt($message . str_repeat("\0", $l - strlen($message)), 'des-ede3-cbc', $key, OPENSSL_RAW_DATA, "\0\0\0\0\0\0\0\0"), 0, $l);
}

$message = "testMessage";
$key = "testKey";

$res = base64_encode(encrypt_3DES($message, $key));
$res2 = base64_encode(openssl_encrypt($message, 'des-ede3-cbc', $key, OPENSSL_RAW_DATA, "\0\0\0\0\0\0\0\0"));

/* $res: HLu4p18KtFfTr47KvI/WEw== (My goal is to get this in javascript) */
/* $res2: HLu4p18KtFfy5VjwjFkDHA== (without the substr operation, the results match) */
?>

So, my question is: Is there a way to apply the same substring to the encrypted result of the javascript's CryptoJS operation?


Solution

  • The PHP code implements a custom zero padding. However, the default PKCS#7 padding is not disabled, so in addition to the explicit zero padding, a full block is unnecessarily appended by the implicit PKCS#7 padding.
    This last unnecessary block is removed using substr(). The bottom line is that simply zero padding is used.

    A more efficient PHP implementation would be to just disable the default padding with OPENSSL_ZERO_PADDING instead of truncating the last block:

    return openssl_encrypt($message . str_repeat("\0", $l - strlen($message)), 'des-ede3-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, "\0\0\0\0\0\0\0\0"). 
    

    Anyway, in order for the C# code to return the same ciphertext as the PHP code, the padding in the C# code needs to be changed to CryptoJS.pad.ZeroPadding.


    Note that a static IV (like the zero IV used here) is insecure, TripleDES is slow (better: AES) and zero padding is unreliable (better: PKCS#7).
    Also, valid key sizes for TripleDES are 24 bytes and 16 bytes.


    Test:

    The PHP code below:

    $key = '012345678901234567890123';
    $message = 'The quick brown fox jumps over the lazy dog';
    print(base64_encode(encrypt_3DES($message, $key)) . PHP_EOL);
    

    produces the following Base64 encoded ciphertext:

    HNQad59lGkM83byQY2pwhluVkB7YOyIx/kGh/ibcIXQdFoiODkD1kTIIEHhqcoFm
    

    The fixed JavaScript code gives the same ciphertext.

    function encrypt_3DES(message, key) {
        // Parse input parameters
        message = CryptoJS.enc.Utf8.parse(message);
        key = CryptoJS.enc.Utf8.parse(key);
        // Build an iv with zero values
        let iv = CryptoJS.lib.WordArray.create(64/8);
        let encrypted = CryptoJS.TripleDES.encrypt(message, key, {iv: iv, padding: CryptoJS.pad.ZeroPadding});
        // Encrypt with TripleDES
        return encrypted.toString();
    }
    
    let message = "The quick brown fox jumps over the lazy dog";
    let key = "012345678901234567890123";
    
    let encrypted = encrypt_3DES(message, key);
    document.getElementById("ct").innerHTML = encrypted;
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
    <p style="font-family:'Courier New', monospace;" id="ct"></p>