PHP AES encrypt / decrypt

PhpEncryptionCryptographyAesEncryption Symmetric

Php Problem Overview


I found an example for en/decoding strings in PHP. At first it looks very good but it wont work :-(

Does anyone know what the problem is?

$Pass = "Passwort";
$Clear = "Klartext";

$crypted = fnEncrypt($Clear, $Pass);
echo "Encrypted: ".$crypted."</br>";

$newClear = fnDecrypt($crypted, $Pass);
echo "Decrypted: ".$newClear."</br>";

function fnEncrypt($sValue, $sSecretKey) {
	return trim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $sSecretKey, $sDecrypted, MCRYPT_MODE_ECB, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND))));
}

function fnDecrypt($sValue, $sSecretKey) {
	return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $sSecretKey, base64_decode($sEncrypted), MCRYPT_MODE_ECB, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND)));
}

The result is:

Encrypted: boKRNTYYNp7AiOvY1CidqsAn9wX4ufz/D9XrpjAOPk8=

Decrypted: —‚(ÑÁ ^ yË~F'¸®Ó–í œð2Á_B‰Â—

Php Solutions


Solution 1 - Php

Please use an existing secure PHP encryption library

It's generally a bad idea to write your own cryptography unless you have experience breaking other peoples' cryptography implementations.

None of the examples here authenticate the ciphertext, which leaves them vulnerable to bit-rewriting attacks.

If you can install PECL extensions, libsodium is even better

<?php
// PECL libsodium 0.2.1 and newer

/**
 * Encrypt a message
 * 
 * @param string $message - message to encrypt
 * @param string $key - encryption key
 * @return string
 */
function safeEncrypt($message, $key)
{
    $nonce = \Sodium\randombytes_buf(
        \Sodium\CRYPTO_SECRETBOX_NONCEBYTES
    );
    
    return base64_encode(
        $nonce.
        \Sodium\crypto_secretbox(
            $message,
            $nonce,
            $key
        )
    );
}

/**
 * Decrypt a message
 * 
 * @param string $encrypted - message encrypted with safeEncrypt()
 * @param string $key - encryption key
 * @return string
 */
function safeDecrypt($encrypted, $key)
{   
    $decoded = base64_decode($encrypted);
    $nonce = mb_substr($decoded, 0, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
    $ciphertext = mb_substr($decoded, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
    
    return \Sodium\crypto_secretbox_open(
        $ciphertext,
        $nonce,
        $key
    );
}    

Then to test it out:

<?php
// This refers to the previous code block.
require "safeCrypto.php"; 

// Do this once then store it somehow:
$key = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_KEYBYTES);
$message = 'We are all living in a yellow submarine';

$ciphertext = safeEncrypt($message, $key);
$plaintext = safeDecrypt($ciphertext, $key);

var_dump($ciphertext);
var_dump($plaintext);

This can be used in any situation where you are passing data to the client (e.g. encrypted cookies for sessions without server-side storage, encrypted URL parameters, etc.) with a reasonably high degree of certainty that the end user cannot decipher or reliably tamper with it.

Since libsodium is cross-platform, this also makes it easier to communicate with PHP from, e.g. Java applets or native mobile apps.


Note: If you specifically need to add encrypted cookies powered by libsodium to your app, my employer Paragon Initiative Enterprises is developing a library called Halite that does all of this for you.

Solution 2 - Php

If you don't want to use a heavy dependency for something solvable in 15 lines of code, use the built in OpenSSL functions. Most PHP installations come with OpenSSL, which provides fast, compatible and secure AES encryption in PHP. Well, it's secure as long as you're following the best practices.

The following code:

  • uses AES256 in CBC mode
  • is compatible with other AES implementations, but not mcrypt, since mcrypt uses PKCS#5 instead of PKCS#7.
  • generates a key from the provided password using SHA256
  • generates a hmac hash of the encrypted data for integrity check
  • generates a random IV for each message
  • prepends the IV (16 bytes) and the hash (32 bytes) to the ciphertext
  • should be pretty secure

IV is a public information and needs to be random for each message. The hash ensures that the data hasn't been tampered with.

function encrypt($plaintext, $password) {
    $method = "AES-256-CBC";
    $key = hash('sha256', $password, true);
    $iv = openssl_random_pseudo_bytes(16);

    $ciphertext = openssl_encrypt($plaintext, $method, $key, OPENSSL_RAW_DATA, $iv);
    $hash = hash_hmac('sha256', $ciphertext . $iv, $key, true);

    return $iv . $hash . $ciphertext;
}

function decrypt($ivHashCiphertext, $password) {
    $method = "AES-256-CBC";
    $iv = substr($ivHashCiphertext, 0, 16);
    $hash = substr($ivHashCiphertext, 16, 32);
    $ciphertext = substr($ivHashCiphertext, 48);
    $key = hash('sha256', $password, true);

    if (!hash_equals(hash_hmac('sha256', $ciphertext . $iv, $key, true), $hash)) return null;

    return openssl_decrypt($ciphertext, $method, $key, OPENSSL_RAW_DATA, $iv);
}

Usage:

$encrypted = encrypt('Plaintext string.', 'password'); // this yields a binary string

echo decrypt($encrypted, 'password');
// decrypt($encrypted, 'wrong password') === null

edit: Updated to use hash_equals and added IV to the hash.

Solution 3 - Php

$sDecrypted and $sEncrypted were undefined in your code. See a solution that works (but is not secure!):


STOP!

> This example is insecure! Do not use it!


$Pass = "Passwort";
$Clear = "Klartext";        
    
$crypted = fnEncrypt($Clear, $Pass);
echo "Encrypred: ".$crypted."</br>";
    
$newClear = fnDecrypt($crypted, $Pass);
echo "Decrypred: ".$newClear."</br>";        
    
function fnEncrypt($sValue, $sSecretKey)
{
    return rtrim(
    	base64_encode(
    		mcrypt_encrypt(
    			MCRYPT_RIJNDAEL_256,
    			$sSecretKey, $sValue, 
    			MCRYPT_MODE_ECB, 
    			mcrypt_create_iv(
    				mcrypt_get_iv_size(
    					MCRYPT_RIJNDAEL_256, 
    					MCRYPT_MODE_ECB
    				), 
    				MCRYPT_RAND)
    			)
    		), "\0"
    	);
}
    
function fnDecrypt($sValue, $sSecretKey)
{
    return rtrim(
    	mcrypt_decrypt(
    		MCRYPT_RIJNDAEL_256, 
    		$sSecretKey, 
    		base64_decode($sValue), 
    		MCRYPT_MODE_ECB,
    		mcrypt_create_iv(
    			mcrypt_get_iv_size(
    				MCRYPT_RIJNDAEL_256,
    				MCRYPT_MODE_ECB
    			), 
    			MCRYPT_RAND
    		)
    	), "\0"
    );
}

But there are other problems in this code which make it insecure, in particular the use of ECB (which is not an encryption mode, only a building block on top of which encryption modes can be defined). See Fab Sa's answer for a quick fix of the worst problems and Scott's answer for how to do this right.

Solution 4 - Php

For information MCRYPT_MODE_ECB doesn't use the IV (initialization vector). ECB mode divide your message into blocks and each block is encrypted separately. I really don't recommended it.

CBC mode use the IV to make each message unique. CBC is recommended and should be used instead of ECB.

Example :

<?php
$password = "myPassword_!";
$messageClear = "Secret message";

// 32 byte binary blob
$aes256Key = hash("SHA256", $password, true);

// for good entropy (for MCRYPT_RAND)
srand((double) microtime() * 1000000);
// generate random iv
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC), MCRYPT_RAND);


