Javascript DES加密/ PHP解密

I have a PHP endpoint that is used by several websites. While POSTing data to the endpoint the sites use encrypted headers to identify various information. The sites use the standard PHP mcrypt_ecb function, and the endpoint decrypts the variables like this:

$var = trim(mcrypt_ecb(MCRYPT_DES, $key, base64_decode($input), MCRYPT_DECRYPT)));

I don't really have much chance of altering the PHP endpoint, although I could conceivably make another endpoint if mcrypt_ecb doesn't play nicely.

I now have a PhoneGap app, which also needs to send the encrypted headers. Phonegap can only really use javascript to POST to a remote server. I've looked at the Google CryptoJS to try to acheive this, along side a base64 library. Note that the key is the same as the $key var above.

var encrypted = CryptoJS.DES.encrypt(user.email, key).toString();
var base64 = base64.encode(encrypted);

The problem is that the encrypted string (and I have tried without the base64 encode/decode) when passed through mcrypt_ecb is garbage. Apparently there are different methods of triple DES encryption?

How would I create a mcrypt_ecb compatible encrypted string in JavaScript?

I used the same library. Had to ensure that the des() result was trimmed as there was some odd characters after and it wasn't matching.

$password = trim(des("12345678", hexToString("1212121121"), 0, 1, null));

Hope it helps someone.

As per: http://php.net/manual/en/function.mcrypt-decrypt.php

Warning This function (mcrypt-decrypt) has been DEPRECATED as of PHP 7.1.0. Relying on this function is highly discouraged.

Then I had to investigate a lot until I discover a way to encrypt using openssl via javascript and decrypt using php.

The only problem I got is when I send a long text. That's because RSA, by definition, supports strings of limited lenght.

https://security.stackexchange.com/questions/33434/rsa-maximum-bytes-to-encrypt-comparison-to-aes-in-terms-of-security/33445#33445

RSA, as defined by PKCS#1, encrypts "messages" of limited size. With the commonly used "v1.5 padding" and a 2048-bit RSA key, the maximum size of data which can be encrypted with RSA is 245 bytes. No more.

i.e. If I use private_key_bits of 1024 I can send

"José compró en Perú una vieja zampoña. Excusándose, Sofía tiró su whisky al desagüe de la banqueta."

nothing longer. If I use private_key_bits of 512 I can send

"José compró en Perú una vieja zampoña. Excusánd"

nothing longer.

On long strings JavaScript console reports: "Message too long for RSA"

Then if you want to encrypt long strings you must compress and split them before javascript encryption and after decryption join and uncompress on php, I think zlib is a good solution for split/join because it is supported on javascript and php.

Maybe is better to use an AES library instead of this one like https://github.com/brainfoolong/cryptojs-aes-php

My working code is as follows:

<?php
    //------------------------------------------------------------
    // Global Settings.
    //------------------------------------------------------------
    ini_set('display_errors', 1);
    error_reporting(E_ALL);
    $directorio = "/path/to/key/directory/apache/writable/";
    $nombre_base = "llaves_php";

    //------------------------------------------------------------
    // Initialization.
    //------------------------------------------------------------
    $encabezado_html = "";
    $cuerpo_html = "";

    //------------------------------------------------------------
    // Loading keys
    //------------------------------------------------------------
    list($privateKey, $pubKey) =
        cargar_llaves_RSA($directorio, $nombre_base);

    //------------------------------------------------------------
    // Form that uses javascript to encrypt data.
    // (it uses only the public key)
    //------------------------------------------------------------
    $librerias_html = "
        <script type='text/javascript'
                src='https://ajax.googleapis.com/ajax/libs/".
                    "jquery/3.2.1/jquery.min.js'></script>
        <script type='text/javascript'
                src='lib/jsencrypt.js'></script>
        ";

    $pubKey_html = htmlentities($pubKey);
    $datos_html = "
        <h2>Cifrando con Javascript</h2>
        <input type='text' id='mensaje' />
        <br />
        <button id='ENVIAR'>Enviar</button>
        <br />
        <textarea id='pubkey' style='display: none;'>".
        $pubKey_html.
        "</textarea>
        <script type='text/javascript'>
            $('#ENVIAR').click(function () {
                var codificador = new JSEncrypt();
                codificador.setKey($('#pubkey').val());
                var cifrado = codificador.encrypt($('#mensaje').val());
                window.open('?mensaje=' + encodeURIComponent(cifrado)
                           , '_top');
            });
        </script>
        ";

    //------------------------------------------------------------
    // Decrypting using php (it uses only the privateKey)
    //------------------------------------------------------------
    if (isset($_REQUEST['mensaje'])) {
        openssl_private_decrypt( base64_decode($_REQUEST['mensaje'])
                               , $descifrado
                               , $privateKey);
        $datos_html.= "
            <h2>Descifrando con PHP</h2>
            ".$descifrado."
            ";
    }

    //------------------------------------------------------------
    // HTML DISPLAY
    //------------------------------------------------------------
    $encabezado_html.= "<title>Receptor de mensaje cifrado</title>"
                     . $librerias_html;

    $cuerpo_html.= $datos_html;

    $contenido = "<head>$encabezado_html</head><body>$cuerpo_html</body>";
    $contenido = "<html>$contenido</html>";
    print $contenido;

