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 chosen 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).
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!
Josh Hartman 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!
Josh Hartman says
If you’re just looking to speed things up you can try a different cipher. For example, replace all
MCRYPT_RIJNDAEL_256
references withMCRYPT_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
Josh Hartman 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.
Josh Hartman 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.
Josh Hartman 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 usedhash_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?
Kenan says
Thank you for your suggestions! Will Try them out 🙂
Galih says
Hi Josh,
It works, thanks you very much.
Regards
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
Josh Hartman 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.
Store the master key hash per user and use as the encryption/decryption key for their personal data.
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
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
Josh Hartman 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.
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.
Josh Hartman says
Doubtful, since the internal PHP functions probably don’t exist as exact duplicates in other languages.
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.
Josh Hartman says
Both oversights on my part, fixed now. Thanks!
Jonathan says
Awesome, thanks for fixing 🙂
Jonathan says
Also Wikipedia recommends:
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?
Josh Hartman 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.
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.
Josh Hartman says
Yes, with a few billion years it would be possible to brute force the key.
Yasser says
Than you for your reply and your help.
Best wishes.
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
Josh Hartman 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.
Josh Hartman 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.
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.
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?
Josh Hartman 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.
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!)
Josh Hartman says
Everything is one script as an example of usage. How and where you store the key and how you retrieve it or make it available to the functions is up to you.
Rob says
Hi Josh,
Ok, understood. Thanks for the swift response.
Rob
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’).’%”‘;
Josh Hartman says
No, absolutely not.
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
Josh Hartman 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
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) . ”;
Josh Hartman 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.
David says
Thank you very much!
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?
Josh Hartman says
Sounds good to me, assuming that
$user_password = $encryption_key
and you’re only saving the result ofmc_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).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.
Josh Hartman says
That’s not enough to go on. Please create a Gist with an example of your usage and share the link.
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!
Josh Hartman 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.