$crypted = fnEncrypt($messageClear, $aes256Key);

$newClear = fnDecrypt($crypted, $aes256Key);

echo
"IV: 		<code>".$iv."</code><br/>".
"Encrypred: <code>".$crypted."</code><br/>".
"Decrypred: <code>".$newClear."</code><br/>";

function fnEncrypt($sValue, $sSecretKey) {
	global $iv;
	return rtrim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $sSecretKey, $sValue, MCRYPT_MODE_CBC, $iv)), "\0\3");
}

function fnDecrypt($sValue, $sSecretKey) {
	global $iv;
	return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $sSecretKey, base64_decode($sValue), MCRYPT_MODE_CBC, $iv), "\0\3");
}

You have to stock the IV to decode each message (IV are not secret). Each message is unique because each message has an unique IV.

Solution 5 - Php

This is a working solution of AES encryption - implemented using openssl. It uses the Cipher Block Chaining Mode (CBC-Mode). Thus, alongside data and key, you can specify iv and block size

 <?php
      class AESEncryption {
           
            protected $key;
            protected $data;
            protected $method;
            protected $iv;
        
            /**
             * Available OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
             *
             * @var type $options
             */
            protected $options = 0;
        
            /**
             * 
             * @param type $data
             * @param type $key
             * @param type $iv
             * @param type $blockSize
             * @param type $mode
             */
            public function __construct($data = null, $key = null, $iv = null, $blockSize = null, $mode = 'CBC') {
                $this->setData($data);
                $this->setKey($key);
                $this->setInitializationVector($iv);
                $this->setMethod($blockSize, $mode);
            }
        
            /**
             * 
             * @param type $data
             */
            public function setData($data) {
                $this->data = $data;
            }
        
            /**
             * 
             * @param type $key
             */
            public function setKey($key) {
                $this->key = $key;
            }
        
            /**
             * CBC 128 192 256 
              CBC-HMAC-SHA1 128 256
              CBC-HMAC-SHA256 128 256
              CFB 128 192 256
              CFB1 128 192 256
              CFB8 128 192 256
              CTR 128 192 256
              ECB 128 192 256
              OFB 128 192 256
              XTS 128 256
             * @param type $blockSize
             * @param type $mode
             */
            public function setMethod($blockSize, $mode = 'CBC') {
                if($blockSize==192 && in_array('', array('CBC-HMAC-SHA1','CBC-HMAC-SHA256','XTS'))){
                    $this->method=null;
                    throw new Exception('Invalid block size and mode combination!');
                }
                $this->method = 'AES-' . $blockSize . '-' . $mode;
            }
        
            /**
             * 
             * @param type $data
             */
            public function setInitializationVector($iv) {
                $this->iv = $iv;
            }
        
            /**
             * 
             * @return boolean
             */
            public function validateParams() {
                if ($this->data != null &&
                        $this->method != null ) {
                    return true;
                } else {
                    return FALSE;
                }
            }
            
            //it must be the same when you encrypt and decrypt
            protected function getIV() { 
                return $this->iv;
            }
        
             /**
             * @return type
             * @throws Exception
             */
            public function encrypt() {
                if ($this->validateParams()) { 
                    return trim(openssl_encrypt($this->data, $this->method, $this->key, $this->options,$this->getIV()));
                } else {
                    throw new Exception('Invalid params!');
                }
            }
            
            /**
             * 
             * @return type
             * @throws Exception
             */
            public function decrypt() {
                if ($this->validateParams()) {
                   $ret=openssl_decrypt($this->data, $this->method, $this->key, $this->options,$this->getIV());
                  
                   return   trim($ret); 
                } else {
                    throw new Exception('Invalid params!');
                }
            }
        
        }

