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 (AES) 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.

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.

  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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>