Highly Secure Data Encryption & Decryption Made Easy with PHP, MCrypt, Rijndael-256, and CBC

Random hexadecimal codes on a computer monitor.  Shallow depth of field.In various projects in the past I’ve had to revisit the topic of data encryption and decryption and the best way to accomplish it. In the interest of developing in the simplest, most efficient, and most secure way I have choosen the MCrypt PHP library (built-in to PHP since v4.0.2), Rijndael-256 cipher, and the Cipher Block Chaining (CBC) mode.

Previously I have used the Electronic CodeBook (ECB) mode, but have learned that it is far less secure than CBC because it creates the same hash every time for the same source data. CBC on the other hand creates a unique hash every time even for the same source data.

Anyways, below you’ll find my revised encrypt/decrypt functions with support for all PHP data types.

Note: If you are not running PHP 5.3+ then you may need to replace MCRYPT_DEV_URANDOM with MCRYPT_RAND.

Update 11/14/2013: Switched to MCRYPT_DEV_URANDOM. Changed to hexadecimal encryption key that is later converted to binary for use with MCrypt. Added message authentication code (MAC) check. Thanks go to Bryan C. Geraghty for his recommendations.

Update 4/9/2014: Created an object-oriented version for those interested, view the Gist on GitHub.

Update 9/25/2014: Updated decrypt function so that you don’t receive a PHP notice if the input data is formatted incorrectly (not pipe delimited). Thanks go to Jonathan for noticeing.

Update 2/21/2015: Removed references to AES in the article since Rijndael-256 (256-bit blocks & 256-bit keys) is not the same as AES-256 (128-bit blocks & 256-bit keys).