Sample usage:

<?php
        $data = json_encode(['first_name'=>'Dunsin','last_name'=>'Olubobokun','country'=>'Nigeria']);
        $inputKey = "W92ZB837943A711B98D35E799DFE3Z18";
        $iv = "tuqZQhKP48e8Piuc";
        $blockSize = 256;
        $aes = new AESEncryption($data, $inputKey, $iv, $blockSize);
        $enc = $aes->encrypt();
        $aes->setData($enc);
        $dec=$aes->decrypt();
        echo "After encryption: ".$enc."<br/>";
        echo "After decryption: ".$dec."<br/>";

Solution 6 - Php

These are compact methods to encrypt / decrypt strings with PHP using AES256 CBC:

function encryptString($plaintext, $password, $encoding = null) {
    $iv = openssl_random_pseudo_bytes(16);
    $ciphertext = openssl_encrypt($plaintext, "AES-256-CBC", hash('sha256', $password, true), OPENSSL_RAW_DATA, $iv);
    $hmac = hash_hmac('sha256', $ciphertext.$iv, hash('sha256', $password, true), true);
    return $encoding == "hex" ? bin2hex($iv.$hmac.$ciphertext) : ($encoding == "base64" ? base64_encode($iv.$hmac.$ciphertext) : $iv.$hmac.$ciphertext);
}

