<

Keeping Secrets with PHP

Published on
12,852 Points
1,352 Views
5 Endorsements
Last Modified:
Since pre-biblical times, humans have sought ways to keep secrets, and share the secrets selectively.  This article explores the ways PHP can be used to hide and encrypt information.
Introduction
People have always sought ways to keep secrets from prying eyes or to send secret messages.  Since 499BC, when Histiaeus shaved the head of a slave and tattooed a message on his scalp, we have used steganography, encoding, and more recently, HTTPS protocol, hashing, and encryption to hide information  This article explores some of PHP's tools that help us keep our secrets.

Obfuscation
At the lowest level of secrecy, we can simply obscure the information.  The information is not really encoded differently, it's just made invisible.  Steganography ("covered writing") is an example of security by obscurity.  A message might be hidden in the metadata of a picture, for example in the IPTC information of an image.  When rendered in a browser window, the image would look like any other image, but when the IPTC information is parsed, the information can be revealed.  Another steganography technique can embed the message in the image itself, using a color that is so close to its background that it is not readily visible.  A single "signature pixel" containing this color is located at a predetermined x/y coordinate inside the image.  Using image processing software, it's possible to detect the color of this pixel and raise the brightness or change the color of all the pixels that exactly match the signature.  When the pixels are changed the message can be made visible.

This image contains a secret message.  Most of the background is RGB = #006D8B, but the message text is RGB = #016D8B.  The color difference is likely to be imperceptible to the naked eye, even on a good computer monitor.
steganography_elephant.png
When image processing software is applied to reveal the message, we get this image.
revealed_elephant.png
Here is the script that demonstrates the way to reveal the hidden message.
<?php // demo/steganography_elephant.php
/**
 * Color-Sensitive Image Steganography
 */
error_reporting(E_ALL);

// ORIGINAL AND SOON-TO-BE-REVEALED IMAGE URLS
$url = 'images/steganography_elephant.png';
$new = 'images/revealed_text_elephant.png';

// ACQUIRE THE ORIGINAL IMAGE WITH THE SECRET MESSAGE
$img = ImageCreateFromPNG($url);

// LOCATE THE COLOR OF THE SIGNAL PIXEL
$rgb = ImageColorAt($img, 0, 0);

// ALLOCATE BRIGHT YELLOW REPLACEMENT COLOR IDENTIFIER
$cid = ImageColorAllocate($img, 0xFF, 0xFF, 0x00);

// GET PIXEL COUNTS FOR WIDTH AND HEIGHT OF IMAGE
$wide = ImagesX($img);
$high = ImagesY($img);

// PIXEL POINTERS SKIP THE SIGNAL PIXEL
$nw = $nh = 1;

// ITERATE OVER ALL OF THE PIXELS
while ($nw < $wide)
{
    while ($nh < $high)
    {
        $pxl = ImageColorAt($img, $nw, $nh);

        // REPLACE THE SIGNAL PIXELS WITH THE REPLACEMENT COLOR
        if ($pxl == $rgb)
        {
            ImageSetPixel($img, $nw, $nh, $cid);
        }

        $nh++;
    }
    $nh = 0;
    $nw++;
}

// WRITE THE NEW IMAGE FILE
ImagePNG($img, $new);
ImageDestroy($img);

// SHOW THE TWO IMAGES
$original = '<img style="padding:4px" src="' . $url . '" />';
$revealed = '<img style="padding:4px" src="' . $new . '" />';
echo $original, $revealed;

Open in new window


One step up from covered writing is letter-scramble obfuscation.  PHP implements letter-scramble through the str_rot13() function.  ROT13 encoding shifts every letter by 13 places in the alphabet.  Since the English alphabet has 26 characters, the same function can be used to encode and decode strings.  Consider this code snippet and its output.

<?php // demo/rot13.php
/**
 * Scramble Letters
 *
 * http://php.net/manual/en/function.str-rot13.php
 * https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog
 */
error_reporting(E_ALL);
echo '<pre>';

// TEST DATA
$str = "The quick brown fox jumps over the lazy dog.";

// ENCODE ONCE
$rot = str_rot13($str);

// ENCODE AGAIN TO DECODE
$new = str_rot13($rot);

// SHOW THE WORK PRODUCT
echo PHP_EOL . $str;
echo PHP_EOL . $rot;
echo PHP_EOL . $new;

Open in new window

Outputs:

The quick brown fox jumps over the lazy dog.
Gur dhvpx oebja sbk whzcf bire gur ynml qbt.
The quick brown fox jumps over the lazy dog.

Open in new window


ROT13 encoding is less useful if you have non-English letters.  It does not take much effort to understand and reverse ROT13 encoding.  All it's good for is providing momentary confusion, which might be enough to discourage prying eyes.  After all, what would you think if your message said, "Abj vf gur gvzr sbe nyy tbbq zra gb pbzr gb gur nvq bs gur cnegl?"

But security by obscurity is not really good security at all.  Stronger measures are needed to protect information that really needs protection.

