2-way string encryption in PHP - which of these is more secure?
Asked Answered
S

1

5

I have a need to pass a value through the query string to be displayed on my page. The value will have to be encrypted as it contains some PII for the user viewing the page. And in order to make readable to the user, it needs to be able to be decrypted when displayed.

I'm using PHP and research so far has led me to openssl_encrypt and openssl_decrypt along with these 2 code resources:

  1. https://bhoover.com/using-php-openssl_encrypt-openssl_decrypt-encrypt-decrypt-data/
  2. https://gist.github.com/joashp/a1ae9cb30fa533f4ad94

I liked #1 a lot because of the way the iv was actually attached to the returned, encrypted string. This allows us NOT to have to store an iv as a constant and to be able to generate a new one each time the function is called. This seems more secure to me than using the same key and iv every time. Is it really though? And if so, is it for any reasons I should know about beyond the painfully obvious?. The thing I didn't like is that I think concatenating the iv and key with a character/string (in this case ::) that can be found occurring naturally in other potential cypher-text or iv became problematic. Using this method, in attempting to encrypt 7000+ email addresses, a little over half of them ended up with these weird characters, ���6CTΣAmong others) in the decoded string thus breaking it.

#2 was great because it worked!! I've yet to find a string that will break it... especially in my email list. BUT as mentioned above, this requires. the iv and key to always be the same values and stored in a variable somewhere. This seemed like 1 tiny maintenance issue, but more of a security thing.

So I did a little more reading/thinking and came up with this working example - here's the code:

<?php

// generate key with base64_encode(openssl_random_pseudo_bytes(32);
// and save it in a constant.
define('ENCRYPT_KEY_1', 'CuH8WPfXzMj0xRWybHjssWJ+IhTDqL5w0OD9+zXFloA=');

function encrypt_decrypt($action, $string) {
    $output = false;

    $encrypt_method = "AES-256-CBC";
    $key = ENCRYPT_KEY_1;
    
    $ivLen = openssl_cipher_iv_length($encrypt_method);
    

    /**
     * the key was generated with 32 pseudo-bytes and then base64Encod()-ed.
     * Not sure of the reason for encoding - just decoding in case it's necessary.
     * 
     * Thoughts?
     * **/
    $key = base64_decode($key);

    if ( $action == 'encrypt' ) {
        /**
        * "AES-256-CBC" expects a 16-byte string - create an 8-byte string to be 
        * converted to a 16-byte hex before being used as the initialization vector
        * TLDR" in order to end up with 16-bytes to feed to openssl_random_pseudo_bytes,
        * divide initial length in half as the hex value will double it
        * */
        $iv = openssl_random_pseudo_bytes($ivLen/2);
        $iv = bin2hex($iv);
        $tmp_data_str_in = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
        $output = base64_encode($tmp_data_str_in . $iv);
    } else if( $action == 'decrypt' ) {
        $data_str_in = base64_decode($string);
        
        // This time, rather than generating one, we get the iv (converted to hex)
        // by grabbing the last 16 characters of the concatenation of the 2 that happened
        // during encryption.
        $iv = substr($data_str_in, -$ivLen);
        
        // And now we just grab everything BUT the last 16 characters. We'll
        // run openssl_decrypt and return a copy of the original input
        $encrypted_txt_str = substr($data_str_in, 0, strlen($data_str_in)-$ivLen); 

        // Notice we are returning the encrypted value of a string consisting of
        // encoded versions of the input text and a random `IV` - we'll grab the `IV`
        // from it later in order to decrypt later. 
        $output = openssl_decrypt($encrypted_txt_str, $encrypt_method, $key, 0, $iv);
    }

    return $output;
}

$plain_txt = "[email protected]";
echo "Plain Text = " .$plain_txt. "\n";  

$encrypted_txt = encrypt_decrypt('encrypt', $plain_txt);
echo "Encrypted Text = " .$encrypted_txt. "\n";

$decrypted_txt = encrypt_decrypt('decrypt', $encrypted_txt);
echo "Decrypted Text = " .$decrypted_txt. "\n";

if ( $plain_txt === $decrypted_txt ) echo "SUCCESS";
else echo "FAILED";

echo "\n";
?>

So I guess my main questions are:

  1. am I right in thinking a solution that uses a dynamic iv generated at the time the function is executed is more secure than having a static iv defined well ahead of time and used for every encryption? If not, what am I missing?
  2. what openings for (potentially successful) attack so any/all of these approaches expose? How can they be fixed or modified to mitigate the risk
  3. Are any (hopefully the one I put together) of these approaches acceptable for being used in a production environment on a site that display's a user's PII - nothing banking or super-top-secret in nature - and allows him to make updates? It's being used in a PHP looking a little something like: print "<li>Email: " . encrypt_decrypt('decrypt', my_sanitize_fxn($_GET['ue']) . "</li">;