function decryptString($ciphertext, $password, $encoding = null) {
    $ciphertext = $encoding == "hex" ? hex2bin($ciphertext) : ($encoding == "base64" ? base64_decode($ciphertext) : $ciphertext);
    if (!hash_equals(hash_hmac('sha256', substr($ciphertext, 48).substr($ciphertext, 0, 16), hash('sha256', $password, true), true), substr($ciphertext, 16, 32))) return null;
    return openssl_decrypt(substr($ciphertext, 48), "AES-256-CBC", hash('sha256', $password, true), OPENSSL_RAW_DATA, substr($ciphertext, 0, 16));
}

Usage:

$enc = encryptString("mysecretText", "myPassword");
$dec = decryptString($enc, "myPassword");

EDIT: This is a new version of functions that use AES256 GCM and PBKDF2 as key derivation, more secure.

function str_encryptaesgcm($plaintext, $password, $encoding = null) {
    if ($plaintext != null && $password != null) {
        $keysalt = openssl_random_pseudo_bytes(16);
        $key = hash_pbkdf2("sha512", $password, $keysalt, 20000, 32, true);
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length("aes-256-gcm"));
        $tag = "";
        $encryptedstring = openssl_encrypt($plaintext, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $tag, "", 16);
        return $encoding == "hex" ? bin2hex($keysalt.$iv.$encryptedstring.$tag) : ($encoding == "base64" ? base64_encode($keysalt.$iv.$encryptedstring.$tag) : $keysalt.$iv.$encryptedstring.$tag);
    }
}

