PHP AES encrypt / decrypt

275,181

Solution 1

$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 2

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 3

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 4

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

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/>";
Share:
275,181
Andreas Prang
Author by

Andreas Prang

Updated on February 03, 2022

Comments

  • Andreas Prang
    Andreas Prang over 2 years

    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‰Â—

  • Paul Jacobse
    Paul Jacobse almost 12 years
    I was using this code, and I found a bug. The trim should NOT be used!!! it should be a rtrim() with a second parameter "\0". In rare cases the first or last character of the encrypted value could be a space or return, the decryption goes wrong...
  • techie_28
    techie_28 over 11 years
    I am using this to store a field in the database.But it fails when It has to be matched against some user provided data even on correct input,But If I do it as a MYSQL search from the PHPMYADMIN,it works fine.Any Ideas?
  • Thunder Rabbit
    Thunder Rabbit over 11 years
    Thank you @PaulJacobse for the tweak; fixed version available here stackoverflow.com/review/suggested-edits/1314732
  • mgutt
    mgutt about 11 years
    You should add an example to clearify the usage of aes256Key. This example shows how to use it: php.net/manual/fr/book.mcrypt.php#107483
  • Fabien Sa
    Fabien Sa about 11 years
    It's pretty the same but my example doesn't use salt for clarity.
  • Luc
    Luc over 10 years
    Could you obfuscate that anymore please? I'm afraid I can, with some effort, still read what your code does. </sarcasm mode> Code should be self-descriptive. Give things like the output of mcrypt_get_iv_size an appropriate variable name and then use it. This kind of indentation is hard to read. Unless you're used to Lisp, maybe, but most PHP programmers aren't I'd say. I'm not saying the OP's (question asker's) code was any better, but as a good answer you might improve the code too.
  • Polynomial
    Polynomial about 10 years
    -1 for ECB. See the Wikipedia entry for details on why it's a poor choice of block cipher mode.
  • Clément
    Clément about 10 years
    -1: Passing different IVs in the encrypt and decrypt functions makes no sense at all. The only reason why this works is because the ECB mode does not use an initialization vector at all, so any value would do and produce the same output.
  • Clément
    Clément about 10 years
    +1. The top answer generates random IVs to feed a system (ECB) that doesn't need any.
  • Ashwin Parmar
    Ashwin Parmar almost 10 years
    It is working, Great Should I use md5() Value as a Secret Key?
  • Admin
    Admin almost 10 years
    If I have understood it properly, the $password = "myPassword_!" becomes part of the encryption algorithm, right?
  • Sergio Viudes
    Sergio Viudes almost 10 years
    Using MCRYPT_RIJNDAEL_128 this worked for me: rtrim($output, "\x00..\x1F")
  • Maarten Bodewes
    Maarten Bodewes over 9 years
    Be warned that the code above does not use AES nor does it use PKCS#7 padding, which means it will be incompatible with any other system out there. I'm the guy that fixed that example code for mcrypt_encrypt.
  • Maarten Bodewes
    Maarten Bodewes over 9 years
    This code does not use AES, it uses Rijndael with a block cipher of 256 bits (AES uses a block size of 128 bits). This code is using ECB etc. and should not be considered a secure example on how to perform encryption/decryption.
  • wappy
    wappy about 9 years
    I don't know why this answer is so up voted. Just because 'it works' it doesn't mean it secured and should be used on production environments. MCRYPT_MODE_ECB use is highly descurged and even PHP mcrypt_ecb function has been DEPRECATED as of PHP 5.5.0. Relying on this function is highly discouraged. Instead you should use MCRYPT_MODE_CBC mode: wpy.me/blog/15-encrypt-and-decrypt-data-in-php-using-aes-256
  • Andrew
    Andrew over 8 years
    the library you provided, returns encrypted message in binary format - correct? Is it possible to return in a simple string format? Thanks
  • Erwin Mayer
    Erwin Mayer over 7 years
    A C# .NET port is available in this GitHub repo, in case anyone needs it : github.com/mayerwin/SaferCrypto. Thank you @ScottArciszewski.
  • jww
    jww almost 7 years
  • jww
    jww almost 7 years
  • Riking
    Riking almost 7 years
    No @Andrew, it returns the message with base64 encoding, which is a simple string
  • Moritz Friedrich
    Moritz Friedrich over 6 years
    Hey there, I converted this in a slightly easier to use class, see here: gist.github.com/Radiergummi/b326219f55edb33759791a78a1c134c3‌​. Thank you for your work!
  • MBourne
    MBourne over 5 years
    Every ssl_encrypt/decrypt example I come across (including the ones on this page) have the 2 processes back to back. But if you just want to decrypt some encrypted text from a file, it is not successful. Can anyone please answer my question here: stackoverflow.com/questions/53238812/…
  • blade
    blade over 5 years
    @MBourne this example is compatible with any standard AES-256-CBC implementation. The problem is likely somewhere else. For example you may be reading the file as a text (instead of binary), the crypto functions have quite specific requirements, like for IV length, AES supports a other algorithms besides CBC, ... a lot of factors in play here.
  • MBourne
    MBourne over 5 years
    @blade: I gave up and now use str_rot13(). I just needed jumbled text - security wasn't the issue. Thanks anyway.
  • FireEmerald
    FireEmerald about 5 years
    Why don't you @blade also include the $iv while generating the hash_hmac()? Wouldn't it be better to also verify, that the nonce wasn't changed?
  • blade
    blade about 5 years
    @FireEmerald I don't think it's necessary, as IV is already "baked" into the ciphertext. If you changed the IV, you wouldn't be able to decipher the data, so integrity is guaranteed.
  • Yohannes Kristiawan
    Yohannes Kristiawan about 5 years
    I am trying many AES-256 functions I can find on the internet with no success. This one just works! THANKS @blade!!!
  • Shahin
    Shahin about 5 years
    It's not a good practice to compare hashes using the equality operator, You should use hash_equals() instead, as it's vulnerable to the timing attack, more information here
  • Luke Joshua Park
    Luke Joshua Park almost 5 years
    This answer is almost there, but needs... 1) A better KDF, SHA-256 is a very poor KDF. Use PBKDF2 at the very least, but Argon2/bcrypt would be better. 2) The IV needs to be included in the HMAC - the point of an HMAC is to ensure decryption will either result in the plaintext or fail - not including the IV gives rise to a situation where the user thinks they're getting the original plaintext, but aren't. 3) Use a time safe comparison when comparing the hashes, otherwise this code could be vulnerable to timing attacks. 4) Don't use the same key for HMAC as you do for AES.
  • Luke Joshua Park
    Luke Joshua Park almost 5 years
    This code leaves IV handling to the user (who *will do it poorly) and also doesn't include any integrity checks. Not good crypto code.
  • blade
    blade almost 5 years
    @LukeJoshuaPark Thank you for the suggestions, they're all valid and I'll update the answer - one question though - when using a salted KDF like PBKDF2, is it necessary to generate salt for each key and bundle the salts with ciphertext for later decryption or can I safely reuse the IV? Argon2 is unfortunately not readily available in most PHP installations.
  • relipse
    relipse about 4 years
    is it possible to base64 encode the stuff so it is actual letters and numbers characters? readable in a command line or what not
  • Mikko Rantalainen
    Mikko Rantalainen about 4 years
    @LukeJoshuaPark: about your point (1): why do you think one should use e.g. PBKDF2 or argon2? I think we can assume that the key is secure and sha-256 here is not about key derivation but converting string input into binary 256 bit output. It's not like output of SHA-256 is leaked to encrypted output as plaintext so where's the problem here?
  • Luke Joshua Park
    Luke Joshua Park about 4 years
    @MikkoRantalainen We can't assume the key is secure, specifically because it isn't a key, it's a human-selected, low-entropy password. If we use SHA-256 to derive our encryption key, which takes a very negligible amount of time, brute force attacks on passwords are quite easy. However, if we use PBKDF2 or Argon2, where we can fine-tune the time it takes to derive a password (think a few hundred milliseconds), brute forcing becomes far less feasible. Pretty much the exact same reason we wouldn't use SHA-256 as a password hash.
  • Mikko Rantalainen
    Mikko Rantalainen about 4 years
    @LukeJoshuaPark: OK, I assumed that the developer would understand encryption requirements and use a key with equivalent amount of entropy to encryption used. If one uses keys such as output of openssl rand -hex 32 there's no need to avoid SHA-256 in the above code.
  • Luke Joshua Park
    Luke Joshua Park about 4 years
    @MikkoRantalainen The input is defined as a password though, not a key. It could be developer chosen, or it could be a user chosen password - definitely requires a decent KDF. Assuming that "developers understand encryption requirements" is a footgun - with plenty of historical evidence. WEP, for example.
  • Luke Joshua Park
    Luke Joshua Park about 4 years
    It isn't enough just to hash a password to use as an encryption key, see comments on blade's answer.
  • Mikko Rantalainen
    Mikko Rantalainen about 4 years
    @LukeJoshuaPark: I added a new answer with tweaked implementation: stackoverflow.com/a/61016088/334451
  • Luke Joshua Park
    Luke Joshua Park about 4 years
    Nice edits, looks good! The one thing, like we previously discussed, is that this is vulnerable to brute-force through lookup tables because we trust the user to provide a "cryptokey" that has sufficient entropy. This problem could be fixed with a real KDF rather than SHA-256. Otherwise, looks good!
  • Mikko Rantalainen
    Mikko Rantalainen about 4 years
    @LukeJoshuaPark: Yeah, I think these methods would be the low level implementation using a real key. Perhaps I should add a method for using Key derivation function (KDF) to go from user password to encryption key. However, such method should not claim to magically have 256 bit of entropy from low quality user password. Instead, KDF logically is an injection from e.g. 32 bit key to 256 bit keyspace where attacker does not have an easy way to simply enumerate all the 2^32 possible keys out of 256 bit keyspace.
  • Mikko Rantalainen
    Mikko Rantalainen about 4 years
    Assuming that we have only password (=no storage for salt), the KDF would need to be something like hash_pbkdf2("sha256", $password, $password, 500000). I'm not sure if even that's enough with low quality passwords when we consider SHA-256 performance on GPUs.
  • Mikko Rantalainen
    Mikko Rantalainen about 4 years
    @LukeJoshuaPark do you think it would be okay to generate hash key and encryption key from the same password? For example $hash_key = hash_pbkdf2("sha256", "$password", "hash$password", 500000) and $encryption_key = hash_pbkdf2("sha256", $password, "enc$password", 500000).
  • Luke Joshua Park
    Luke Joshua Park about 4 years
    Yes - although I'd recommend running PBKDF2 with SHA-512 rather than SHA-256 if you're going to do that. This allows the first 256 bits of output to be the encryption key and the last 256 bits of output to be the hash key.
  • WilliamK
    WilliamK almost 4 years
    I found that this worked, while the others above produced strange characters and didn't decrypt to anything legible.
  • WilliamK
    WilliamK almost 4 years
    I am finding that this works intermittently. Each time it encrypts it produces a different answer. Sometimes it does not decrypt.
  • Marco Concas
    Marco Concas over 3 years
    Try this: encryptString("mysecretText", "myPassword", "hex") | decryptString($enc, "myPassword", "hex") @WilliamK
  • Chris
    Chris almost 3 years
    If you want to store this in a database you should do base64_encode on the return value of the encrypt function and base64_decode on the $ivHashCiphertext value of the decrypt function. If not you might run into encoding problems with the database.
  • naman1994
    naman1994 almost 3 years
    @blade I used it in PHP and it works perfectly fine. But when I encrypt the string and serve it as response of an API it's not quite working. I tried to use Content-Type: text/plain in httpresponse, but I'm not sure what really is the issue. Any help would be appreciated.
  • blade
    blade almost 3 years
    @naman1994 The output a binary blob, so you need to encode it into plain text - ie. using base64_encode, bin2hex, etc.
  • Jeaf Gilbert
    Jeaf Gilbert almost 3 years
    I encrypt the string in Javascript using crypto-es and want to decrypt it in PHP using your function, but it returns null. The passphrase are the same in JS and PHP. I already set encoding param using 'base64', but no luck. What am I possibly missing here?
  • Marco Concas
    Marco Concas almost 3 years
    I should check the function you use on JS to understand the problem, anyway I have tested this with C# and everything works perfectly.