mcrypt is deprecated, what is the alternative?

Solution 1:

It's best practice to hash passwords so they are not decryptable. This makes things slightly more difficult for attackers that may have gained access to your database or files.

If you must encrypt your data and have it decryptable, a guide to secure encryption/decryption is available at https://paragonie.com/white-paper/2015-secure-php-data-encryption. To summarize that link:

  • Use Libsodium - A PHP extension
  • If you can't use Libsodium, use defuse/php-encryption - Straight PHP code
  • If you can't use Libsodium or defuse/php-encryption, use OpenSSL - A lot of servers will already have this installed. If not, it can be compiled with --with-openssl[=DIR]

Solution 2:

As suggested by @rqLizard, you can use openssl_encrypt/openssl_decrypt PHP functions instead which provides a much better alternative to implement AES (The Advanced Encryption Standard) also known as Rijndael encryption.

As per the following Scott's comment at php.net:

If you're writing code to encrypt/encrypt data in 2015, you should use openssl_encrypt() and openssl_decrypt(). The underlying library (libmcrypt) has been abandoned since 2007, and performs far worse than OpenSSL (which leverages AES-NI on modern processors and is cache-timing safe).

Also, MCRYPT_RIJNDAEL_256 is not AES-256, it's a different variant of the Rijndael block cipher. If you want AES-256 in mcrypt, you have to use MCRYPT_RIJNDAEL_128 with a 32-byte key. OpenSSL makes it more obvious which mode you are using (i.e. aes-128-cbc vs aes-256-ctr).

OpenSSL also uses PKCS7 padding with CBC mode rather than mcrypt's NULL byte padding. Thus, mcrypt is more likely to make your code vulnerable to padding oracle attacks than OpenSSL.

Finally, if you are not authenticating your ciphertexts (Encrypt Then MAC), you're doing it wrong.

Further reading:

  • Using Encryption and Authentication Correctly (for PHP developers).
  • If You're Typing the Word MCRYPT Into Your PHP Code, You're Doing It Wrong.

Code examples

Example #1

AES Authenticated Encryption in GCM mode example for PHP 7.1+

<?php
//$key should have been previously generated in a cryptographically safe way, like openssl_random_pseudo_bytes
$plaintext = "message to be encrypted";
$cipher = "aes-128-gcm";
if (in_array($cipher, openssl_get_cipher_methods()))
{
    $ivlen = openssl_cipher_iv_length($cipher);
    $iv = openssl_random_pseudo_bytes($ivlen);
    $ciphertext = openssl_encrypt($plaintext, $cipher, $key, $options=0, $iv, $tag);
    //store $cipher, $iv, and $tag for decryption later
    $original_plaintext = openssl_decrypt($ciphertext, $cipher, $key, $options=0, $iv, $tag);
    echo $original_plaintext."\n";
}
?>

Example #2

AES Authenticated Encryption example for PHP 5.6+

<?php
//$key previously generated safely, ie: openssl_random_pseudo_bytes
$plaintext = "message to be encrypted";
$ivlen = openssl_cipher_iv_length($cipher="AES-128-CBC");
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext_raw = openssl_encrypt($plaintext, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv);
$hmac = hash_hmac('sha256', $ciphertext_raw, $key, $as_binary=true);
$ciphertext = base64_encode( $iv.$hmac.$ciphertext_raw );

//decrypt later....
$c = base64_decode($ciphertext);
$ivlen = openssl_cipher_iv_length($cipher="AES-128-CBC");
$iv = substr($c, 0, $ivlen);
$hmac = substr($c, $ivlen, $sha2len=32);
$ciphertext_raw = substr($c, $ivlen+$sha2len);
$original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv);
$calcmac = hash_hmac('sha256', $ciphertext_raw, $key, $as_binary=true);
if (hash_equals($hmac, $calcmac))//PHP 5.6+ timing attack safe comparison
{
    echo $original_plaintext."\n";
}
?>

Example #3

Based on above examples, I've changed the following code which aims at encrypting user's session id:

class Session {