Hashing vs Encryption
Most of the information we want to collect and disseminate is kept in clear text, but what about information like passwords or personally identifiable information?  These are things we want to keep from prying eyes.  Hashing and encryption are different things, and the differences matter in information protection schemes.  With hashing, the process consists of making a one-way coded representation of the original information.  The hash can be used to verify the original information (or any copy of the original information) but the hash cannot be reversed to get the original information.  With encryption, there is a two-way process.  Encrypted information can be used (usually with a secret key) to recover the original information.

Hashing
A common form of hashing is accomplished with the md5() function.  This function implements the RSA message digest algorithm.  It is idempotent, meaning that for any given input, there is one and only one associated message digest.  The message digest is 256 bits long, and this means that MD5 "collisions" are possible.  A collision occurs when two different source strings produce identical message digest strings.  Fortunately collisions are pretty rare outside of the laboratory.  Here is an example of md5() and its output.

<?php // demo/md5.php
/**
 * Compute message digest
 *
 * http://php.net/manual/en/function.md5.php
 * http://www.faqs.org/rfcs/rfc1321.html
 */
error_reporting(E_ALL);
echo '<pre>';

// TEST DATA
$str = "The quick brown fox jumps over the lazy dog.";

// MAKE A HASH
$md5 = md5($str);

// SHOW THE WORK PRODUCT
echo PHP_EOL . $str;
echo PHP_EOL . $md5;

Open in new window

Outputs:

The quick brown fox jumps over the lazy dog.
e4d909c290d0fb1ca068ffaddf22cbd0

Open in new window


The Decline and Fall of MD5
One of the criticisms of MD5, entirely valid, goes to the nature of its idempotence.  In the early days of hashing, idempotence was not really a problem because the difficulty of reversing the MD5 hash was substantial, even to the point of being beyond the limits of computability.  That is no longer the case, and it's not necessary to reverse the MD5 hash.  A "dictionary" or rainbow table attack is computationally trivial today.  In a dictionary attack, every possible word or phrase is fed to the md5() function, and the corresponding hash string is stored in a database.  When an attacker seeks to reverse the MD5 hash, it only takes a database lookup to find the original word or phrase.  The MD5 hash of password is always 5f4dcc3b5aa765d61d8327deb882cf99 no matter how many times it is hashed, and an attacker who finds 5f4dcc3b5aa765d61d8327deb882cf99 in a database, knows that the original string was password.

To counteract the predictable nature of idempotence, early efforts led us to add salt strings before making the md5() hash.  This helps some, but not enough to make the hashed strings cryptographically strong.  Some additional discussion on md5() is in this article (see About Storing Passwords).  The consensus today is that secrecy of MD5 hashing has been overtaken by technology.  If we want to keep a secret hash today we use PHP password hashing techniques.

Where MD5 is Still Useful
Idempotence is not all bad, and is quite useful when we want to verify that a message has not been damaged in transmission.  We can take the md5() string of the message text and append it to the message document.  On the receiving end, the client can also take the md5() string of the message text and compare it to the md5() string at the end of the message document.  If there is a mismatch, the client can know that the message is damaged.  Try installing and running this little script.  If you change even one of the characters in the message, it will be detected.
<?php // demo/md5.php
/**
 * Compare message digest
 *
 * http://php.net/manual/en/function.md5.php
 * http://www.faqs.org/rfcs/rfc1321.html
 */
error_reporting(E_ALL);
echo '<pre>';

// TEST DATA
$str = "The quick brown fox jumps over the lazy dog.";

// MAKE A HASH
$md5 = md5($str);

// TEST THE MESSAGE
if (!empty($_POST))
{
    // TEST FOR DAMAGED FIELDS
    if ( ($str != $_POST['str']) || ($md5 != $_POST['md5']) )
    {
        echo "Message is damaged";
    }
    else echo "Message is intact";
}

// MAKE A FORM
$form = <<<EOF
<form method="post">
<input name="str" value="$str" size="48" />
<input name="md5" value="$md5" size="32" type="hidden" readonly />
<input type="submit" />
</form>
EOF;

echo $form;

Open in new window


The md5() string is useful when creating HTTP cookies.  If you want to know when a cookie is intact, you can set the cookie with both the cookie value and the salted md5() string of the cookie value.  When the browser returns the cookie, you can recreate the salted md5() string and compare it to the contents of the cookie.  A mismatch tells you to disregard the cookie.  This little code snippet shows the praxis of the anti-tamper cookie.

<?php // demo/cookie_safety.php
/**
 * Demonstrate how to encode information in a cookie in a way that
 * can reduce the risk of cookie tampering.  We do this with a
 * salted message digest that contains the contents of the cookie.
 *
 * When the cookie is returned, we use the value to recreate the
 * salted message digest.  If the new message digest does not match
 * the message digest in the cookie, we can discard the cookie.
 *
 * The cookie will be url-encoded on the browser like this:
 * MARY+HAD+A+LITTLE+LAMB%7Ccf783c37f18d007d23483b11759ec181
 * It will be url-decoded before it is presented to php.
 *
 * http://php.net/manual/en/function.setcookie.php
 * http://php.net/manual/en/function.md5.php
 */