Quick note on #3:

I'm guessing it would be FAR better to encrypt something that ISN'T PII (such as a user's unique id in the database) to send through the query string, then decrypt that value and use it to look up his email address with a DB query. And although I'll probably end up going that way in he end of it all, lets just say there factors in play at the moment (for which explaining would drag this question SO far off topic) that is preventing it from being even a remotely viable option.

I think learning about what I've got here will carry over into the final solution nicely. I'd love to hear about anything done particularly poorly or well or just general comments in addition to answers to some of the formal questions I've posed throughout.

Thanks in advance for any wisdom you care to share.

Survivor answered 23/9, 2020 at 6:58 Comment(4)
The usage of a static IV would make a AES-CBC-mode encryption UNSECURE so you are on a good way. Please consider that a potential attacker could do an easy "tampering" attack (here you can change the data without knowledge of the key by simple xor-ing the IV in the right way. That said you should consider to use an authenticated AES mode like GCM to get a higher level of security. It is not much more complicated as CBC mode (you just have to add another variable (called "tag") to the ciphertext as you did it with the IV.Unmeet
@MichaelFehr - wow i just watched all that fly right over my head. I'm going to read up on authenticated AES modes but was wondering if you had any resources in mind that might contain some examples similar to mine?Survivor
This question appears to be off-topic because it is a code review request. This might be better suited to the Code Review Stack Exchange site. Before posting there be sure to read their FAQ to ensure that your question meets their guidelines.Ichnite
@JohnConde - I respectfully disagree. I'm asking how to write code for a specific feature and gave a few examples of things I've tried so far (so that I'm not just coming here looking for someone else tot do my work for me). If you view michael Fehr's answer below, you'll see that my code was missing the use of an entire parameter for the function I'm inquiring about. It sounds like i don't fully how to use that function and could use some assistance as it pertains to my use case.Survivor
U
9

Sorry for being lazy to adopt my example to your code but it should be not so complicated as the following code is a full sample for an AES GCM 256 string encryption with random IV. The IV and tag are prepended to the ciphertext and then Base64-encoded.

Please note that the code does not have any error handling and is for educational purpose only ! Do not use static keys for encryption.

Output:

Sample AES GCM 256 string encryption
Please note that this code does not have any error handling and is for educational purpose only
Do NOT use static keys for encryption !

plaintext: The quick brown fox jumps over the lazy dog
encrypt: jemvFuwhIaUYx49d1nap6uKz8wMIorvQuRD/PGt+SYhFt8iaK1fiqAf8CjWtVNYqFZATStgq2XQuUAhbnhMtpzHDPN7oUFo=
decrypt: The quick brown fox jumps over the lazy dog

code:

<?php
function encrypt($encryptionKey, $data) {
    $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-gcm'));
    $encrypted = openssl_encrypt($data, 'aes-256-gcm', $encryptionKey, OPENSSL_RAW_DATA, $iv, $tag);
    return base64_encode($iv . $tag . $encrypted);
}

function decrypt($encryptionKey, $data) {
    $c = base64_decode($data);
    $ivlen = openssl_cipher_iv_length($cipher="AES-256-GCM");
    $iv = substr($c, 0, $ivlen);
    $tag = substr($c, $ivlen, $taglen=16);
    $ciphertext_raw = substr($c, $ivlen+$taglen);
    return openssl_decrypt($ciphertext_raw, 'aes-256-gcm', $encryptionKey, OPENSSL_RAW_DATA, $iv, $tag);
}

echo 'Sample AES GCM 256 string encryption' . PHP_EOL;
echo 'Please note that this code does not have any error handling and is for educational purpose only' . PHP_EOL;
echo 'Do NOT use static keys for encryption !'. PHP_EOL . PHP_EOL;

$plaintext = 'The quick brown fox jumps over the lazy dog';
$key = '12345678901234567890123456789012'; // 32 bytes = 256 bit key
echo 'plaintext: ' . $plaintext .PHP_EOL;
$encrypt = encrypt($key, $plaintext);
echo 'encrypt: ' . $encrypt . PHP_EOL;
$decrypt = decrypt($key, $encrypt);
echo 'decrypt: ' . $decrypt . PHP_EOL;
?>
Unmeet answered 23/9, 2020 at 9:16 Comment(2)
This is great, thanks! Would you mind explaining what makes this method secure and/or what makes my method un or less secure?Survivor
Kindly make your own mind in searching for "authenticated encryption modes". In short: the GCM mode generates a security tag ("HMAC") that prevents the ciphertext of changing and is so called "authenticated". The AES GCM mode is more secure than an AES CBC mode.Unmeet

© 2022 - 2024 — McMap. All rights reserved.