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:
- https://bhoover.com/using-php-openssl_encrypt-openssl_decrypt-encrypt-decrypt-data/
- 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:
- 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 staticiv
defined well ahead of time and used for every encryption? If not, what am I missing? - what openings for (potentially successful) attack so any/all of these approaches expose? How can they be fixed or modified to mitigate the risk
- 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.