Comments

  1. Kenan says

    Hi there, this is a very good script, but also a bit taxing when I need to decrypt say 20 data strings in one page. It currently takes around 10 secs to decrypt one.

    Is there a way to maybe shorten the processing time? I don’t mind reducing the security a bit. Thanks!

    • says

      Encrypt/decrypt speed is CPU-dependent, so on shared hosting it will be limited because your CPU usage is limited.

      To encrypt & decrypt a 1024 character string 100 times it takes around 150 seconds on shared hosting, but in a local or dedicated hosting environment it takes only a fraction of a second.

      Instead of encrypting & decrypting 20 times can you put your strings in an associative array and then encrypt/decrypt it once?

      If you have an array containing a hundred 1024 character strings it only takes around 0.5 seconds on shared hosting. Quite a time-saver!

    • says

      If you’re just looking to speed things up you can try a different cipher. For example, replace all MCRYPT_RIJNDAEL_256 references with MCRYPT_BLOWFISH and the speed should improve by 5-10 times.

      • Bryan C. Geraghty says

        Hey Josh,

        A friend of mine found these functions and asked if these were okay to use and here is the feedback I sent:

        1. CRITICAL: No MAC (Message Authentication Code); I could create all kinds of havoc in a system that use these functions.
        2. If the key is encoded, it must be decoded before use in encryption or decryption. (in this case, base64)
        3. Padding is done with null bytes. This means that if your original plaintext contains nulls at the end, they will be lost. Use PKCS7.
        4. The mcrypt_create_iv() call should use MCRYPT_DEV_URANDOM

        • says

          Thanks for the analysis Bryan, see my comments below:

          1) Could you please reference an article or explain how would you cause havoc? It just returns bool(false) if I modify the encrypted data – because the data is then corrupt and unable to be unserialized. I looked into adding MAC, looks like I would just need to create a hash of the data to be encrypted, save that like the IV and then check against it on decrypt.
          2) I’m not encoding the key, just the IV, but if I’m missing something let me know.
          3) Mcrypt does pad with \0 but since everything is serialized before encrypting any nulls will be inside the serialized data and not at the end. Try it for yourself.
          4) Great tip, that increases the performance. After some research it looks like, depending on the system, /dev/random must wait until it gets more entropy, which causes a delay.

          Thanks again, I look forward to your response!

          • Bryan C. Geraghty says

            Hey Josh,

            Sure. My responses are below.

            1) Sure. Please see the following links that are common attacks against this specific weakness: 1a) http://en.wikipedia.org/wiki/Bit-flipping_attack 1b) http://en.wikipedia.org/wiki/Padding_oracle_attack

            2) The statement define(‘ENCRYPTION_KEY’, ‘0GSQKwtk14lzW4TkRrOZwxUM5QaNIE8a’) shows a base64 encoded key. This should be decoded before use.

            3) Your point is valid about serialization and no valid following nulls… is there a reason you are doing that? The only thing it adds is predictable plaintext positions which will aide a cryptanalyst. I recommend removing the serialization and using PKCS7 padding.

            4) It’s not just a performance enhancement :) The locking leaks information about the internal state of your PRNG.

          • says

            1) Thanks, I’ll plan on implementing MAC even though the unserializing seems to prevent the decrypt function from returning corrupt data for attackers to analyze.

            2) It’s an 32-byte ASCII key right now. I’ll plan on switching to generating a 32-byte binary key from an ASCII key.

            3) Serializing enables easy encryption/decryption of all PHP data types, a main goal of these functions.

          • says

            I’ve updated the gist with my changes.

            I tried using crypt with 5000 and 1024 iterations for the purpose of MAC but that was too slow so I used hash_hmac and that didn’t seem to affect the performance.

            Thanks for the helpful comments Bryan, it’s better solution because of them.

        • Hexalys says

          One quick follow up. Would a PKCS7 padding be necessary or recommended in addition to the MAC? Or does the MAC now solve the “predictable plaintext positions” aspect?

  2. Pete says

    Hello Josh,
    Love these functions, they work great.

    My question is with regard to PKCS7 padding. I would like to use a Master Key system which would replace the static Key you are using. The problem is that the master key created by users may or may not equal 64 characters. Most likely they would be under 20 characters.

    Can I use PKCS7 padding to keep the Key at 64 characters and still decrypt the string with the same master key.

    Thanks for any help.

    Pete

    • says

      The length of the password or pass phrase the user provides won’t matter, just hash it to produce a key of static length.

      $pass_phrase = 'HA HA, my pass phrase is unbreakable. Run in fear!';
      $master_key = hash('sha256', $pass_phrase);
      // Result is a 32-byte (64 character) hexadecimal string
      // fce73352086cd2f4013a18e089d0081d61371dcd4371b9b473db6c9f183179b3

      Store the master key hash per user and use as the encryption/decryption key for their personal data.

  3. Pete says

    Thanks Josh,

    I didn’t think of that. I was taking a totally different route.

    Your idea is much easier. I’ll consider it.

    Thanks again.

    Pete

  4. Guigs says

    Hello, thanks for the code. I was just wondering : if I want to store a string encrypted in a DB, what would be the size of the field compared to the original string ?

    Thanks and regards,
    G

    • says

      I don’t think there is a hard and fast rule to this. After some tests there seems to be a curve. Looks like it settles down to ~33% overhead when encrypting strings longer than 25,000 characters in length. Use a TEXT (~65KB), MEDIUMTEXT (~16MB) or LONGTEXT (~4GB) data field in the DB depending on the size of what you’re storing.

  5. Abhay Raj says

    Hi Josh

    Just i need to know, is this method gives the same result if we use it on different -2 languages like java, .NET, perl etc.

  6. Jonathan says

    Regarding the OOP version:

    1. Why don’t you call method “setKey” in the constructor?
    2. The IV can be changed by anyone (it is public) so it is recommended to include a check between line 29 and line 30 to validate the IV size, right? Otherwise PHP might throw an nasty error.

  7. Jonathan says

    Also Wikipedia recommends:

    To prevent this [padding oracle] attack, one could append an HMAC (Hash-based message authentication code) to the ciphertext. Without the key used to generate the HMAC, an attacker won’t be able to produce valid ciphertexts. Since the HMAC is checked before the decryption stage, the attacker cannot do the required bit-fiddling and hence cannot discover the plaintext.

    Source: http://en.wikipedia.org/wiki/Padding_oracle_attack

    Your code does not append the HMAC but actually encrypts it, so you have to decrypt the message before checking the HMAC.
    I’m not a cryptoanalysist, but is this the correct way of implementing a HMAC when encrypting messages?
    Is your code still safe to use?

    • says

      I used the MAC-then-encrypt method, which SSL/TLS uses. If your opinion of that MAC method is that it’s not safe then there are other solutions that use encrypt-then-MAC.

  8. Yasser says

    Hi
    thank you for this great script but I have a question.Is it possible for professional hackers to decrypt the encrypted data? (without the key). If you have an advanced and more secure version of this script please provide us with it.
    Thanks a lot.

  9. Jonathan says

    Some questions regarding your code (OOP version):

    1.
    The key is a hexadecimal String (length: 32 byte).
    In the encrypt/decrypt method you convert this hex-key to binary format using `pack(‘H*’, $this->key);`
    Then you convert it back to a hex String using bin2hex($key).
    Question: Why not use $this->key instead of bin2hex($key) ($key is the binary representation of the hexadecimal String)?

    2.
    `$decrypt = explode(‘|’, $decrypt);
    $decoded = base64_decode($decrypt[0]);`
    You should check if `is_array($decrypt) && count($decrypt) === 2` or, alternatively: `isset($decrypt[0]) && isset($decrypt[1])`, right? Otherwise one could produce nasty error messages in the error log.

    3.
    You are aware, that MCRYPT_RIJNDAEL_256 is NOT Rijndael using a 256-bit-key?
    The “256” in MCRYPT_RIJNDAEL_256 refers to the block size length. Maybe that should be mentioned somewhere.

    4.
    You use the last 32 chars from the hex key as a key for the hash_hmac function.
    Is this safe to use? This might leak some information, right? Shouldn’t the encryption class have two different keys: One for mcrypt and one for the hmac function?

    Thanks for helping,
    Jonathan

    • says

      1. Yes, you can use $this->key instead of bin2hex($key). It was carry over from the non-OOP version. Code updated.
      2. Thanks, I appended a pipe character to the input data to prevent the notice.
      3. I never mentioned anything about a 256-bit key but thank you for informing readers.
      4. As far as I know this is safe and does not leak any information. The MAC hash is encrypted with the original data and not visible like the IV.

      • Bruno says

        Hi Josh.

        Would a PKCS7 padding be a good addition to the MAC or unnecessary?

        I like having the serialization, but at the same time, it would nice to solve the “predictable plaintext positions” aspect mentioned by Bryan C. Geraghty.

        • says

          I don’t believe that it’s necessary. Yes, the fact that the data is serialized means that the structure will be the known to a certain degree (e.g., serialized data will always have a colon as the 2nd character and the last two characters will always be “;}”). PKCS#7 padding will not change this fact.

  10. Thom Turnbull says

    Great way for hackers to encypt codes into software, then sell the software as a modification to their site, since there’s no way to figure out what the code is. As I have found in boonex free mods for dolphin 7. If you’re a hacker this encyption method was created just for you. Enjoy.

  11. zzjin says

    Thanks great scripts. When use it, I found that it genarate a so long string with little inputs(12char->191char). I know it was build for more safe.
    I wanna to use it as cookie encrypt so the encrypted data length must be careful conisdered. Is there any optional options such as ‘MCRYPT_RIJNDAEL_128’ or some thing can much reduce encrypted data length but with little security down?

    • says

      Yes, you can replace all MCRYPT_RIJNDAEL_256 references with MCRYPT_RIJNDAEL_128. I’m not sure if it will work with just any cipher, but you can experiment.

  12. Rob says

    You store the ENCRYPTION_KEY in plain text in your script. Doesn’t that undermine the whole thing?

    If someone managed to see your script then they’d be able to decrypt all of the encrypted info surely.

    (I’ve only just started learning about encryption so I’m almost definitely missing something!)

  13. Hamed says

    Hi Josh,

    is it possible to search item based on LIKE or = in select statement ? something like:
    ‘SELECT * from tbl WHERE name = “‘.$crypt->encrypt(‘George’).'”‘;
    OR
    ‘SELECT * from tbl WHERE name LIKE “‘.$crypt->encrypt(‘Geo’).’%”‘;

  14. Cary Bondoc says

    Hello Josh,

    Thank you for this wonderful code, this is what I’m looking for. I am trying to integrate it to my register and login function. However, I am having a hard time to find a way on how can I make the encrypt static.

    For example I want to encrypt ‘admin’. The first encryption will become like this

    P/ztzuh5ejtfdM1/uvuedA5qROxBlLize8hQyc1quArGXqzf2uPVAa9iFnnKmMVUsbJzPd1WsIHMeC9dH2eRMpiiyrdyy8v36xMUwNVp5ZD+06tWJVJce+70lun6zS9w|+dtrYmXJvkg/U9TdWPhBOegtd26HvyYQtQnlEDMgiTU=

    Then if I refresh it it will become like this

    szEfnPFcCS6nqSGuVlbJXYgS0k4unJoBD6HnHRLbuot2NntgQ72EhGF6nxVTTPswJ6GNPCpWdM1PzGH/sTSwnHCXCnyL1nQYNjRCfmsRjODkVLzcnADVF5qQrn+OWI36|caixc+ZibwhyiQr1oDoUYIMl7uBS/zzGvAp3C5DJKiw=

    Do I need to store to the database the key also so I can use it to decrypt properly?
    How can I get the key so it will not become random?

    I am just an intermediate PHP developer, and I am not familiar in security, that’s why I’m having a hard time.

    Thanks,

    Cary Bondoc

    • says

      Using a static key, the encrypted data for the same source data will be unique each time because of the mode – Cipher Block Chaining (CBC).

      • Jonathan says

        Not true.
        The only reason Cary gets different output when encrypting the same input (string “admin”) is that the IV (initialization vector) is different on every encrypt() call.

        Same input + same IV = always same output

  15. eric says

    i am passing data through url

    $data= $row[‘id’];
    $encrypted_data = mc_encrypt($data, ENCRYPTION_KEY);

    “. $row[‘title’].”

    can’t decrypt data when i am trying to decrypt in other page please help

    $encrypted_data= $_GET[‘ref’];

    echo’Decrypted Data: ‘ . mc_decrypt($encrypted_data, ENCRYPTION_KEY) . ”;

    • says

      First off, what you’re doing is a bad idea since you’ll be exposing your encrypted data. Putting that aside, you would need to urlencode the data and be aware of your URL length and request size limits. Servers and clients vary so what you’re doing may work for some and not for others. Find another way to do what you’re trying to do.

      • Jonathan says

        It’s absolutely fine to expose the encrypted data along with the IV.
        Only the key has to be secret.

  16. Victor says

    Josh, thanks for your work!

    I have a question. I need to store confidential users data. For this I encrypt text using mc_encrypt(). As a encrypt/decrypt key, I use the result of function:

    $encryption_key = hash('sha256', $ _POST ['user_password']);

    When add a user, I saved in the unencrypted and encrypted user login using the mc_encrypt($login, $user_password) function, and during the authentication process trying to decrypt it. If it turns out – the user log in.

    So I never keep password or keys that encrypts the data. In case of hacking the maximum that can get the hacker – unencrypted and encrypted using mc_encrypt logins.

    Is this scheme secure? Can hackers, knowing that “that hash” – is “this login, but encrypted” simplify password cracking?

    • says

      Sounds good to me, assuming that $user_password = $encryption_key and you’re only saving the result of mc_encrypt() into the database. You’ll also need to force users to use strong passwords (12 or more characters with a mixture of upper/lower case letters, numbers, and symbols).

  17. Khim Thapa says

    I tried this function but doesn’t work when I save encrypted login details( from user registration form) on DB table and validate it from login form. Your kind help will be appreciated.

  18. ulysses says

    Hi there,

    thank you for a nice and clean class. I’ll be using it in an upcomming open source project. After quickly skimming the source I have two suggestions that will increase security level even further:

    1.) the comparison of the hmac hashes may be used to run a timing attack (refer to: http://blog.astrumfutura.com/2010/10/nanosecond-scale-remote-timing-attacks-on-php-applications-time-to-take-them-seriously/). To circumvent this just compare hashed values: if(md5($calcmac) !== md5($mac)). This will render useless the gained information from any timing attack.

    2.) it is advisable to use independent keys for encryption and hmac. You might want to consider implementing that.

    Keep up the great work!

    • says

      Thank you ulysses. Your suggestions are good and have been mentioned before. They would be implemented in a v2.0 that is not backwards compatible with the current version.

      1) I had written code for a constant-time text comparison of the MAC but then realized that this is not an issue when using the MAC-then-encrypt method because the MAC is only compared after decryption to verify that the data is intact and the same as when it was encrypted. Since the correct MAC is only exposed when the correct encryption key is used it doesn’t matter whether the text comparison is done in constant-time or not. Changing to an encrypt-then-MAC method (requiring a constant-time MAC comparison) would not be backwards compatible with the current version.

      2) Agreed, this would also break backwards compatibility so not in this version.