.. Source: Paragon Initiative Enterprises, LLC. URL: https://paragonie.com/b/FmKm92tOMhEosukg Format: ReStructuredText Created: 2015-05-04T19:56:49-04:00 Modified: 2017-08-15T20:45:28-04:00 Accessed 2024-04-26T17:45:31-04:00 .. _Crypto Fails: http://www.cryptofails.com/archive .. _CIA triad: http://whatis.techtarget.com/definition/Confidentiality-integrity-and-availability-CIA .. _PHP Cryptography: https://paragonie.com/white-paper/2015-secure-php-data-encryption .. _The Cryptographic Doom Principle: http://www.thoughtcrime.org/blog/the-cryptographic-doom-principle/ .. _Timing Attacks: http://blog.astrumfutura.com/2010/10/nanosecond-scale-remote-timing-attacks-on-php-applications-time-to-take-them-seriously/ .. _Chosen Prefix Attacks on MD5: https://www.iacr.org/conferences/eurocrypt2007/slides/s01t1.pdf .. _Non-strict equality operator bugs: http://3v4l.org/tT4l8 .. _defuse/php-encryption: https://github.com/defuse/php-encryption .. _Galois/Counter Mode: https://en.wikipedia.org/wiki/Galois/Counter_Mode .. _ChaCha20: http://cr.yp.to/chacha.html .. _Poly1305: http://cr.yp.to/mac.html .. _libsodium: https://pecl.php.net/package/libsodium .. _secure cookie: https://gist.github.com/wopot/94e33bdd1d7faaaa56e2 .. _openssl, not mcrypt: https://paragonie.com/blog/2015/05/if-you-re-typing-word-mcrypt-into-your-code-you-re-doing-it-wrong .. _industry recommended best practices: http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html .. _Halite: https://github.com/paragonie/halite .. _CAESAR: http://competitions.cr.yp.to/caesar.html .. _Cryptography Decoded: Terms and Concepts for the Average Developer: https://paragonie.com/blog/2015/08/you-wouldnt-base64-a-password-cryptography-decoded .. _How to Safely Implement Cryptography Features in any Application: https://paragonie.com/blog/2015/09/how-to-safely-implement-cryptography-in-any-application .. _Choosing the Right Cryptography Library for your PHP Project: https://paragonie.com/blog/2015/11/choosing-right-cryptography-library-for-your-php-project-guide "Encryption is not authentication" is common wisdom among cryptography experts, but it is only rarely whispered among developers whom aren't also cryptography experts. This is unfortunate; a lot of design mistakes could be avoided if this information were more widely known and deeply understood. (These mistakes are painfully common in home-grown PHP cryptography classes and functions, as many of the posts on `Crypto Fails`_ demonstrates.) The concept itself is not difficult, but there is a rich supply of detail and nuance to be found beneath the surface. What's the Difference Between Encryption and Authentication? ============================================================ **Encryption** is the process of rendering a message such that it becomes unreadable without possessing the correct key. In the simple case of symmetric cryptography, the same key is used for encryption as is used for decryption. In asymmetric cryptography, it is possible to encrypt a message with a user's *public key* such that only possessing their *private key* can read it. Our white paper on `PHP cryptography`_ covers anonymous public-key encryption. **Authentication** is the process of rendering a message tamper-resistant (typically within a certain very low probability, typically less than 1 divided by the number of particles in the known universe) while also proving it originated from the expected sender. Note: When we say *authenticity*, we mean specifically *message authenticity*, not *identity authenticity*. That is a PKI and key management problem, which we may address in a future blog post. In respect to the `CIA triad`_: Encryption provides confidentiality. Authentication provides integrity. **Encryption does not provide integrity**; a tampered message can (usually) still decrypt, but the result will usually be garbage. Encryption alone also does not inhibit malicious third parties from sending encrypted messages. **Authentication does not provide confidentiality**; it is possible to provide tamper-resistance to a plaintext message. A common mistake among programmers is to confuse the two. It is not uncommon to find a PHP library or framework that encrypts cookie data and then trusts it wholesale after merely decrypting it. Message encryption without message authentication is a bad idea. Cryptography expert Moxie Marlinspike wrote about why message authentication matters (as well as the **correct** order of operations) in what he dubbed, `The Cryptographic Doom Principle`_. Encryption ========== We previously defined encryption and specified that it provides confidentiality but not integrity or authenticity. You can tamper with an encrypted message and give the recipient garbage. But what if you could use this garbage-generating mechanism to bypass a security control? Consider the case of encrypted cookies. .. code-block:: php function setUnsafeCookie($name, $cookieData, $key) { $iv = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); return setcookie( $name, base64_encode( $iv. mcrypt_encrypt( MCRYPT_RIJNDAEL_128, $key, json_encode($cookieData), MCRYPT_MODE_CBC, $iv ) ) ); } function getUnsafeCookie($name, $key) { if (!isset($_COOKIE[$name])) { return null; } $decoded = base64_decode($_COOKIE[$name]); $iv = mb_substr($decoded, 0, 16, '8bit'); $ciphertext = mb_substr($decoded, 16, null, '8bit'); $decrypted = rtrim( mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $key, $ciphertext, MCRYPT_MODE_CBC, $iv ), "\0" ); return json_decode($decrypted, true); } The above code provides AES encryption in Cipher-Block-Chaining mode. If you pass a 32-byte string for ``$key``, you can even claim to provide 256-bit AES encryption for your cookies and people might be misled into believing it's secure. How to Attack Unauthenticated Encryption ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Let's say that, after logging into this application, you see that you receive a session cookie that looks like ``kHv9PAlStPZaZJHIYXzyCnuAhWdRRK7H0cNVUCwzCZ4M8fxH79xIIIbznxmiOxGQ7td8LwTzHFgwBmbqWuB+sQ==``. Let's change a byte in the first block (the initialization vector) and iteratively sending our new cookie until something changes. It should take a total of 4096 HTTP requests to attempt all possible one-byte changes to the IV. In our example above, after 2405 requests, we get a string that looks like this: ``kHv9PAlStPZaZZHIYXzyCnuAhWdRRK7H0cNVUCwzCZ4M8fxH79xIIIbznxmiOxGQ7td8LwTzHFgwBmbqWuB+sQ==`` For comparison, only one character differs in the base64-encoded cookie (``kHv9PAlStPZaZ``**``J``** vs ``kHv9PAlStPZaZ``**``Z``**): .. code-block:: diff - kHv9PAlStPZaZJHIYXzyCnuAhWdRRK7H0cNVUCwzCZ4M8fxH79xIIIbznxmiOxGQ7td8LwTzHFgwBmbqWuB+sQ== + kHv9PAlStPZaZZHIYXzyCnuAhWdRRK7H0cNVUCwzCZ4M8fxH79xIIIbznxmiOxGQ7td8LwTzHFgwBmbqWuB+sQ== The original data we stored in this cookie was an array that looked like this: .. code-block:: php array(2) { ["admin"]=> int(0) ["user"]=> "aaaaaaaaaaaaa" } But after merely altering a single byte in the initialization vector, we were able to rewrite our message to read: .. code-block:: php array(2) { ["admin"]=> int(1) ["user"]=> "aaaaaaaaaaaaa" } Depending on how the underlying app is set up, you might be able to flip one bit and become and administrator. **Even though your cookies are encrypted.** If you would like to reproduce our results, our encryption key was ``000102030405060708090a0b0c0d0e0f`` (convert from hexadecimal to raw binary). Authentication ============== As stated above, authentication aims to provide both integrity (by which we mean significant tamper-resistance) to a message, while proving that it came from the expected source (authenticity). The typical way this is done is to calculate a keyed-**H**ash **M**essage **A**uthentication **C**ode (HMAC for short) for the message and concatenate it with the message. .. code-block:: php function hmac_sign($message, $key) { return hash_hmac('sha256', $message, $key) . $message; } function hmac_verify($bundle, $key) { $msgMAC = mb_substr($bundle, 0, 64, '8bit'); $message = mb_substr($bundle, 64, null, '8bit'); return hash_equals( hash_hmac('sha256', $message, $key), $msgMAC ); } It is important that an appropriate cryptographic tool such as HMAC is used here and not just a simple hash function. .. code-block:: php function unsafe_hash_sign($message, $key) { return md5($key.$message) . $message; } function unsafe_hash_verify($bundle, $key) { $msgHash = mb_substr($bundle, 0, 64, '8bit'); $message = mb_substr($bundle, 64, null, '8bit'); return md5($key.$message) == $msgHash; } These two functions are prefixed with **unsafe** because they are vulnerable to a number of flaws: 1. `Timing Attacks`_ 2. `Chosen Prefix Attacks on MD5`_ (PDF) 3. `Non-strict equality operator bugs`_ (largely specific to PHP) To authenticate a message, you always want some sort of keyed Message Authentication Code rather than just a hash with a key. Using a hash without a key is even worse. While a hash function can provide simple message integrity, **any attacker can calculate a simple checksum or non-keyed hash of their forged message.** Well-designed MACs require the attacker to know the authentication key to forge a message. Simple integrity without authenticity (e.g. a checksum or a simple unkeyed hash) is insufficient for providing secure communications. In cryptography, if a message is not authenticated, it offers *no integrity guarantees* either. **Message Authentication gives you Message Integrity for free.** Authenticated Encryption ======================== The only surefire way to prevent bit-rewriting attacks is to make sure that, after encrypting your information, you authenticate the encrypted message. **This detail is very important!** Encrypt *then* authenticate. Verify *before* decryption. Let's revisit our encrypted cookie example, but make it a little safer. Let's also switch to CTR mode, in accordance with `industry recommended best practices`_. Note that the encryption key and authentication key are different. .. code-block:: php function setLessUnsafeCookie($name, $cookieData, $eKey, $aKey) { $iv = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); $ciphertext = mcrypt_encrypt( MCRYPT_RIJNDAEL_128, $eKey, json_encode($cookieData), 'ctr', $iv ); // Note: We cover the IV in our HMAC $hmac = hash_hmac('sha256', $iv.$ciphertext, $aKey, true); return setcookie( $name, base64_encode( $hmac.$iv.$ciphertext ) ); } function getLessUnsafeCookie($name, $eKey, $aKey) { if (!isset($_COOKIE[$name])) { return null; } $decoded = base64_decode($_COOKIE[$name]); $hmac = mb_substr($decoded, 0, 32, '8bit'); $iv = mb_substr($decoded, 32, 16, '8bit'); $ciphertext = mb_substr($decoded, 48, null, '8bit'); $calculated = hash_hmac('sha256', $iv.$ciphertext, $aKey, true); if (hash_equals($hmac, $calculated)) { $decrypted = rtrim( mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $eKey, $ciphertext, 'ctr', $iv ), "\0" ); return json_decode($decrypted, true); } } Now we're a little closer to our goal of robust symmetric authenticated encryption. There are still a few more questions left to answer, such as: * What happens if our original message ends in null bytes? * Is there a better padding strategy than the one ``mcrypt`` uses by default? * What side-channels are exposed by the AES implementation? Fortunately, these questions are already answered in existing cryptography libraries. **We highly recommend using an existing library** instead of writing your own encryption features. For PHP developers, you should use `defuse/php-encryption`_ (or `libsodium`_ if it's available for you). If you still believe you should write your own, consider using `openssl, not mcrypt`_. Note: There is a narrow band of use-cases where authenticated encryption is either impractical (e.g. software-driven full disk encryption) or unnecessary (i.e. the data is never sent over the network, even by folder synchronization services such as Dropbox). If you suspect your problems or goals permit unauthenticated ciphertext, consult a professional cryptographer, because this is not a typical use-case. Secure Encrypted Cookies with Libsodium --------------------------------------- If you wish to implement encrypted cookies in one of your projects, check out `Halite`_. It has a cookie class dedicated to this use case. .. code-block:: php fetch('data'); // Then do some stuff: $cookie->store('index', $values); If you want to reinvent this wheel yourself, you can always do something like this: .. code-block:: php /* // At some point, we run this command: $key = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_KEYBYTES); */ /** * Store ciphertext in a cookie * * @param string $name - cookie name * @param mixed $cookieData - cookie data * @param string $key - crypto key */ function setSafeCookie($name, $cookieData, $key) { $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); return setcookie( $name, base64_encode( $nonce. \Sodium\crypto_secretbox( json_encode($cookieData), $nonce, $key ) ) ); } /** * Decrypt a cookie, expand to array * * @param string $name - cookie name * @param string $key - crypto key */ function getSafeCookie($name, $key) { if (!isset($_COOKIE[$name])) { return array(); } $decoded = base64_decode($_COOKIE[$name]); $nonce = mb_substr($decoded, 0, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($decoded, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); $decrypted = \Sodium\crypto_secretbox_open( $ciphertext, $nonce, $key ); if (empty($decrypted)) { return array(); } return json_decode($decrypted, true); } For developers without access to libsodium (i.e. you aren't allowed to install PHP extensions through PECL in production), one of our blog readers offered an example `secure cookie`_ implementation that uses ``defuse/php-encryption`` (the PHP library we recommend). Authenticated Encryption with Associated Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In our previous examples, we focused on building the encryption and authentication as separate components that must be used with care to avoid cryptographic doom. Specifically, we focused on AES in Cipher Block-Chaining mode (and more recently in Counter mode). However, cryptographers have developed newer, more resilient modes of encryption that encrypt and authenticate a message in the same operation. These modes are called AEAD modes (Authenticated Encryption with Associated Data). **Associated Data** means whatever your application needs to authenticate, but not to encrypt. AEAD modes are typically intended for stateful purposes, e.g. network communications where a nonce can be managed easily. Two reliable implementations of AEAD are **AES-GCM** and **ChaCha20-Poly1305**. * ``AES-GCM`` is the Advanced Encryption Standard (a.k.a. Rijndael cipher) in `Galois/Counter Mode`_. This mode is available in the latest versions of openssl, but **it is currently not supported in PHP**. * ``ChaCha20-Poly1305`` combines the `ChaCha20`_ stream cipher with the `Poly1305`_ Message Authentication Code. **This mode is available in the `libsodium`_ PHP extension.** * ``\Sodium\crypto_aead_chacha20poly1305_encrypt()`` and ``\Sodium\crypto_aead_chacha20poly1305_decrypt()`` In a few years, we anticipate the `CAESAR`_ competition will produce a next-generation authenticated encryption mode that we can recommend over these two. Take-Away ========= * Encryption is not authentication. * Encryption provides confidentiality. * Authentication provides integrity. * **Confuse the two at your own peril.** * To complete the `CIA triad`_, you need to solve Availability separately. This is not usually a cryptography problem. And most importantly: Use a library with a proven record of resilience under the scrutiny of cryptography experts rather than hacking something together on your own. You'll be much better off for it. Further Reading =============== * `Cryptography Decoded: Terms and Concepts for the Average Developer`_ * `How to Safely Implement Cryptography Features in any Application`_ (not just PHP) * `Choosing the Right Cryptography Library for your PHP Project`_