error_reporting(E_ALL);
ob_start();
echo '<pre>';


// A DATA DELIMITER
$dlm = '|';

// YOUR OWN SECRET CODE IS A SALT FOR THE MESSAGE DIGEST STRING
$secret_code = 'MY SECRET';

// A DATA STRING THAT WE WANT TO STORE (MIGHT BE A DB KEY)
$cookie_value = 'MARY HAD A LITTLE LAMB';

// MAKE A MESSAGE DIGEST FROM THE DATA STRING TOGETHER WITH OUR SECRET CODE
$cookie_code = md5($cookie_value . $secret_code);

// CONSTRUCT THE COOKIE STRING WITH THE CLEAR TEXT AND THE CODED STRING
$safe_cookie_value = $cookie_value . $dlm . $cookie_code;

// SET THE COOKIE LIKE "MARY HAD A LITTLE LAMB|cf783c37f18d007d23483b11759ec181"
setcookie
( 'safe_cookie'         // COOKIE NAME
, $safe_cookie_value    // COOKIE VALUE
, NULL                  // COOKIE EXPIRATION
, NULL                  // COOKIE PATH
, NULL                  // COOKIE DOMAIN/SUBDOMAIN http://php.net/manual/en/function.setcookie.php#76395
, NULL                  // COOKIE SECURE (HTTPS)
, NULL                  // COOKIE HTTP-ONLY
)
;


// HOW TO TEST THE COOKIE
if (isset($_COOKIE["safe_cookie"]))
{
    // BREAK THE COOKIE VALUE APART AT THE DELIMITER
    $array = explode($dlm, $_COOKIE["safe_cookie"]);

    // ENCODE THE DATA STRING TOGETHER WITH YOUR SECRET
    $cookie_test = md5($array[0] . $secret_code);

    // IF THE MD5 CODES DO NOT MATCH, THE COOKIE IS NO LONGER INTACT
    if ($cookie_test == $array[1])
    {
        echo PHP_EOL . "THE COOKIE {$_COOKIE["safe_cookie"]} IS INTACT";
    }
    else
    {
        echo PHP_EOL . "THE COOKIE {$_COOKIE["safe_cookie"]} IS <i>CORRUPT</i>";
    }
}
else
{
    die('COOKIE IS SET - REFRESH THE BROWSER WINDOW NOW');
}


// SHOW WHAT HAPPENS WITH A CORRUPT COOKIE
$_COOKIE["safe_cookie"] = str_replace('MARY', 'FRED', $_COOKIE["safe_cookie"]);


// TEST THE COOKIE AGAIN
if (isset($_COOKIE["safe_cookie"]))
{
    // BREAK THE COOKIE VALUE APART AT THE DELIMITER
    $array = explode($dlm, $_COOKIE["safe_cookie"]);

    // ENCODE THE DATA STRING TOGETHER WITH OUT SECRET
    $cookie_test = md5($array[0] . $secret_code);

    // IF THE MD5 CODES DO NOT MATCH, THE COOKIE IS NO LONGER INTACT
    if ($cookie_test == $array[1])
    {
        echo PHP_EOL . "THE COOKIE {$_COOKIE["safe_cookie"]} IS INTACT";
    }
    else
    {
        echo PHP_EOL . "THE COOKIE {$_COOKIE["safe_cookie"]} IS <i>CORRUPT</i>";
    }
}

Open in new window



Other Hashing Algorithms
To move beyond md5() hashing, please have a look at the PHP hash_algos() function to find other hashing algorithms. Here is a little script that you can install and run to see other ways PHP can provide a hash.

<?php // demo/hash_algos.php
/**
 * Demonstrate PHP hashing algorithms
 *
 * http://php.net/manual/en/book.hash.php
 * http://php.net/manual/en/function.hash.php
 */
error_reporting(E_ALL);
echo '<pre>';

// IF THE FORM HAS BEEN SUBMITTED, HASH THE REQUEST VARIABLE
if (!empty($_GET['q']))
{
    $qget = substr($_GET['q'], 0, 10);
    $qpad = str_pad($qget, 10, ' ', STR_PAD_RIGHT);
    $algos = hash_algos();
    echo PHP_EOL . '                                    1...5...10...15...20...25...30...35...40...45...50...55...60...65...70...75...80...85...90...95..100..105..110..115..120..125..130';
    echo PHP_EOL . '                                    |        |         |         |         |         |         |         |         |         |         |         |         |         |';
    foreach ($algos as $algo)
    {
        $hash = hash($algo, $qget);
        $algo_p = str_pad($algo . ':' , 12, ' ', STR_PAD_RIGHT);
        echo PHP_EOL . $qpad . " hashed with $algo_p " . $hash;
    }
}

