.. Source: Paragon Initiative Enterprises, LLC. URL: https://paragonie.com/b/EWdTt51DxzJTchq9 Format: ReStructuredText Created: 2015-05-15T07:05:34-04:00 Modified: 2017-08-15T20:45:33-04:00 Accessed 2024-04-26T10:33:31-04:00 .. _deploying your own cryptography: http://www.cryptofails.com/post/75204435608/write-crypto-code-dont-publish-it .. _encryption is not authentication: https://paragonie.com/blog/2015/05/using-encryption-and-authentication-correctly .. _PECL libsodium: https://github.com/jedisct1/libsodium-php .. _defuse/php-encryption: https://github.com/defuse/php-encryption .. _libmcrypt: http://sourceforge.net/projects/mcrypt/files/Libmcrypt .. _plenty of bugs: http://sourceforge.net/p/mcrypt/bugs .. _patches available: http://sourceforge.net/p/mcrypt/patches .. _list of mcrypt ciphers: https://php.net/manual/en/mcrypt.ciphers.php .. _AES-256 has much worse key scheduling than AES-128: https://eprint.iacr.org/2009/317 .. _not authenticating your ciphertexts: http://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken .. _padding oracle attacks: http://robertheaton.com/2013/07/29/padding-oracle-attack .. _Encrypt then MAC: http://www.thoughtcrime.org/blog/the-cryptographic-doom-principle .. _available here: http://3v4l.org/fAt72 .. _PKCS7: https://en.wikipedia.org/wiki/Padding_%28cryptography%29#PKCS7 .. _AES-NI: https://en.wikipedia.org/wiki/AES_instruction_set .. _CSPRNG: https://php.net/openssl_random_pseudo_bytes .. _Thomas Ptacek: http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers .. _LibreSSL: http://www.libressl.org .. _use libsodium: https://paragonie.com/blog/2015/09/how-to-safely-implement-cryptography-in-any-application **Foreword**: You probably should not be `deploying your own cryptography`_ to begin with, especially if you don't already understand that `encryption is not authentication`_. For production systems, use `PECL libsodium`_ or `defuse/php-encryption`_ and save yourself the headache. The rest of this post is intended for PHP developers who still want to write their own cryptography code, or already have. Top 3 Reasons to Avoid Mcrypt ============================= I. Mcrypt is Abandonware ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PHP's optional ``mcrypt`` extension provides bindings for a cryptography library called `libmcrypt`_, which has been collecting dust since 2007 (eight years and counting) despite `plenty of bugs`_, some which even have `patches available`_. If bit rot weren't enough reason to avoid using this library, the major design flaws which make it easier to write insecure code than it is to write secure code should. II. It's Confusing and Counter-Intuitive ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Look at this `list of mcrypt ciphers`_ and tell me how you would implement ``AES-256-CBC``. If your code looks like this, you've just run headfirst into the first (and arguably most common) mcrypt design wart: .. code-block:: php function encryptOnly($plaintext, $key) { $iv = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); $ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $plaintext, MCRYPT_MODE_CBC, $iv); return $iv.$ciphertext; } **Surprise! ``MCRYPT_RIJNDAEL_256`` doesn't mean ``AES-256``.** All variants of AES use a 128-bit block size with varying key lengths (128, 192, or 256). This means that ``MCRYPT_RIJNDAEL_128`` is the only correct choice if you want AES. ``MCRYPT_RIJNDAEL_192`` and ``MCRYPT_RIJNDAEL_256`` instead refer to non-standard, less-studied variants of the Rijndael block cipher that operate on larger blocks. Considering that `AES-256 has much worse key scheduling than AES-128`_, it's not at all unreasonable to suspect there might be unknown weaknesses in the non-standard Rijndael variants that are not present in the standardized 128-bit block size version of the algorithm. At the very least, it makes interoperability with other encryption libraries that only implement AES a challenge. Isn't it great that mcrypt makes you feel dumb for not knowing details that you probably shouldn't really *need* to know? Don't worry, it gets worse. III. Null Padding ~~~~~~~~~~~~~~~~~~~~~~~~ We already stated that `not authenticating your ciphertexts`_ is a bad idea, and in all fairness, `padding oracle attacks`_ are going to be a problem in CBC (Cipher Block Chaining) mode no matter what padding scheme you select if you fail to `Encrypt then MAC`_. If you encrypt your message with ``mcrypt_encrypt()``, you have to choose between writing your own plaintext padding strategy or using the one mcrypt implements by default: zero-padding. To see why zero-padding sucks, let's encrypt then decrypt a binary string in AES-128-CBC using mcrypt (The result of running this code is `available here`_): .. code-block:: php $key = hex2bin('000102030405060708090a0b0c0d0e0f'); $message = hex2bin('5061726101676f6e000300'); $iv = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $message, MCRYPT_MODE_CBC, $iv); $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $encrypted, MCRYPT_MODE_CBC, $iv); // This should still be padded: var_dump(bin2hex($decrypted)); // Let's strip off the padding: $stripped = rtrim($decrypted, "\0"); var_dump(bin2hex($stripped)); // Does this equal the original message? var_dump($stripped === $message); As you can see, padding a plaintext with zero bytes can lead to a loss of data. A much safer alternative is to use `PKCS7`_ padding. OpenSSL Does It Better ====================== Here is an example of an unauthenticated AES-256-CBC encryption library written in Mcrypt with PKCS7 padding. .. code-block:: php /** * This library is unsafe because it does not MAC after encrypting */ class UnsafeMcryptAES { const CIPHER = MCRYPT_RIJNDAEL_128; public static function encrypt($message, $key) { if (mb_strlen($key, '8bit') !== 32) { throw new Exception("Needs a 256-bit key!"); } $ivsize = mcrypt_get_iv_size(self::CIPHER); $iv = mcrypt_create_iv($ivsize, MCRYPT_DEV_URANDOM); // Add PKCS7 Padding $block = mcrypt_get_block_size(self::CIPHER); $pad = $block - (mb_strlen($message, '8bit') % $block, '8bit'); $message .= str_repeat(chr($pad), $pad); $ciphertext = mcrypt_encrypt( MCRYPT_RIJNDAEL_128, $key, $message, MCRYPT_MODE_CBC, $iv ); return $iv . $ciphertext; } public static function decrypt($message, $key) { if (mb_strlen($key, '8bit') !== 32) { throw new Exception("Needs a 256-bit key!"); } $ivsize = mcrypt_get_iv_size(self::CIPHER); $iv = mb_substr($message, 0, $ivsize, '8bit'); $ciphertext = mb_substr($message, $ivsize, null, '8bit'); $plaintext = mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $key, $ciphertext, MCRYPT_MODE_CBC, $iv ); $len = mb_strlen($plaintext, '8bit'); $pad = ord($plaintext[$len - 1]); if ($pad <= 0 || $pad > $block) { // Padding error! return false; } return mb_substr($plaintext, 0, $len - $pad, '8bit'); } } And here's the library written using OpenSSL. .. code-block:: php /** * This library is unsafe because it does not MAC after encrypting */ class UnsafeOpensslAES { const METHOD = 'aes-256-cbc'; public static function encrypt($message, $key) { if (mb_strlen($key, '8bit') !== 32) { throw new Exception("Needs a 256-bit key!"); } $ivsize = openssl_cipher_iv_length(self::METHOD); $iv = openssl_random_pseudo_bytes($ivsize); $ciphertext = openssl_encrypt( $message, self::METHOD, $key, OPENSSL_RAW_DATA, $iv ); return $iv . $ciphertext; } public static function decrypt($message, $key) { if (mb_strlen($key, '8bit') !== 32) { throw new Exception("Needs a 256-bit key!"); } $ivsize = openssl_cipher_iv_length(self::METHOD); $iv = mb_substr($message, 0, $ivsize, '8bit'); $ciphertext = mb_substr($message, $ivsize, null, '8bit'); return openssl_decrypt( $ciphertext, self::METHOD, $key, OPENSSL_RAW_DATA, $iv ); } } In almost every metric, openssl wins over mcrypt: 1. Specifying ``'aes-256-cbc'`` is much more obvious than remembering to use ``MCRYPT_RIJNDAEL_128`` with a 32-byte binary key. 2. ``openssl_encrypt()`` performs PKCS7 padding by default, and lets you specify ``OPENSSL_ZERO_PADDING`` if you really want it. 3. The code you write ends up much more compact and readable, with less room for implementation errors. 4. It performs AES encryption/decryption much faster, since it supports `AES-NI`_ if your processor has this feature. ``AES-NI`` also means you don't have to worry about an attacker recovering your secret key from cache-timing information. 5. **OpenSSL is being actively developed and maintained.** In response of the Heartbleed vulnerability last year, several organizations (including the Linux Foundation) declared the project critical Internet infrastructure and began pouring resources into finding and fixing bugs in the system. If you still don't trust it, there's always `LibreSSL`_. Simplicity, security, and performance. What more is there to ask for? There are, however, two things with OpenSSL that you should watch out for. OpenSSL Gotchas ~~~~~~~~~~~~~~~ 1. The `CSPRNG`_ they offer is a userspace PRNG based on hash functions, which goes against the advice of `Thomas Ptacek`_ to use ``/dev/urandom``. The only one-liner alternative is ``mcrypt_create_iv()``, as demonstrated above, but this function is only exposed if you enable the ``mcrypt`` extension. Fortunately, PHP 7 will offer a core ``random_bytes()`` function that leverages the kernel's CSPRNG. 2. Although your version of OpenSSL might list GCM based cipher modes (e.g. ``aes-128-gcm``), PHP doesn't actually support these methods yet. In Sum ====== **Don't use ``mcrypt``.** If you're typing the word ``mcrypt`` into your code, you're probably making a mistake. Although it's possible to provide a relatively secure cryptography library that builds on top of ``mcrypt`` (the earlier version of `defuse/php-encryption`_ did), switching your code to ``openssl`` will provide better security, performance, maintainability, and portability. Even better: `use libsodium`_ instead.