简体   繁体   中英

Implement Php open_ssl_decrypt AES 256 CBC as CryptoJS

I try to make the following code in ReactJs (not NodeJs) but this doesn't work in JS.

The original code in PHP works fine:

    function decryptOpensslDigestSHA256Sum($data)
    {
        $key = hash('sha256', 'Nootric2703202'); //My password has 14 characters

        $method = 'AES-256-CBC';

        $data = base64_decode($data);

        $iv_size = openssl_cipher_iv_length($method);

        $salt_header = substr($data, 0, $iv_size);

        if (substr($salt_header, 0, 8) != "Salted__") {
            return "";
        }
        $salt = substr($salt_header, 8);

        $creds = extractOpenSSLCreds($key, $salt, $iv_size);

        $data = openssl_decrypt(substr($data, $iv_size), $method, $creds['password'], OPENSSL_RAW_DATA, $creds['iv']);

        return $data;
    }

    function extractOpenSSLCreds($key, $salt, $iv_size)
    {
        $m = "";
        while (strlen($m) < 48) {
            $m .= hashCryptoDigestSHA256Sum($m, $key, $salt);
        }
        $result = array(
            'password' => substr($m, 0, 32),
            'iv' => substr($m, 32, $iv_size)
        );
        return $result;
    }

    function hashCryptoDigestSHA256Sum($hash, $key, $salt)
    {
        $hash.= $key.$salt;
        $prev = openssl_digest($hash, "sha256", true);
        return $prev;
    }

If I invoke in php this function:

$data = "U2FsdGVkX1++7PN6CsF5Bi38t0N3EjXpH5oGpaIZXUwk4T8QCwcATjvA4b/8VaxD8nf/MZhKPnWb1L8raLR4lw==";
echo "Data urlEncoded: $data<br>";
$decryption = decryptOpensslDigestSHA256Sum($data);
echo "Data decrypted: $decryption<br><br>";

This shows: Data decrypted: email=abc@xyz.abc&name=&gpw_id=gpwID

But when I try to use in JS this function this doesn't work (the source code I found on this Stackoverflow entry decrypt-openssl-aes-256-cbc-in-browser-cryptojs

function CryptoJSAesDecrypt(encrypted){
    // 1. Separate ciphertext and salt
    var encryptedWA = CryptoJS.enc.Base64.parse(encrypted);
    var prefixWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(0, 8/4)); // Salted__ prefix

    var saltWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(8/4, 16/4));  // 8 bytes salt: 0x0123456789ABCDEF
    var ciphertextWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(16/4, encryptedWA.words.length)); // ciphertext        

    // 2. Determine key and IV using PBKDF2
    var password = 'Nootric2703202'
    var keyIvWA = CryptoJS.PBKDF2(
    password, 
    saltWA, 
    {
        keySize: (32+16)/4,          // key and IV
        iterations: 10000,
        hasher: CryptoJS.algo.SHA256
    }
    );
    var keyWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(0, 32/4));
    var ivWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(32/4, (32+16)/4));

    // 3. Decrypt
    var decryptedWA = CryptoJS.AES.decrypt(
    {ciphertext: ciphertextWA}, 
    keyWA, 
    {iv: ivWA}
    );
    var decrypted = decryptedWA.toString(CryptoJS.enc.Utf8)
    return decrypted;
}

If I invoke the last function in JS:

    const dec = CryptoJSAesDecrypt("U2FsdGVkX1++7PN6CsF5Bi38t0N3EjXpH5oGpaIZXUwk4T8QCwcATjvA4b/8VaxD8nf/MZhKPnWb1L8raLR4lw==");
    console.log("Data Decrypted: " + dec);

This shows empty: Data Decrypted:

If I invoke with this data:

    const dec = CryptoJSAesDecrypt("U2FsdGVkX18BI0VniavN78vlhR6fryIan0VvUrdIr+YeLkDYhO2xyA+/oVXJj/c35swVVkCqHPh9VdRbNQG6NQ==");
    console.log("Data Decrypted: " + dec);

And in the Javascript function I replace this line:

var password = 'Nootric2703202'

with this one:

var password = 'mypassword'