// PUT UP THE FORM TO RECEIVE THE INPUT STRING
$form = <<<EOF
<form>
String to hash (up to 10 characters): <input name="q" autocomplete="off" />
<input type="submit" />
</form>
EOF;

echo $form;

Open in new window



Encryption
Encryption can encode information in ways that make it possible to reverse the process and recover the original clear-text string.  PHP has several built-in encryption functions.  These provide different levels of cryptographic strength.  The rest of this article will show different experiments in encryption and will highlight the strengths and weaknesses of each design. 

With a little forethought we can design the interface for our encryption classes.  There are two important things that we need to do: encrypt and decrypt.

<?php
/**
 * The Interface defines the two main data transformation activities
 */
Interface Encryption_Interface
{
    public function encrypt($text, $key);
    public function decrypt($text, $key);
}

Open in new window


Encryption with MCrypt
Here is a simple example of reversible encryption using MCrypt.  You can copy this code and install it on your own server to see how it works.  You will see how we implemented the Encryption_Interface.  Note that any encryption algorithm, by itself, may produce binary values that are unsuitable for transport across binary-sensitive boundaries.  For this reason, we use base64_encode() and base64_decode() to produce a binary-safe string.

This is useful as a teaching example, but don't use this in a deployed application.  It has some flaws, most notably, idempotence.  An encryption process that always turns out the same encryption values is subject to rainbow attacks, just like a hashing process that exhibits idempotence.  If we encrypt password with an empty key, we always get 9xp0gHiq+GhGyXQ+1/Y4XuR0RIE/QJ9ZSrZqBLMyzy0=.  If we encrypt password with secret for the key, we always get  PEl/zm3PAwwjrg+Sc8xfELuWin3/Yd8cbtRBnnip/20=.  This kind of predictability is a disadvantage in cryptographic science.

<?php // demo/encrypt_decrypt_mcrypt.php
/**
 * Show how to encrypt and decrypt information
 * with binary-safe transport over the internet
 * Note: ECB may not be the "best" mode, YMMV
 *
 * http://php.net/manual/en/book.mcrypt.php
 * http://php.net/manual/en/ref.mcrypt.php
 * http://php.net/manual/en/mcrypt.ciphers.php
 *
 * https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
 * https://en.wikipedia.org/wiki/Base64
 *
 * Parallel construction in the encrypt() decrypt() methods
 */
error_reporting(E_ALL);


/**
 * The Interface defines the two main data transformation activities
 */
Interface Encryption_Interface
{
    public function encrypt($text, $key);
    public function decrypt($text, $key);
}


class Mcrypt Implements Encryption_Interface
{
    protected $cipher;
    protected $mode;

    public function __construct($cipher = MCRYPT_RIJNDAEL_256, $mode = MCRYPT_MODE_ECB)
    {
        $this->cipher = $cipher;
        $this->mode   = $mode;
    }

    public function encrypt($text, $key)
    {
        $data = mcrypt_encrypt($this->cipher, $key, $text, $this->mode);
        return base64_encode($data);
    }

    public function decrypt($text, $key)
    {
        $text = base64_decode($text);
        $data = mcrypt_decrypt($this->cipher, $key, $text, $this->mode);
        return $data;
    }
}


// INSTANTIATE AN ENCRYPTION OBJECT FROM THE CLASS
$m = new Mcrypt;

// INITIALIZE VARS FOR LATER USE IN THE HTML FORM
$encoded = $decoded = NULL;
$secret  = !empty($_POST['secret']) ? $_POST['secret'] : '';

// IF ANYTHING WAS POSTED SHOW THE DATA
if (!empty($_POST["clearstring"]))
{
    $encoded = $m->encrypt($_POST["clearstring"], $secret);
    echo "<br/>{$_POST["clearstring"]} YIELDS ENCODED ";
    var_dump($encoded);
}

if (!empty($_POST["cryptstring"]))
{
    $decoded = $m->decrypt($_POST["cryptstring"], $secret);
    echo "<br/>{$_POST["cryptstring"]} YIELDS DECODED ";
    var_dump($decoded);
}


// CREATE THE FORM USING HEREDOC NOTATION
$form = <<<FORM
<form method="post">
Secret Key:<input name="secret" value="$secret" autocomplete="off" />
<br>
<textarea name="clearstring">$decoded</textarea>
<input type="submit" value="ENCRYPT" />
<br>
<textarea name="cryptstring">$encoded</textarea>
<input type="submit" value="DECRYPT" />
</form>
FORM;

echo $form;

Open in new window


Encryption with MCrypt (Improved)
This example uses MCrypt, and improves on the earlier example by using an initialization vector ("IV").  The IV is a starting variable that is used to provide additional entropy in the output.  A random IV will cause the encrypted values to be different every time, effectively eliminating idempotence and making it more difficult for an attacker to infer relationships between encrypted messages.  Rainbow attacks are much less useful when there is an IV in play.