//============================================================
//============================================================
// Functions
//============================================================
//============================================================

    //------------------------------------------------------------
    function cargar_llaves_RSA($directorio, $nombre_base) {
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // PROPÓSITO: Genera o carga desde archivos las llaves RSA
    // ENTRADAS:
    // $directorio: Directorio donde se encuentran los archivos.
    // $nombre_base: Nombre, sin extensión, de los archivos con
    //               las llaves.
    // SALIDAS:
    //------------------------------------------------------------
        if (  !file_exists($directorio.$nombre_base.".crt")
           || !file_exists($directorio.$nombre_base.".pub")) {
            list($privateKey, $pubKey) = crear_llaves_RSA($directorio.$nombre_base);
        } else {
            //------------------------------------------------------------
            // CARGA DE LLAVES RSA ARCHIVADAS
            //------------------------------------------------------------
            $privateKey = file_get_contents($directorio.$nombre_base.".crt");
        if (!$privKey = openssl_pkey_get_private($privateKey))
            die('Loading Private Key failed');
            $pubKey  = file_get_contents($directorio.$nombre_base.".pub");
        }

    return array($privateKey, $pubKey);
    }

    //------------------------------------------------------------
    function crear_llaves_RSA($ruta_base) {
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // PROPÓSITO:
    // generacion de llaves RSA en php
    // ENTRADAS:
    // $ruta_base: Ruta de los archivos a generar sin extensión.
    // SALIDAS:
    // Se generarán dos archivos, uno con la llave privada con
    // extensión .crt, el otro con llave pública con extensión
    // .pub; la función retorna tanto la llave pública como la
    // privada en un arreglo.
    //------------------------------------------------------------
        $config = array(
            "private_key_bits" => 1024,
            "private_key_type" => OPENSSL_KEYTYPE_RSA,
        );

        $llavePrivadaCruda = openssl_pkey_new($config);
        openssl_pkey_export_to_file($llavePrivadaCruda, $ruta_base.".crt");
        $privateKey = file_get_contents($ruta_base.".crt");
        openssl_pkey_export($llavePrivadaCruda, $privKey);

        $pubKeyData = openssl_pkey_get_details($llavePrivadaCruda);
        $pubKey = $pubKeyData["key"];
        file_put_contents($ruta_base.".pub", $pubKey);
        openssl_free_key($llavePrivadaCruda);

    return array($privateKey, $pubKey);
    }

    //------------------------------------------------------------
    function Mostrar($valor) {
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // PROPÓSITO: Genera el código HTML para presentar una
    // variable embebida en la página.
    // ENTRADAS:
    // $valor: el valor a presentar.
    // SALIDAS: código html que permite visualizar la variable.
    //------------------------------------------------------------
        $retorno = htmlentities(stripslashes(var_export($valor, true)));
        $retorno = "<pre>$retorno</pre>";
        return $retorno;
    }

?>

Directory tree must looks like:

├── script.php
└── lib
    └── jsencrypt.js

and a directory writable by php outside of public zone named

/path/to/key/directory/apache/writable/

This answer was posted as an edit to the question by Op. Although asked to post as an answer, it was left as an edit to the original question.


I've resolved this issue, by using a different javascript encryption library. Here is how:

I used Paul Tero's simpler DES library for the javascript, and the standard PHP decrypt functions.

Javascript:

var key = '12345678';
var encrypted = stringToHex(des(key, 'This is something private', 1));

Things to note:

  • the key must (apparently) have an even number of chars. I used 8. I imagine 16 or more would be better, but as you can probably tell, I know nothing about Cryptography.
  • The function stringToHex is included in the DES javascript code.

PHP

$key = '12345678'
$unencrypted = trim(mcrypt_decrypt(MCRYPT_DES
                                  , $key
                                  , safeHexToString($encrypted)
                                  , MCRYPT_MODE_ECB))

Using this helper function:

/**
 *
 * Convert input HEX encoded string to a suitable format for decode
 * // Note: JS generates strings with a leading 0x
 *
 * @return string
 */
function safeHexToString($input)
{
    if(strpos($input, '0x') === 0)
    {
        $input = substr($input, 2);
    }
    return hex2bin($input);
}

That did it for me. The javascript hex-encoded strings having a leading '0x' caused some issues for me as I was debugging in a particularly silly way.

So I hope this helps.