37

How do I encrypt data in PHP, properly, using symmetric-key encryption? I have a message M and a secret S. I'm looking for a solution that uses cryptography properly without making any of the usual mistakes. In particular, the solution should use authenticated encryption, choose IVs properly, and generate the actual encryption key from the secret S using a suitable slow hash (in case S happens to be a password instead of an actual cryptographic key).

Can you suggest PHP code for this purpose?


My motive: I'd like to give PHP programmers good advice on how to do this, not bad advice. This question on StackOverflow is a real disappointment: it is full of highly-upvoted answers that are appallingly bad (ECB mode encryption? repeating IVs? encryption without authentication?). Let's figure out the right answer -- a code snippet that does things right -- and then go fix that broken window on StackOverflow. In keeping with the PHP programmer ethos, I'd prefer a snippet of code (that works for as many settings as possible; possibly with explanation and/or explanation of limitations/caveats) instead of just advice about algorithms and concepts.

D.W.
  • 98,420
  • 30
  • 267
  • 572
  • 23
    You weren't pleased with the base64 suggestion from SO? What a shame. – Simon Feb 04 '15 at 18:34
  • 12
    Don't use `base64`. ROT13 is enough for almost every use. If you need more security, use it twice... – ThoriumBR Feb 04 '15 at 19:20
  • 6
    @ThoriumBR this joke, I like. –  Feb 04 '15 at 19:28
  • Your question is too general. If you can re-state it with more specifics I think it could be answered, but the one answer posted is a general answer to a general question. – Steve Sether Feb 04 '15 at 23:18
  • 4
    @SteveSether, can you tell me in what way it is too general? I provided some specifications in the question (use authenticated encryption, use a slow hash for key derivation). What specific questions do you have about the requirements? What do you need elaborated? I'd be happy to clarify if you can help me understand what you find unclear. Thank you for your comments! – D.W. Feb 04 '15 at 23:23
  • 2
    @D.W. I think your question has more to do with how do I change the PHP community to understand the importance of understanding choosing the right cryptography and what the various modes of an algorithm mean. The reason they chose and upvoted the ECB mode was because it was the easiest, and it worked. Whose to say that if you give a good example with one of the feedback modes that anyone would understand the implications of why it's better? I think the code is really the unimportant part, and that sort of "copy/paste" mentality is the real culprit destroying security. – Steve Sether Feb 04 '15 at 23:48
  • 2
    The code snippit is just the symptom of the problem. The fact that the PHP community upvoted a TERRIBLE answer to that question is the real culprit. – Steve Sether Feb 04 '15 at 23:51
  • 2
    @SteveSether While that may be the case, we can't know that for sure when *no* good examples exist. If there were reasonable examples as alternatives that were being ignored, you'd have a point, but step one in solving the problem is to create (or find, if it exists) that canonical example that can be used as a reference. Until that point, it's just as likely that the cause is that the PHP community at large simply doesn't have the crypto experience to create such an example, and doesn't know any better than to propagate the use of poor alternatives. – Xander Feb 06 '15 at 14:46
  • 1
    @Xander You're certainly right, and I didn't want to imply giving a cannonical answer wasn't part of the solution, it's just not the full problem. I just wanted to point out that this is a cultural problem, not a technical one. The PHP community is rather notorious for many, many security problems. There's various reasons for that, but the underlying reasons need to be addressed aside from the technical ones. – Steve Sether Feb 06 '15 at 15:42
  • 1
    @SteveSether We certainly have no argument on that point! – Xander Feb 06 '15 at 16:02
  • 1
    Xander, SteveSether, I'm with you 100%. That said, as Xander hints, *this question* is about asking for a canonical code snippet. It's not about the broader question of how to change the PHP community (which is also important, but that's not what I'm asking in this question). All I'm asking in this question is for a canonical example of such a code snippet -- this question is scoped very narrowly. As you quite correctly say, the answer to this narrow question won't magically solve the broader problems you point out. – D.W. Feb 06 '15 at 20:52
  • I suggest migrating this to the cryptography SE. –  Feb 12 '15 at 20:14
  • 2
    Your question is too broad in the sense that there are different cryptographic methods and algorithms based on what your needs are. There isn't simply a one-size fits all solution, and this is the common pitfall people fall victim to when attempting to implement encryption. In general there aren't many scenarios where you would be forced to roll your own encryption. There are a lot of vetted public tooling for all sorts of uses, and in any case you should be looking into these before attempting anything on your own. – Alex Urcioli Feb 19 '15 at 16:25
  • 1
    There's actually [a pretty nice implementation on GitHub](https://github.com/defuse/php-encryption/blob/master/Crypto.php) that @Simon pointed out to me awhile back. It does not yet support turning a low-entropy password into strong key material, but in the review that I've done so far seems to take the other common issues into account. I'm going to keep an eye on it. – Xander Mar 08 '15 at 21:36
  • 1
    @KodeRiot yes, that's also what I was trying to convey. Encryption has many different use cases, and providing a single answer to one of them isn't terribly useful. – Steve Sether Mar 12 '15 at 20:03
  • 1
    I think a better question would be about what are the specific quirks of PHP that you should avoid? i.e. what's a good secure random number generator? What encryption modes are normally poor choices to avoid? It's difficult to give a cannonical answer to a rather general question, but the best you can give is a guideline, and warning. If you feed people the copy/paste solution they're looking for, you still haven't really solved the problem. – Steve Sether Mar 12 '15 at 20:06
  • 1
    The implementation @Xander referenced is solid. Use that if you can't use libsodium. – Scott Arciszewski Sep 13 '15 at 01:22
  • I was unable to access the link provided by @Xander - this one works for me: https://github.com/defuse/php-encryption – symcbean Aug 11 '17 at 14:56
  • @symcbean It's also linked in [Scott's answer](https://security.stackexchange.com/a/100139/12) – Xander Aug 11 '17 at 15:03

3 Answers3

16

Can you suggest PHP code for this purpose?

In order of preference.

1. PECL Libsodium

If you haven't heard of libsodium, go learn about it right now. I very strongly recommend that you use libsodium if possible.

These instructions should get you started with installing libsodium and the PECL extension on your computer. The rest of that e-book should give you an understanding of how to use specific components, and contains plenty of example code. If you need a starting point for PHP cryptography to point non-experts to, libsodium is the best available today.

For secret-key authenticated encryption, refer to this chapter of the e-book.

2. paragonie/halite

(Disclaimer: I wrote Halite.)

All the power of libsodium, and an interface that looks like:

use \ParagonIE\Halite\Symmetric\Crypto as Symmetric;

$key = KeyFactory::loadEncryptionKey('/path/to/key/file');
$ciphertext = Symmetric::encrypt($plaintext, $key);

Check it out: paragonie/halite.

3. defuse/php-encryption

Defuse Security published an encryption* library on Github at defuse/php-encryption.

By encryption, I actually mean authenticated encryption:

  • Version 1:
    • AES-128-CBC with PKCS#7 padding and a random IV
    • HMAC-SHA-256 (verified in constant-time)
    • Uses HKDF-SHA256 to split your key into an encryption key and an authentication key
  • Version 2:
    • AES-256-CTR with a random nonce (CTR mode doesn't need padding)
    • HMAC-SHA-256 (verified in constant-time)
    • Uses HKDF-SHA256 to split your key into an encryption key and an authentication key; now with a random HKDF salt to mitigate the impact of birthday collisions in the random CTR after encrypting 2^64 blocks

Disclaimer: I'm one of the coauthors of this library, although most of my contributions won't land until the version 2 rewrite, which includes version tagging and a streaming interface for encrypting/decrypting files safely.

4. zend/zend-crypt

I recommend this one last because I have not personally audited it, but it is part of the Zend Framework and a lot of PHP security experts attest to its quality. Go ahead and use it if you're already using the Zend Framework.

Update: Since I originally wrote this answer, I did discover an issue in Zend\Crypt\PublicKey\Rsa (ZF2015-10 a.k.a. CVE-2015-7503). However, their symmetric-key encryption implementation appears to be secure.

Scott Arciszewski
  • 835
  • 11
  • 28
1

D.W., the answer needs to come more in the form of a Design Pattern than a code snippet. One reason for this is the state of the art with PHP and the PHP community. There are precious few examples of "getting it right" that are available to average PHP developers.

Examples of this difficulty:

  1. If you are typing mcrypt into your code you are doing it wrong: mycrypt, as of last time I looked, is no longer maintained and is effectively abandoned. However, mycrypt is the extension able to obtain random numbers from /dev/urandom, which does appear to be the right way to do it on Linux-based servers.

  2. How to safely generate a random number: PHP's openssl extension gets it wrong. Openssl is the extension to use for new development, but it doesn't get everything right.

This brings us to the conclusion that we need to create our own cryptography code. Any expert will tell us, "Don't do that!" But with PHP there may not be an alternative. There are community-provided crypto packages, but it's the same issue: How do we know if that package "gets it right?"

The other reason is that, with PHP, the question comes up because of a specific situation. Are you trying to store hashed passwords across an insecure network? One set of protocols might be best.

Are you trying to secure web services between your server and your mobile app? A different approach may be more appropriate. (For example, can you depend on the app keeping a secret key secret, when the app can be reverse-engineered by an attacker?)

For the record, here are the steps I considered with symmetric secret-key encryption. I am using PHP 5.3, the mcrypt extension for access to /dev/urandom, and openssl for aes-256-cbc.

  1. Recognize that everything depends on keeping the secret key secret. If the secret keys can be extracted from the remote client software (or from either endpoint of your communication), an attacker with access to the encrypted text can read anything and everything, and insert spurious messages.

  2. Recognize that transmitting over HTTPS, for example, provides insufficient security against some attacks. For example, an attacker can install our app and observe decrypted HTTPS traffic being sent/received to/from our app.

  3. In other words, understand the environment in which your encryption is operating, and find out what attacks must be considered.

  4. Both sides of the encryption need the same shared secret key. Both sides need to obtain the key, and both need to keep the key from being stolen, seen, reverse engineered, etc.

  5. I believe (but lack sufficient expertise to be sure) that it's correct practice to hash your secret key to exactly 256 bits (for AES-256), e.g., use hash('sha256', $secretKey) to create the 256-bit key to feed into the encrypt/decrypt function. In other words, plain ASCII text as-is doesn't have enough entropy to be directly used as the secret key. So, use a hash function creating (or being truncated to) the correct number of bytes.

  6. For AES, you need a 128-bit input vector. Understand that this input vector needs to be different for every encryption/transmission. I have seen too many code samples with a fixed input vector! Use mcrypt with mode MCRYPT_DEV_URANDOM for the random bytes.

  7. The Doom Principle *thanks @mti2935) states that, if you perform ANY cryptographic operation on the received message before verifying its integrity, it will SOMEHOW inevitably lead to doom. One best practice is to use the HMAC (the next point).

  8. (not enough reputation to post the link): Understand that Encryption is not Authentication. The HMAC can only verify that the message has not been altered. It also verifies that the message was sent by someone possessing your secret key. You may or may not consider that fact to be sufficient authentication.

  9. The HMAC must cover ALL inputs to your decryption function. That means both the encrypted message AND the input vector. If you have some identifier showing which decryption algorithm to use, THAT must also be part of the HMAC calculation. Otherwise, the attacker could change that indicator and mount certain attacks, and your HMAC validation will still appear clean.

  10. The above is the "encrypt-then-HMAC" school of thought. There are opposing schools of thought, notably including Bruce Schneier. Bruce's reasoning is that it's notably difficult to get "encrypt-then-HMAC" right. If Bruce says it's notably difficult, it's notably difficult.

My point is that if you are encrypting and HMAC'ing, in whatever order, FIND OUT what is so difficult to get right, and get concurrence that you got it right.

Rory Alsop
  • 61,367
  • 12
  • 115
  • 320
Edward Barnard
  • 672
  • 6
  • 17
  • 2
    This is an interesting discussion. However, I wasn't looking for principles or concepts or ideas or a design. I was looking for specific code that I can recommend to PHP developers. I already understand the principles and already present the outline of a specific design in the question. It sounds like you might have some of the pieces, but you miss a few of the requirements in the question: item 5 fails to use a slow hash; item 8 doesn't describe how to compute a HMAC in PHP; we need a code snippet for key derivation (deriving multiple independent keys from a passphrase). – D.W. Sep 12 '15 at 03:39
  • 2
    Okay. I'll answer with specific code then :P – Scott Arciszewski Sep 13 '15 at 01:23
  • 1
    @ScottArciszewski I don't have enough rep to comment on Scott's answer. His article https://paragonie.com/blog/2015/09/state-cryptography-in-php points out my limitations: Our production site remains on PHP 5.3 for some time to come. We only use vetted OS and PHP versions from our Linux packager. defuse/php-encryption takes PHP 5.4. Now I have one more reason for getting to at least 5.4. I should NOT be writing my own encryption code! – Edward Barnard Sep 13 '15 at 02:24
1

Write crypto code don't publish it Upon further reflection it's possibly better that I don't expose roll-your-own code as an example. Use libsodium.

Edward Barnard
  • 672
  • 6
  • 17
  • 1
    Thank you for sharing your code! 1. It requires two separate passphrases, which is unrealistic. Instead, you should derive all the keys from a single passphrase: perform a slow hash of the passphrase, then generate your encryption key and authentication key from that (e.g., if *p* is the passphrase and *h* is a slow hash like PBKDF2 or scrypt, set *k = h(p)*, then use *E_k(0),E_k(1)* as the encryption key and *E_k(2),E_k(3)* as the authentication key, where *E* is AES ECB encryption). – D.W. Sep 12 '15 at 23:54
  • 2
    2. Hardcoding the keys in the source code is usually not a good idea. Usually it's better to pass them in as a parameter. Other than that, looks very promising -- thank you! – D.W. Sep 12 '15 at 23:57
  • @D.W., I'd like to better understand what you're saying. 1. Can you give a PHP sample for deriving the two keys? I think I follow you but I'm sketchy on proper terminology. 2. I'm just not seeing the need for a slow hash in these circumstances and I'd like to understand. The primary use of a slow hash, as I understand it, is for something like a large number of stored passwords. With individual salts, each hash must be attacked individually, and the slow hash makes the time factor prohibitive. In this case we are not storing the hashed result so there is nothing to attack. – Edward Barnard Sep 13 '15 at 00:06
  • 1
    1. I'm not very good at PHP, so I hesitate to give a sample as I worry it might be wrong. You could alternatively use [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) and ask for 512 bits of output (dkLen=512), then use the first 256 bits as the encryption key and the last 256 bits as the authentication key. 2. A slow hash is needed for these purposes too, otherwise offline bruteforce attacks on the passphrase are too easy. It's not limited to storing password hashes. Do http://security.stackexchange.com/a/2213/971 and http://security.stackexchange.com/q/15581/971 make it any clearer? – D.W. Sep 13 '15 at 00:19
  • @D.W., I agree with you on keys in source code, for many reasons. I chose to here to provide a complete self-contained example. I'll add a comment to the sample code. – Edward Barnard Sep 13 '15 at 00:44
  • @D.W. Now I see what you mean on slow hash! As it happens I probably had it right but didn't say so. My pass phrases are indeed random bytes of (I hope) sufficient entropy. If I am in fact using 32 full bytes of random noise (as opposed to 7-bit ASCII characters), the sha256 transformation is redundant. I dumbed-down the pass phrases for the example, and misled by calling them "pass phrases." – Edward Barnard Sep 13 '15 at 01:00
  • I'd like to highlight something I wrote in the question: "the solution should ... generate the actual encryption key from the secret S using a suitable slow hash (in case S happens to be a password instead of an actual cryptographic key)." It's about hardening the system against likely failure modes -- where in this case, someone using a pass phrase as the key is an entirely predictable and common failure mode. Your code is helpful in a number of ways, but it doesn't meet this particular requirement. – D.W. Sep 13 '15 at 03:26
  • @D.W. You are correct. I did see that as a requirement but did not and still don't know how to meet it. You noted the same issue with my first "design pattern" answer. Since we're looking for a PHP-specific answer, perhaps I should post that as a separate question. – Edward Barnard Sep 13 '15 at 03:34