PHP gives us two functions that make IV creation easy.  mcrypt_get_iv_size() will tell us the byte size of the IV string belonging to our cipher and mode combination.  mcrypt_create_iv() will return an initialization vector (IV) from a random source.  The man page for MCrypt_Create_IV() has some links that provide a "deep dive" into theory and practice of cryptology.  If you're interested in learning more about how this works, it makes for great reading.  Here is the code sample using the IV.

<?php // demo/encrypt_decrypt_mcrypt_iv.php
/**
 * Show how to encrypt and decrypt information
 * with binary-safe transport over the internet
 * Note: ECB may not be the "best" mode, YMMV
 *
 * http://php.net/manual/en/book.mcrypt.php
 * http://php.net/manual/en/ref.mcrypt.php
 * http://php.net/manual/en/mcrypt.ciphers.php
 *
 * https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
 * https://en.wikipedia.org/wiki/Base64
 *
 * Parallel construction in the encrypt() decrypt() methods
 */
error_reporting(E_ALL);


/**
 * The Interface defines the two main data transformation activities
 */
Interface Encryption_Interface
{
    public function encrypt($text, $key);
    public function decrypt($text, $key);
}


class Mcrypt Implements Encryption_Interface
{
    protected $cipher;
    protected $mode;
    protected $ivsize;
    protected $vector;

    public function __construct($cipher = MCRYPT_RIJNDAEL_256, $mode = MCRYPT_MODE_ECB)
    {
        $this->cipher = $cipher;
        $this->mode   = $mode;
        $this->ivsize = mcrypt_get_iv_size($cipher, $mode);
        var_dump($this->ivsize);
        $this->vector = mcrypt_create_iv($this->ivsize, MCRYPT_RAND);
    }

    public function encrypt($text, $key)
    {
        $data = mcrypt_encrypt($this->cipher, $key, $text, $this->mode, $this->vector);
        return base64_encode($this->vector . $data);
    }

    public function decrypt($text, $key)
    {
        $text = base64_decode($text);
        // SEPARATE THE IV AND THE ENCRYPTED DATA
        $vect = substr($text, 0, $this->ivsize);
        $text = substr($text, $this->ivsize);
        $data = mcrypt_decrypt($this->cipher, $key, $text, $this->mode, $this->vector);
        return $data;
    }
}


// INSTANTIATE AN ENCRYPTION OBJECT FROM THE CLASS
$m = new Mcrypt;

// INITIALIZE VARS FOR LATER USE IN THE HTML FORM
$encoded = $decoded = NULL;
$secret  = !empty($_POST['secret']) ? $_POST['secret'] : '';

// IF ANYTHING WAS POSTED SHOW THE DATA
if (!empty($_POST["clearstring"]))
{
    $encoded = $m->encrypt($_POST["clearstring"], $secret);
    echo "<br/>{$_POST["clearstring"]} YIELDS ENCODED ";
    var_dump($encoded);
}

if (!empty($_POST["cryptstring"]))
{
    $decoded = $m->decrypt($_POST["cryptstring"], $secret);
    echo "<br/>{$_POST["cryptstring"]} YIELDS DECODED ";
    var_dump($decoded);
}


// CREATE THE FORM USING HEREDOC NOTATION
$form = <<<FORM
<form method="post">
Secret Key:<input name="secret" value="$secret" autocomplete="off" />
<br>
<textarea name="clearstring">$decoded</textarea>
<input type="submit" value="ENCRYPT" />
<br>
<textarea name="cryptstring">$encoded</textarea>
<input type="submit" value="DECRYPT" />
</form>
FORM;

echo $form;

Open in new window


If we encrypt password (even with an empty key), we get a different value every time.  As you can see we have much stronger encryption, and it is much harder to discern patterns with the naked eye or with a database of encrypted strings.  Here are my first few tries.

1...5...10...15...20...25...30...35...40...45...50...55...60...65...70...75...80...85...
|                              |
| Initialization Vector        | Encrypted Data
|  32 bytes in this example    |  followed by encrypted padding
|                              |
199zZRmtQ2LYxDAc1NOOVwuH/upttDL6wCH7XHsB2HL3GnSAeKr4aEbJdD7X9jhe5HREgT9An1lKtmoEszLPLQ==
ZJ6eXQbXWipyod5vCXGPXcC0gMWzbud6vSeVIKmR0Qb3GnSAeKr4aEbJdD7X9jhe5HREgT9An1lKtmoEszLPLQ==
lSAukpbvItlgzp6TxHSsbZH18EZIRgBdgKYIGI4TjAL3GnSAeKr4aEbJdD7X9jhe5HREgT9An1lKtmoEszLPLQ==

Open in new window


But this is not cryptographically "good enough" any more.  See how the padding characters are replicated?  This means the encryption scheme can be defeated through a Padding Oracle attack.  So it's back to the drawing board, in search of a more secure encryption process.

Encryption with OpenSSL
The current state of the art in PHP encryption is implemented by the OpenSSL library.  OpenSSL has many features that are not implemented (yet) in PHP, and presently has over 50 functions.  Fortunately we can get good encryption results with only a small subset of the functions!