function str_decryptaesgcm($encryptedstring, $password, $encoding = null) {
    if ($encryptedstring != null && $password != null) {
        $encryptedstring = $encoding == "hex" ? hex2bin($encryptedstring) : ($encoding == "base64" ? base64_decode($encryptedstring) : $encryptedstring);
        $keysalt = substr($encryptedstring, 0, 16);
        $key = hash_pbkdf2("sha512", $password, $keysalt, 20000, 32, true);
        $ivlength = openssl_cipher_iv_length("aes-256-gcm");
        $iv = substr($encryptedstring, 16, $ivlength);
        $tag = substr($encryptedstring, -16);
        return openssl_decrypt(substr($encryptedstring, 16 + $ivlength, -16), "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $tag);
    }
}

Usage:

$enc = str_encryptaesgcm("mysecretText", "myPassword", "base64"); // return a base64 encrypted string, you can also choose hex or null as encoding.
$dec = str_decryptaesgcm($enc, "myPassword", "base64");

Solution 7 - Php

Few important things to note with AES encryption:

  1. Never use plain text as encryption key. Always hash the plain text key and then use for encryption.
  2. Always use Random IV (initialization vector) for encryption and decryption. True randomization is important.
  3. As mentioned above, don't use [tag:ECB] mode, use CBC instead.

Solution 8 - Php

If you are using MCRYPT_RIJNDAEL_128, try rtrim($output, "\0\3"). If the length of the string is less than 16, the decrypt function will return a string with length of 16 characters, adding 03 at the end.

You can easily check this, e.g. by trying:

$string = "TheString";
$decrypted_string = decrypt_function($stirng, $key);

echo bin2hex($decrypted_string)."=".bin2hex("TheString");

Solution 9 - Php

Here's an improved version based on code written by blade

  • add comments
  • overwrite arguments before throwing to avoid leaking secrets with the exception
  • check return values from openssl and hmac functions

The code:

class Crypto
{
	/**
	 * Encrypt data using OpenSSL (AES-256-CBC)
	 * @param string $plaindata Data to be encrypted
	 * @param string $cryptokey key for encryption (with 256 bit of entropy)
	 * @param string $hashkey key for hashing (with 256 bit of entropy)
	 * @return string IV+Hash+Encrypted as raw binary string. The first 16
	 *     bytes is IV, next 32 bytes is HMAC-SHA256 and the rest is
	 *     $plaindata as encrypted.
	 * @throws Exception on internal error
	 *
	 * Based on code from: https://stackoverflow.com/a/46872528
	 */
	public static function encrypt($plaindata, $cryptokey, $hashkey)
	{
		$method = "AES-256-CBC";
		$key = hash('sha256', $cryptokey, true);
		$iv = openssl_random_pseudo_bytes(16);

		$cipherdata = openssl_encrypt($plaindata, $method, $key, OPENSSL_RAW_DATA, $iv);

		if ($cipherdata === false)
		{
			$cryptokey = "**REMOVED**";
			$hashkey = "**REMOVED**";
			throw new \Exception("Internal error: openssl_encrypt() failed:".openssl_error_string());
		}

		$hash = hash_hmac('sha256', $cipherdata.$iv, $hashkey, true);

		if ($hash === false)
		{
			$cryptokey = "**REMOVED**";
			$hashkey = "**REMOVED**";
			throw new \Exception("Internal error: hash_hmac() failed");
		}

		return $iv.$hash.$cipherdata;
	}

	/**
	* Decrypt data using OpenSSL (AES-256-CBC)
	 * @param string $encrypteddata IV+Hash+Encrypted as raw binary string
	 *     where the first 16 bytes is IV, next 32 bytes is HMAC-SHA256 and
	 *     the rest is encrypted payload.
	 * @param string $cryptokey key for decryption (with 256 bit of entropy)
	 * @param string $hashkey key for hashing (with 256 bit of entropy)
	 * @return string Decrypted data
	 * @throws Exception on internal error
	 *
	 * Based on code from: https://stackoverflow.com/a/46872528
	 */
	public static function decrypt($encrypteddata, $cryptokey, $hashkey)
	{
		$method = "AES-256-CBC";
		$key = hash('sha256', $cryptokey, true);
		$iv = substr($encrypteddata, 0, 16);
		$hash = substr($encrypteddata, 16, 32);
		$cipherdata = substr($encrypteddata, 48);

		if (!hash_equals(hash_hmac('sha256', $cipherdata.$iv, $hashkey, true), $hash))
		{
			$cryptokey = "**REMOVED**";
			$hashkey = "**REMOVED**";
			throw new \Exception("Internal error: Hash verification failed");
		}

		$plaindata = openssl_decrypt($cipherdata, $method, $key, OPENSSL_RAW_DATA, $iv);

		if ($plaindata === false)
		{
			$cryptokey = "**REMOVED**";
			$hashkey = "**REMOVED**";
			throw new \Exception("Internal error: openssl_decrypt() failed:".openssl_error_string());
		}

		return $plaindata;
	}
}

If you truly cannot have proper encryption and hash keys but have to use an user entered password as the only secret, you can do something like this:

/**
 * @param string $password user entered password as the only source of
 *   entropy to generate encryption key and hash key.
 * @return array($encryption_key, $hash_key) - note that PBKDF2 algorithm
 *   has been configured to take around 1-2 seconds per conversion
 *   from password to keys on a normal CPU to prevent brute force attacks.
 */
public static function generate_encryptionkey_hashkey_from_password($password)
{
    $hash = hash_pbkdf2("sha512", "$password", "salt$password", 1500000);
    return str_split($hash, 64);
}

Solution 10 - Php

If you are using PHP >= 7.2 consider using inbuilt sodium core extension for encrption.

Find more information here - http://php.net/manual/en/intro.sodium.php.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAndreas PrangView Question on Stackoverflow
Solution 1 - PhpScott ArciszewskiView Answer on Stackoverflow
Solution 2 - PhpbladeView Answer on Stackoverflow
Solution 3 - Phpzz1433View Answer on Stackoverflow
Solution 4 - PhpFabien SaView Answer on Stackoverflow
Solution 5 - PhpDunsin OlubobokunView Answer on Stackoverflow
Solution 6 - PhpMarco ConcasView Answer on Stackoverflow
Solution 7 - PhpNavneet KumarView Answer on Stackoverflow
Solution 8 - PhpKamenView Answer on Stackoverflow
Solution 9 - PhpMikko RantalainenView Answer on Stackoverflow
Solution 10 - PhpM_R_KView Answer on Stackoverflow