  /**
   * Encrypts the session ID and returns it as a base 64 encoded string.
   *
   * @param $session_id
   * @return string
   */
  public function encrypt($session_id) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Encrypt the session ID.
    $encrypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $session_id, MCRYPT_MODE_CBC, $iv);
    // Base 64 encode the encrypted session ID.
    $encryptedSessionId = base64_encode($encrypt);
    // Return it.
    return $encryptedSessionId;
  }

  /**
   * Decrypts a base 64 encoded encrypted session ID back to its original form.
   *
   * @param $encryptedSessionId
   * @return string
   */
  public function decrypt($encryptedSessionId) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Decode the encrypted session ID from base 64.
    $decoded = base64_decode($encryptedSessionId);
    // Decrypt the string.
    $decryptedSessionId = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $decoded, MCRYPT_MODE_CBC, $iv);
    // Trim the whitespace from the end.
    $session_id = rtrim($decryptedSessionId, "\0");
    // Return it.
    return $session_id;
  }

  public function _getIv() {
    return md5($this->_getSalt());
  }

  public function _getSalt() {
    return md5($this->drupal->drupalGetHashSalt());
  }

}

into:

class Session {

  const SESS_CIPHER = 'aes-128-cbc';

  /**
   * Encrypts the session ID and returns it as a base 64 encoded string.
   *
   * @param $session_id
   * @return string
   */
  public function encrypt($session_id) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Encrypt the session ID.
    $ciphertext = openssl_encrypt($session_id, self::SESS_CIPHER, $key, $options=OPENSSL_RAW_DATA, $iv);
    // Base 64 encode the encrypted session ID.
    $encryptedSessionId = base64_encode($ciphertext);
    // Return it.
    return $encryptedSessionId;
  }

  /**
   * Decrypts a base 64 encoded encrypted session ID back to its original form.
   *
   * @param $encryptedSessionId
   * @return string
   */
  public function decrypt($encryptedSessionId) {
    // Get the Drupal hash salt as a key.
    $key = $this->_getSalt();
    // Get the iv.
    $iv = $this->_getIv();
    // Decode the encrypted session ID from base 64.
    $decoded = base64_decode($encryptedSessionId, TRUE);
    // Decrypt the string.
    $decryptedSessionId = openssl_decrypt($decoded, self::SESS_CIPHER, $key, $options=OPENSSL_RAW_DATA, $iv);
    // Trim the whitespace from the end.
    $session_id = rtrim($decryptedSessionId, '\0');
    // Return it.
    return $session_id;
  }

  public function _getIv() {
    $ivlen = openssl_cipher_iv_length(self::SESS_CIPHER);
    return substr(md5($this->_getSalt()), 0, $ivlen);
  }

  public function _getSalt() {
    return $this->drupal->drupalGetHashSalt();
  }

}

To clarify, above change is not a true conversion since the two encryption uses a different block size and a different encrypted data. Additionally, the default padding is different, MCRYPT_RIJNDAEL only supports non-standard null padding. @zaph


Additional notes (from the @zaph's comments):

  • Rijndael 128 (MCRYPT_RIJNDAEL_128) is equivalent to AES, however Rijndael 256 (MCRYPT_RIJNDAEL_256) is not AES-256 as the 256 specifies a block size of 256-bits, whereas AES has only one block size: 128-bits. So basically Rijndael with a block size of 256-bits (MCRYPT_RIJNDAEL_256) has been mistakenly named due to the choices by the mcrypt developers. @zaph
  • Rijndael with a block size of 256 may be less secure than with a block size of 128-bits because the latter has had much more reviews and uses. Secondly, interoperability is hindered in that while AES is generally available, where Rijndael with a block size of 256-bits is not.
  • Encryption with different block sizes for Rijndael produces different encrypted data.

    For example, MCRYPT_RIJNDAEL_256 (not equivalent to AES-256) defines a different variant of the Rijndael block cipher with size of 256-bits and a key size based on the passed in key, where aes-256-cbc is Rijndael with a block size of 128-bits with a key size of 256-bits. Therefore they're using different block sizes which produces entirely different encrypted data as mcrypt uses the number to specify the block size, where OpenSSL used the number to specify the key size (AES only has one block size of 128-bits). So basically AES is Rijndael with a block size of 128-bits and key sizes of 128, 192 and 256 bits. Therefore it's better to use AES, which is called Rijndael 128 in OpenSSL.