Like our improved MCrypt example, OpenSSL uses an initialization vector to increase the entropy of the encrypted string.  And because this is a more up-to-date example, we use Exception to trap and report any errors.
<?php // demo/encrypt_decrypt_openssl.php
/**
 * Show how to encrypt and decrypt information using OpenSSL
 * with binary-safe (base64) transport over the internet
 *
 * http://php.net/manual/en/book.openssl.php
 *
 * https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
 * https://en.wikipedia.org/wiki/Base64
 *
 * https://bugs.php.net/bug.php?id=67304
 *
 * Parallel construction in the encrypt() decrypt() methods
 */
error_reporting(E_ALL);


/**
 * The Interface defines the two main data transformation activities
 */
Interface Encryption_Interface
{
    public function encrypt($text, $key);
    public function decrypt($text, $key);
}


class SSLCrypt Implements Encryption_Interface
{
    protected $cipher_algorithm;
    protected $digest_method;
    protected $ivector_length;

    public function __construct($cipher_algorithm = 'aes-256-ctr', $digest_method = 'sha256')
    {
        $this->cipher_algorithm = $cipher_algorithm;
        $this->digest_method = $digest_method;

        if (!in_array($cipher_algorithm, openssl_get_cipher_methods(TRUE))) {
            throw new \Exception(__METHOD__ . " Unknown cipher $cipher_algorithm");
        }

        if (!in_array($digest_method, openssl_get_md_methods(TRUE))) {
            throw new \Exception(__METHOD__ . " Unknown digest $digest_method");
        }

        $this->ivector_length = openssl_cipher_iv_length($cipher_algorithm);
    }

    public function encrypt($text, $key)
    {
        $keyhash = openssl_digest($key, $this->digest_method, TRUE);
        $ivector = mcrypt_create_iv($this->ivector_length, MCRYPT_DEV_URANDOM);
        $crypted = openssl_encrypt($text, $this->cipher_algorithm, $keyhash, OPENSSL_RAW_DATA, $ivector);
        if ($crypted === FALSE) {
            throw new \Exception(__METHOD__ . ' Failed: ' . openssl_error_string());
        }

        // RETURN THE IV AND THE ENCRYPTED DATA
        return base64_encode($ivector . $crypted);
    }

    public function decrypt($text, $key)
    {
        $keyhash = openssl_digest($key, $this->digest_method, TRUE);
        $rawdata = base64_decode($text);
        if (strlen($rawdata) < $this->ivector_length) {
            throw new \Exception(__METHOD__ . ' Data is too short');
        }

        // SEPARATE THE IV AND THE ENCRYPTED DATA
        $ivector = substr($rawdata, 0, $this->ivector_length);
        $rawtext = substr($rawdata, $this->ivector_length);
        $decrypt = openssl_decrypt($rawtext, $this->cipher_algorithm, $keyhash, OPENSSL_RAW_DATA, $ivector);

        if ($decrypt === FALSE) {
            throw new \Exception(__METHOD__ . ' Failed: ' . openssl_error_string());
        }

        return $decrypt;
    }
}


// INSTANTIATE ENCRYPTION OBJECTS FROM THE CLASS
$s = new SSLCrypt;

// INITIALIZE VARS FOR LATER USE IN THE HTML FORM
$s_encoded = $s_decoded = NULL;
$s_secret  = !empty($_POST['s_secret']) ? $_POST['s_secret'] : NULL;

// IF ANYTHING WAS POSTED SHOW THE DATA
if (!empty($_POST))
{
    if (!empty($_POST["s_clearstring"]))
    {
        $s_encoded = $s->encrypt($_POST["s_clearstring"], $_POST["s_secret"]);
        echo "<br/>{$_POST["s_clearstring"]} YIELDS OpenSSL ENCODED ";
        var_dump($s_encoded);
    }

    if (!empty($_POST["s_cryptstring"]))
    {
        $s_decoded = $s->decrypt($_POST["s_cryptstring"], $_POST["s_secret"]);
        echo "<br/>{$_POST["s_cryptstring"]} YIELDS OpenSSL DECODED ";
        var_dump($s_decoded);
    }
}

// CREATE THE FORM USING HEREDOC NOTATION
$form = <<<FORM
<form method="post">
<h3>Using OpenSSL</h3>
<input type="hidden" name="choice" value="ssl" />
Secret Password (Key):
<br>
<input name="s_secret" value="$s_secret" autocomplete="off" />
<br>
<textarea name="s_clearstring">$s_decoded</textarea>
<input type="submit" value="ENCRYPT OpenSSL" />
<br>
<textarea name="s_cryptstring">$s_encoded</textarea>
<input type="submit" value="DECRYPT OpenSSL" />
<br>
</form>
FORM;

echo $form;

Open in new window



Our initialization vector is shorter (16 bytes) and so is the encrypted data.  Here are the first few encryption tries using an input string of password with an empty key in OpenSSL.

