skip to content

PHP: Basic two-way encryption

 Tweet Share0 Tweets

While the trusty old PHP crypt function is perfect for encrypting and authenticating passwords, the hash it creates is one-way and doesn't allow for decryption.

In this article we explore the art of two-way encryption in PHP which allows us to insert encrypted values into a form which are unreadable to the browser, yet easily decipherable on the server after the form has been submitted.

Encrypting a string

For the purpose of encryption we're using the OpenSSL library, specifically the openssl_encrypt function, defined as follows:

openssl_encrypt($string, $method, $key, $options, $iv)

The parameters can be confusing, even after reading the manual, but can basically be described as:

$string
The text/data to be encrypted
$method
A cipher method chosen from openssl_get_cipher_methods()
$key
Your encryption key (reproducible, but kept private)
$options
0 || OPENSSL_RAW_DATA || OPENSSL_ZERO_PADDING
$iv
A single-use unique Random Initialization Vector (a.k.a. "IV", or "nonce")

Here is some sample code for encrypting a string:

<?PHP $token = "The quick brown fox jumps over the lazy dog."; $cipher_method = 'aes-128-ctr'; $enc_key = openssl_digest(php_uname(), 'SHA256', TRUE); $enc_iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher_method)); $crypted_token = openssl_encrypt($token, $cipher_method, $enc_key, 0, $enc_iv) . "::" . bin2hex($enc_iv); unset($token, $cipher_method, $enc_key, $enc_iv); ?> Encrypted string: KZ4LurHESC0Y8/Ufy1wsio6aaYXW7m7KVuW8NBKQhE5CnLspz+540p1ClhIZvKNx::254f830c42c937fb7e1e2444c632a8a4

Note that our output string consists of the encrypted string followed by the Initialisation Vector in hex format (the actual initialisation vector is an unreadable binary value). We've used "::" to separate the two values, but that's not necessary if you know what the IV length is going to be (more on that later).

String
KZ4LurHESC0Y8/Ufy1wsio6aaYXW7m7KVuW8NBKQhE5CnLspz+540p1ClhIZvKNx
IV (hex)
254f830c42c937fb7e1e2444c632a8a4

We include the IV with the encrypted string because it's needed in order to perform decryption and because it's random and unique each time so we can't recreate it through other means. It's similar to the salt used in one-way encryption which then forms part of the hash.

The encryption key needs to be reproducible, and secret. In the example we've just used the web server uname, but please be creative, and always keep it secure. It is required again for decryption.

The comments section for openssl_encrypt (linked under References below) contains more detailed explanations of the different options and algorithms.

Decrypting an encrypted string

As mentioned above, decryption requires that we know or can recreate the cipher method, encryption key and the initialisation vector. The only difference between the encryption and decryption function calls is that for decrypting you substitute the encrypted string as the first parameter.

We start by separating the encrypted string and IV and then use the same cipher method and key as was used during encryption:

<?PHP list($crypted_token, $enc_iv) = explode("::", $crypted_token);; $cipher_method = 'aes-128-ctr'; $enc_key = openssl_digest(php_uname(), 'SHA256', TRUE); $token = openssl_decrypt($crypted_token, $cipher_method, $enc_key, 0, hex2bin($enc_iv)); unset($crypted_token, $cipher_method, $enc_key, $enc_iv); ?> Decrypted string: The quick brown fox jumps over the lazy dog.

If everything went well the decrypted string will match the original input, which it does. If not, check that you haven't missed a binary->hex or hex->binary conversion.

If your encryption key changes between encryption and decryption - following a server upgrade which changes the uname for example - your data may be lost.

On most servers you now have in the neighbourhood of 200 encryption ciphers to choose from, and the $options value which we haven't discussed can also affect the output. There are any number of ways to generate the $key value and for passing the $iv. Please consider all options carefully.

Note that we're only using hexadecimal encoding for the encrypted string and IV because we plan to pass the encrypted string and IV as a form value. For other purposes you would leave them in binary format and use the OPENSSL_RAW_DATA option (instead of '0') when encrypting and decrypting.

Creating the Cryptor class

With some lessons learned, we can now create a simple PHP class to handle encryption and subsequent decryption:

// Original PHP code by Chirp Internet: www.chirp.com.au // Please acknowledge use of this code by including this header. namespace Chirp; class Cryptor { protected $method = 'aes-128-ctr'; // default cipher method if none supplied private $key; protected function iv_bytes() { return openssl_cipher_iv_length($this->method); } public function __construct($key = FALSE, $method = FALSE) { if(!$key) { $key = php_uname(); // default encryption key if none supplied } if(ctype_print($key)) { // convert ASCII keys to binary format $this->key = openssl_digest($key, 'SHA256', TRUE); } else { $this->key = $key; } if($method) { if(in_array(strtolower($method), openssl_get_cipher_methods())) { $this->method = $method; } else { die(__METHOD__ . ": unrecognised cipher method: {$method}"); } } } public function encrypt($data) { $iv = openssl_random_pseudo_bytes($this->iv_bytes()); return bin2hex($iv) . openssl_encrypt($data, $this->method, $this->key, 0, $iv); } // decrypt encrypted string public function decrypt($data) { $iv_strlen = 2 * $this->iv_bytes(); if(preg_match("/^(.{" . $iv_strlen . "})(.+)$/", $data, $regs)) { list(, $iv, $crypted_string) = $regs; if(ctype_xdigit($iv) && strlen($iv) % 2 == 0) { return openssl_decrypt($crypted_string, $this->method, $this->key, 0, hex2bin($iv)); } } return FALSE; // failed to decrypt } }

expand code box

Not much has changed from the previous examples, except that we're now prepending the $iv to the encrypted string and not using a separator.

We are still able to extract the IV when decrypting because we know that it will have (in hexadecimal format) two characters for each byte, with the byte length is determined by the cipher method.

Note that before decrypting we first confirm that the $iv value is a valid hexadecimal string (ctype_xdigit) and that it has an even number of characters.

Using the Cryptor class

Usage is as simple as creating an instance of the Cryptor class and passing a string to the encrypt method:

<?PHP use \Chirp\Cryptor; $token = "The quick brown fox jumps over the lazy dog."; $encryption_key = 'CKXH2U9RPY3EFD70TLS1ZG4N8WQBOVI6AMJ5'; $cryptor = new Cryptor($encryption_key); $crypted_token = $cryptor->encrypt($token); unset($token); ?>

The encrypted string can then be submitted via a hidden form field to another PHP script where the value needs to be decoded:

<?PHP use \Chirp\Cryptor; $encryption_key = 'CKXH2U9RPY3EFD70TLS1ZG4N8WQBOVI6AMJ5'; $cryptor = new Cryptor($encryption_key); $token = $cryptor->decrypt($crypted_token); ?>

As shown above, you can specify the $key for encryption, but if you leave it blank a (possibly less secure) default value will be used. Similarly, you can pass a cipher method as the second parameter to override the default:

<?PHP use \Chirp\Cryptor; // using the default encryption key and cipher method $cryptor = new Cryptor(); $token = $cryptor->decrypt($crypted_token); // using a custom key and the default cipher method $encryption_key = 'ez1LNL5UQal8YIFy'; $cryptor = new Cryptor($encryption_key); $token = $cryptor->decrypt($crypted_token); // using a custom key and specifying the cipher method $encryption_key = 'IyZG4L9yuXgDOznu'; $cipher_method = 'aes-128-cfb'; $cryptor = new Cryptor($encryption_key, $cipher_method); $token = $cryptor->decrypt($crypted_token); ?>

Whichever approach you choose, for successful decryption both the encryption key and cipher method must be identical to those used when encrypting.

Please read the StackOverflow answer linked below regarding 'Portable Data Encryption' which will tell you why the above approach is 'unsafe' for transferring critical data.

The issue is that we can't detect programatically if the encrypted string has been corrupted. This is/will be addressed in PHP 7.1 where you can pass "Additional authentication data".

Bored with all the talk - try our working example.

References

< PHP

Send a message to The Art of Web:


used only for us to reply, and to display your gravatar.

<- copy the digits from the image into this box

press <Esc> or click outside this box to close

User Comments

Post your comment or question

21 June, 2019

I'm glad I found this as Stack Overflow didn't have the right answer to my problem when I was getting error "openssl_encrypt(): Using an empty Initialization Vector (iv) is potentially insecure and not recommended". Now I was able to do this when crypt functions are deprecated.

9 September, 2018

How do I make the encrypted string url-friendly? It seems that it breaks if if has a + sign in it. Is there any way to only use "safe" charakters that I can pass in the URL?

To pass the (any) string in a URL you can just use the urlencode function.

8 August, 2018

Hi.

Just enquiring
I've used you encryption code to encrypt data just before it's written to a database.

I'd just like to ask, in another php file,
I'm looking to load from the database and decrypt, and I'm unsure how to do this?

Do I need to save any encryption keys to be used later with decryption?

Thank You.

The string resulting from (1) above is made up of the encrypted string and the IV joined with "::".

To decode you need to separate that string on "::" to get the encrypted string and IV, and then use the same key value to decrypt, as shown in (2).

But if you continue reading you will find our Cryptor class which makes everything simpler.

top