This works fine. But with my own password & my encrypted data this JS decrypt code doesn't work? Some help please?

 const dec = CryptoJSAesDecrypt(); console.log("Decrypted: " + dec); function CryptoJSAesDecrypt(encrypted){ // 1. Separate ciphertext and salt var encrypted = "U2FsdGVkX1++7PN6CsF5Bi38t0N3EjXpH5oGpaIZXUwk4T8QCwcATjvA4b/8VaxD8nf/MZhKPnWb1L8raLR4lw=="; console.log("Encrypted:", encrypted); var encryptedWA = CryptoJS.enc.Base64.parse(encrypted); var prefixWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(0, 8/4)); // Salted__ prefix var saltWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(8/4, 16/4)); // 8 bytes salt: 0x0123456789ABCDEF var ciphertextWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(16/4, encryptedWA.words.length)); // ciphertext // 2. Determine key and IV using PBKDF2 var password = 'Nootric2703202' var keyIvWA = CryptoJS.PBKDF2( password, saltWA, { keySize: (32+16)/4, // key and IV iterations: 10000, hasher: CryptoJS.algo.SHA256 } ); var keyWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(0, 32/4)); var ivWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(32/4, (32+16)/4)); // 3. Decrypt var decryptedWA = CryptoJS.AES.decrypt( {ciphertext: ciphertextWA}, keyWA, {iv: ivWA} ); var decrypted = decryptedWA.toString(CryptoJS.enc.Utf8) return decrypted; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>


Here is the source code of PHP openssl-decrypt (It's an openssl.c file) PHP openssl.c :

PHP_OPENSSL_API zend_string* php_openssl_decrypt(
    const char *data, size_t data_len,
    const char *method, size_t method_len,
    const char *password, size_t password_len,
    zend_long options,
    const char *iv, size_t iv_len,
    const char *tag, zend_long tag_len,
    const char *aad, size_t aad_len)
{
    const EVP_CIPHER *cipher_type;
    EVP_CIPHER_CTX *cipher_ctx;
    struct php_openssl_cipher_mode mode;
    int i = 0, outlen;
    zend_string *base64_str = NULL;
    bool free_iv = 0, free_password = 0;
    zend_string *outbuf = NULL;

    PHP_OPENSSL_CHECK_SIZE_T_TO_INT_NULL_RETURN(data_len, data);
    PHP_OPENSSL_CHECK_SIZE_T_TO_INT_NULL_RETURN(password_len, password);
    PHP_OPENSSL_CHECK_SIZE_T_TO_INT_NULL_RETURN(aad_len, aad);
    PHP_OPENSSL_CHECK_SIZE_T_TO_INT_NULL_RETURN(tag_len, tag);


    cipher_type = EVP_get_cipherbyname(method);
    if (!cipher_type) {
        php_error_docref(NULL, E_WARNING, "Unknown cipher algorithm");
        return NULL;
    }

    cipher_ctx = EVP_CIPHER_CTX_new();
    if (!cipher_ctx) {
        php_error_docref(NULL, E_WARNING, "Failed to create cipher context");
        return NULL;
    }

    php_openssl_load_cipher_mode(&mode, cipher_type);

    if (!(options & OPENSSL_RAW_DATA)) {
        base64_str = php_base64_decode((unsigned char*)data, data_len);
        if (!base64_str) {
            php_error_docref(NULL, E_WARNING, "Failed to base64 decode the input");
            EVP_CIPHER_CTX_free(cipher_ctx);
            return NULL;
        }
        data_len = ZSTR_LEN(base64_str);
        data = ZSTR_VAL(base64_str);
    }

    if (php_openssl_cipher_init(cipher_type, cipher_ctx, &mode,
                &password, &password_len, &free_password,
                &iv, &iv_len, &free_iv, tag, tag_len, options, 0) == FAILURE ||
            php_openssl_cipher_update(cipher_type, cipher_ctx, &mode, &outbuf, &outlen,
                data, data_len, aad, aad_len, 0) == FAILURE) {
        outbuf = NULL;
    } else if (mode.is_single_run_aead ||
            EVP_DecryptFinal(cipher_ctx, (unsigned char *)ZSTR_VAL(outbuf) + outlen, &i)) {
        outlen += i;
        ZSTR_VAL(outbuf)[outlen] = '\0';
        ZSTR_LEN(outbuf) = outlen;
    } else {
        php_openssl_store_errors();
        zend_string_release_ex(outbuf, 0);
        outbuf = NULL;
    }

    if (free_password) {
        efree((void *) password);
    }
    if (free_iv) {
        efree((void *) iv);
    }
    if (base64_str) {
        zend_string_release_ex(base64_str, 0);
    }
    EVP_CIPHER_CTX_reset(cipher_ctx);
    EVP_CIPHER_CTX_free(cipher_ctx);
    return outbuf;
}

/* {{{ Takes raw or base64 encoded string and decrypts it using given method and key */
PHP_FUNCTION(openssl_decrypt)
{
    zend_long options = 0;
    char *data, *method, *password, *iv = "", *tag = NULL, *aad = "";
    size_t data_len, method_len, password_len, iv_len = 0, tag_len = 0, aad_len = 0;
    zend_string *ret;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|lsss", &data, &data_len, &method, &method_len,
                    &password, &password_len, &options, &iv, &iv_len, &tag, &tag_len, &aad, &aad_len) == FAILURE) {
        RETURN_THROWS();
    }

    if (!method_len) {
        zend_argument_value_error(2, "cannot be empty");
        RETURN_THROWS();
    }

    if ((ret = php_openssl_decrypt(data, data_len, method, method_len, password, password_len, options, iv, iv_len, tag, tag_len, aad, aad_len))) {
        RETVAL_STR(ret);
    } else {
        RETVAL_FALSE;
    }
}