1...5...10...15...20...25...30..
|               |
| Initial'n     | Encrypted
|  Vector       |  Data
|  16 bytes     |
|               | 
0RmMD1ev5MVBXxKj2Dcfs8ciSXgDPgyx
j+odB29EPsb3i9TasNHXL28jiHN5zBSq
+2aiANr9CA5TJL2Y82zmIK/i0N+cFV3h

Open in new window


Encryption with OpenSSL and Message Authentication
As good as the OpenSSL example might be, it can be improved.  The next code snippet shows a multi-faceted approach to encryption and authentication.  In this design we transmit three data values: The IV, the encrypted data, and the Message Authentication Code ("MAC").  Because the base64() alphabet does not contain the pipe character, we can use the pipe as a delimiter to separate components of the message.  Here is the code that generates an encrypted message under the Encrypt-then-Authenticate strategy.  This strategy means that the first operation on the receiving end can be authentication, and any message that fails authentication can be discarded - it does not have to be decrypted at all.

<?php // demo/encrypt_decrypt_openssl_auth.php
/**
 * Show how to encrypt and decrypt information
 * with Encrypt-then-Authenticate design
 * with binary-safe (base64) transport over the internet
 *
 * http://php.net/manual/en/book.openssl.php
 *
 * https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
 * https://en.wikipedia.org/wiki/Base64
 * https://en.wikipedia.org/wiki/Authenticated_encryption
 *
 * https://bugs.php.net/bug.php?id=67304
 *
 * Parallel construction in the encrypt() decrypt() methods
 */
error_reporting(E_ALL);


/**
 * The Interface defines the two main data transformation activities
 */
Interface Encryption_Interface
{
    public function encrypt($text, $key);
    public function decrypt($text, $key);
}


class SSLCrypt Implements Encryption_Interface
{
    protected $cipher_algorithm;
    protected $digest_method;
    protected $vector_length;

    public function __construct($cipher_algorithm = 'aes-256-ctr', $digest_method = 'sha256')
    {
        $this->cipher_algorithm = $cipher_algorithm;
        $this->digest_method = $digest_method;

        if (!in_array($cipher_algorithm, openssl_get_cipher_methods(TRUE))) {
            throw new \Exception(__METHOD__ . " Unknown cipher $cipher_algorithm");
        }

        if (!in_array($digest_method, openssl_get_md_methods(TRUE))) {
            throw new \Exception(__METHOD__ . " Unknown digest $digest_method");
        }

        $this->ivector_length = openssl_cipher_iv_length($cipher_algorithm);
    }

    public function encrypt($text, $key)
    {
        $keyhash = openssl_digest($key, $this->digest_method, TRUE);

        $ivector = mcrypt_create_iv($this->ivector_length, MCRYPT_DEV_URANDOM);
        $crypted = openssl_encrypt($text, $this->cipher_algorithm, $keyhash, OPENSSL_RAW_DATA, $ivector);
        if ($crypted === FALSE) {
            throw new \Exception(__METHOD__ . ' Failed: ' . openssl_error_string());
        }

        // RETURN THE AUTH DIGEST, IV, AND THE ENCRYPTED DATA
        $payload = base64_encode($ivector . $crypted);
        var_dump($payload);
        $digest  = openssl_digest($key . $payload, $this->digest_method);
        var_dump($digest);
        return $digest . '|' . $payload;
    }

    public function decrypt($text, $key)
    {
        $keyhash = openssl_digest($key, $this->digest_method, TRUE);

        // SEPARATE AND TEST THE AUTH DIGEST
        $inputs  = explode('|', $text);
        $digest  = $inputs[0];
        $compare = openssl_digest($key . $inputs[1], $this->digest_method);
        if ($compare != $digest){
            throw new \Exception(__METHOD__ . ' Authentication digest failure');
        }
        $rawdata = base64_decode($inputs[1]);

        // SEPARATE THE IV AND THE ENCRYPTED DATA
        $ivector = substr($rawdata, 0, $this->ivector_length);
        $rawtext = substr($rawdata, $this->ivector_length);
        $decrypt = openssl_decrypt($rawtext, $this->cipher_algorithm, $keyhash, OPENSSL_RAW_DATA, $ivector);

        if ($decrypt === FALSE) {
            throw new \Exception(__METHOD__ . ' Failed: ' . openssl_error_string());
        }

        return $decrypt;
    }
}


// INSTANTIATE ENCRYPTION OBJECTS FROM THE CLASS
$s = new SSLCrypt;

// INITIALIZE VARS FOR LATER USE IN THE HTML FORM
$s_encoded = $s_decoded = NULL;
$s_secret  = !empty($_POST['s_secret']) ? $_POST['s_secret'] : NULL;

// IF ANYTHING WAS POSTED SHOW THE DATA
if (!empty($_POST))
{
    if (!empty($_POST["s_clearstring"]))
    {
        $s_encoded = $s->encrypt($_POST["s_clearstring"], $_POST["s_secret"]);
        echo "<br/>{$_POST["s_clearstring"]} YIELDS OpenSSL ENCODED ";
        var_dump($s_encoded);
    }

    if (!empty($_POST["s_cryptstring"]))
    {
        $s_decoded = $s->decrypt($_POST["s_cryptstring"], $_POST["s_secret"]);
        echo "<br/>{$_POST["s_cryptstring"]} YIELDS OpenSSL DECODED ";
        var_dump($s_decoded);
    }
}
// CREATE THE FORM USING HEREDOC NOTATION
$form = <<<FORM

<form method="post">
<h3>Using OpenSSL and MAC</h3>
Secret Password (Key):
<br>
<input name="s_secret" value="$s_secret" autocomplete="off" />
<br>
<textarea name="s_clearstring">$s_decoded</textarea>
<input type="submit" value="ENCRYPT OpenSSL" />
<br>
<textarea name="s_cryptstring">$s_encoded</textarea>
<input type="submit" value="DECRYPT OpenSSL" />
<br>
</form>

FORM;

echo $form;

Open in new window



Here are the first few tries, showing the MAC, followed by the IV and encrypted message.
1...5...10...15...20...25...30...35...40...45...50...55...60...65...70...75...80...85...90...95..
|                                                                |
| Message Authentication Code                                    | Payload  
|  Digest of Key and Data                                        |              |
|                                                                | Initial'n    | Encrypted
|                                                                |  Vector      |  Data
|                                                                |              |
16565deb7c4b4c6c13aa42c9653c434b0c5a6247977d5d71dc40b890f125e7c3|egl00WlazSOcDsxDV5iBhMnXpjY74aCF
cddf5078c7ddef45666b45e0a7ee0f16e936ebf8a43cc2d327e99e26740cb0ae|uQOtWB7hhq8Ob1WEbVgEaJAr23aU52bN
aca329b53db224c44d640b75ecfd237c00e6c827945273b5f6ea22f6e14c1d5c|DRFAtnQqIxnnYyMvI4kd5Qb0yvu1TXSO
                                                                |
                                                                | Pipe Delimiter

Open in new window


Encryption with OpenSSL, Message Authentication, and Origin Verification
Any encryption scheme helps secure our messages and keep them away from prying eyes.  And (of course) we only make our RESTful API communications over HTTPS.  But what if these techniques break down, and we receive a message that is encoded with our expected algorithm, uses our secret key, but comes from an attacker?  What are the chances of this happening?  Very small, you would say, and I would agree.  But there is a slightly greater-than-zero possibility, however small.  Disgruntled ex-employees may have access to the secrets of our communication strategy.  The only way to protect against such an attack is with origin verification.

In the origin verification design, we make use of the MAC.  On the originating server, we make a database record of each encrypted message by storing the MAC in a database table.  On the receiving server, we extract the MAC from the message and make a POST-method request back to the originating server, sending the MAC.  The originating server uses the MAC that we posted to make a database lookup, and if the MAC is found, it deletes the MAC from its table and sends a 200 OK response with the word, "Verified" in the body of the response.  Upon receiving the response with "Verified" the receiving server knows that the message origin has been verified.  Now the receiver can proceed with MAC verification and decryption, secure in the knowledge that the message came from a trusted source.  This simple and elegant "handshake" has been used for PayPal communications since the 1990's and it still works well today.

Summary
This article has shown us some of the history of information hiding technologies, and some of the PHP best practices for secure communications today.  The article was written in 2016, and technology is always advancing.  So it might be good to check out the references if you're thinking of using these code snippets in a deployed application.

References and Further Reading
https://www.owasp.org/index.php/Main_Page
https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
https://en.wikipedia.org/wiki/Authenticated_encryption
https://en.wikipedia.org/wiki/Galois/Counter_Mode
https://en.wikipedia.org/wiki/Base64
https://secure.php.net/manual/en/book.openssl.php
https://bugs.php.net/bug.php?id=67304
https://moxie.org/blog/the-cryptographic-doom-principle/

Please give us your feedback!
If you found this article helpful, please click the "thumb's up" button below. Doing so lets the E-E community know what is valuable for E-E members and helps provide direction for future articles.  If you have questions or comments, please add them.  Thanks!

 
5
Comment
Author:Ray Paseur
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
1 Comment
 
LVL 6

Expert Comment

by:mmarth
can a file be encrypted with OpenSSL as it is being streamed in so it is not first saved in plaintext form
0

Featured Post

[Webinar] Lessons on Recovering from Petya

Skyport is working hard to help customers recover from recent attacks, like the Petya worm. This work has brought to light some important lessons. New malware attacks like this can take down your entire environment. Learn from others mistakes on how to prevent Petya like worms.

Join & Write a Comment

The viewer will learn how to count occurrences of each item in an array.
The viewer will learn how to create and use a small PHP class to apply a watermark to an image. This video shows the viewer the setup for the PHP watermark as well as important coding language. Continue to Part 2 to learn the core code used in creat…
Suggested Courses

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month