And the source code for PHP openssl-digest is this:

/* {{{ Computes digest hash value for given data using given method, returns raw or binhex encoded string */
PHP_FUNCTION(openssl_digest)
{
    bool raw_output = 0;
    char *data, *method;
    size_t data_len, method_len;
    const EVP_MD *mdtype;
    EVP_MD_CTX *md_ctx;
    unsigned int siglen;
    zend_string *sigbuf;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss|b", &data, &data_len, &method, &method_len, &raw_output) == FAILURE) {
        RETURN_THROWS();
    }
    mdtype = EVP_get_digestbyname(method);
    if (!mdtype) {
        php_error_docref(NULL, E_WARNING, "Unknown digest algorithm");
        RETURN_FALSE;
    }

    siglen = EVP_MD_size(mdtype);
    sigbuf = zend_string_alloc(siglen, 0);

    md_ctx = EVP_MD_CTX_create();
    if (EVP_DigestInit(md_ctx, mdtype) &&
            EVP_DigestUpdate(md_ctx, (unsigned char *)data, data_len) &&
            EVP_DigestFinal (md_ctx, (unsigned char *)ZSTR_VAL(sigbuf), &siglen)) {
        if (raw_output) {
            ZSTR_VAL(sigbuf)[siglen] = '\0';
            ZSTR_LEN(sigbuf) = siglen;
            RETVAL_STR(sigbuf);
        } else {
            int digest_str_len = siglen * 2;
            zend_string *digest_str = zend_string_alloc(digest_str_len, 0);

            make_digest_ex(ZSTR_VAL(digest_str), (unsigned char*)ZSTR_VAL(sigbuf), siglen);
            ZSTR_VAL(digest_str)[digest_str_len] = '\0';
            zend_string_release_ex(sigbuf, 0);
            RETVAL_NEW_STR(digest_str);
        }
    } else {
        php_openssl_store_errors();
        zend_string_release_ex(sigbuf, 0);
        RETVAL_FALSE;
    }

    EVP_MD_CTX_destroy(md_ctx);
}

The PHP implementation uses EVP_BytesToKey() as key derivation function and is thus compatible with the CryptoJS key derivation.

However, CryptoJS applies MD5 as digest by default, while the PHP code uses SHA256 (note that OpenSSL has changed the default digest from MD5 to SHA256 as of version v1.1.0).
Moreover, the password applied for the key derivation function is not the password itself (ie Nootric2703202 ) but the hex encoded SHA256 hash of the password.

If this is taken into account, decryption with CryptoJS is:

 var password = 'Nootric2703202'; var passwordHashWA = CryptoJS.SHA256(password); var passwordHashHex = passwordHashWA.toString(CryptoJS.enc.Hex); var ciphertext = 'U2FsdGVkX1++7PN6CsF5Bi38t0N3EjXpH5oGpaIZXUwk4T8QCwcATjvA4b/8VaxD8nf/MZhKPnWb1L8raLR4lw=='; CryptoJS.algo.EvpKDF.cfg.hasher = CryptoJS.algo.SHA256.create(); var data = CryptoJS.AES.decrypt(ciphertext, passwordHashHex); console.log(data.toString(CryptoJS.enc.Utf8));
 <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

which produces the expected plaintext:

email=abc@xyz.abc&name=&gpw_id=gpwID

Due to OpenSSL compatibility, the ciphertext can also be decrypted with the following OpenSSL expression:

openssl enc -aes-256-cbc -d -md sha256 -in <ciphertextFile> -k d0f95d5e54a7aa25934a5d4915c9e2a06dadac20d16551693be1d21d4d8e8798 -A -a -p

where <ciphertextFile> is the path to a file containing the Base64 encoded ciphertext (without linebreaks): U2FsdGVkX1... , and the password d0f95d... is the hex encoded SHA256 hash of the password Nootric2703202 .

Please keep in mind that EVP_BytesToKey() is considered insecure, seg here . Instead, a reliable key derivation function like PBKDF2 should be used.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM