21

I want to make an encryption function that should have some secret key. Something like the following:

function encrypt($string) {
    $key = "mastermind";
    $enc = encryptfunc($string, $key);

    return $enc;
}

The same thing should apply for decryption.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 2
    You probably shouldn't roll your own security/encryption functions unless you really know what your doing (or learning/experimenting). – Matthew Nov 24 '09 at 06:51

5 Answers5

34

Here is simple, but secure implementation of AES-256 encryption in CBC mode that uses PBKDF2 to create encryption key out of plain-text password and HMAC to authenticate the encrypted message.

It works with PHP 5.3 and higher.

/**
 * Implements AES-256 encryption/decryption in CBC mode.
 *
 * PBKDF2 is used for creation of encryption key.
 * HMAC is used to authenticate the encrypted message.
 *
 * Requires PHP 5.3 and higher
 *
 * Gist: https://gist.github.com/eugef/3d44b2e0a8a891432c65
 */
class McryptCipher
{
    const PBKDF2_HASH_ALGORITHM = 'SHA256';
    const PBKDF2_ITERATIONS = 64000;
    const PBKDF2_SALT_BYTE_SIZE = 32;
    // 32 is the maximum supported key size for the MCRYPT_RIJNDAEL_128
    const PBKDF2_HASH_BYTE_SIZE = 32;

    /**
     * @var string
     */
    private $password;

    /**
     * @var string
     */
    private $secureEncryptionKey;

    /**
     * @var string
     */
    private $secureHMACKey;

    /**
     * @var string
     */
    private $pbkdf2Salt;

    public function __construct($password)
    {
        $this->password = $password;
    }

    /**
     * Compares two strings.
     *
     * This method implements a constant-time algorithm to compare strings.
     * Regardless of the used implementation, it will leak length information.
     *
     * @param string $knownHash The string of known length to compare against
     * @param string $userHash   The string that the user can control
     *
     * @return bool true if the two strings are the same, false otherwise
     *
     * @see https://github.com/symfony/security-core/blob/master/Util/StringUtils.php
     */
    private function equalHashes($knownHash, $userHash)
    {
        if (function_exists('hash_equals')) {
            return hash_equals($knownHash, $userHash);
        }

        $knownLen = strlen($knownHash);
        $userLen = strlen($userHash);

        if ($userLen !== $knownLen) {
            return false;
        }

        $result = 0;
        for ($i = 0; $i < $knownLen; $i++) {
            $result |= (ord($knownHash[$i]) ^ ord($userHash[$i]));
        }

        // They are only identical strings if $result is exactly 0...
        return 0 === $result;
    }

    /**
     * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
     *
     * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
     * This implementation of PBKDF2 was originally created by https://defuse.ca
     * With improvements by http://www.variations-of-shadow.com
     *
     * @param string $algorithm The hash algorithm to use. Recommended: SHA256
     * @param string $password The password
     * @param string $salt A salt that is unique to the password
     * @param int $count Iteration count. Higher is better, but slower. Recommended: At least 1000
     * @param int $key_length The length of the derived key in bytes
     * @param bool $raw_output If true, the key is returned in raw binary format. Hex encoded otherwise
     * @return string A $key_length-byte key derived from the password and salt
     *
     * @see https://defuse.ca/php-pbkdf2.htm
     */
    private function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
    {
        $algorithm = strtolower($algorithm);
        if (!in_array($algorithm, hash_algos(), true)) {
            trigger_error('PBKDF2 ERROR: Invalid hash algorithm.', E_USER_ERROR);
        }
        if ($count <= 0 || $key_length <= 0) {
            trigger_error('PBKDF2 ERROR: Invalid parameters.', E_USER_ERROR);
        }

        if (function_exists('hash_pbkdf2')) {
            // The output length is in NIBBLES (4-bits) if $raw_output is false!
            if (!$raw_output) {
                $key_length *= 2;
            }
            return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
        }

        $hash_length = strlen(hash($algorithm, '', true));
        $block_count = ceil($key_length / $hash_length);

        $output = '';
        for ($i = 1; $i <= $block_count; $i++) {
            // $i encoded as 4 bytes, big endian.
            $last = $salt . pack('N', $i);
            // first iteration
            $last = $xorsum = hash_hmac($algorithm, $last, $password, true);
            // perform the other $count - 1 iterations
            for ($j = 1; $j < $count; $j++) {
                $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
            }
            $output .= $xorsum;
        }

        if ($raw_output) {
            return substr($output, 0, $key_length);
        } else {
            return bin2hex(substr($output, 0, $key_length));
        }
    }

    /**
     * Creates secure PBKDF2 derivatives out of the password.
     *
     * @param null $pbkdf2Salt
     */
    private function derivateSecureKeys($pbkdf2Salt = null)
    {
        if ($pbkdf2Salt) {
            $this->pbkdf2Salt = $pbkdf2Salt;
        }
        else {
            $this->pbkdf2Salt = mcrypt_create_iv(self::PBKDF2_SALT_BYTE_SIZE, MCRYPT_DEV_URANDOM);
        }

        list($this->secureEncryptionKey, $this->secureHMACKey) = str_split(
            $this->pbkdf2(self::PBKDF2_HASH_ALGORITHM, $this->password, $this->pbkdf2Salt, self::PBKDF2_ITERATIONS, self::PBKDF2_HASH_BYTE_SIZE * 2, true),
            self::PBKDF2_HASH_BYTE_SIZE
        );
    }

    /**
     * Calculates HMAC for the message.
     *
     * @param string $message
     * @return string
     */
    private function hmac($message)
    {
        return hash_hmac(self::PBKDF2_HASH_ALGORITHM, $message, $this->secureHMACKey, true);
    }

    /**
     * Encrypts the input text
     *
     * @param string $input
     * @return string Format: hmac:pbkdf2Salt:iv:encryptedText
     */
    public function encrypt($input)
    {
        $this->derivateSecureKeys();

        $mcryptIvSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);

        // By default mcrypt_create_iv() function uses /dev/random as a source of random values.
        // If server has low entropy this source could be very slow.
        // That is why here /dev/urandom is used.
        $iv = mcrypt_create_iv($mcryptIvSize, MCRYPT_DEV_URANDOM);

        $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $this->secureEncryptionKey, $input, MCRYPT_MODE_CBC, $iv);

        $hmac = $this->hmac($this->pbkdf2Salt . $iv . $encrypted);

        return implode(':', array(
            base64_encode($hmac),
            base64_encode($this->pbkdf2Salt),
            base64_encode($iv),
            base64_encode($encrypted)
        ));
    }

    /**
     * Decrypts the input text.
     *
     * @param string $input Format: hmac:pbkdf2Salt:iv:encryptedText
     * @return string
     */
    public function decrypt($input)
    {
        list($hmac, $pbkdf2Salt, $iv, $encrypted) = explode(':', $input);

        $hmac = base64_decode($hmac);
        $pbkdf2Salt = base64_decode($pbkdf2Salt);
        $iv = base64_decode($iv);
        $encrypted = base64_decode($encrypted);

        $this->derivateSecureKeys($pbkdf2Salt);

        $calculatedHmac = $this->hmac($pbkdf2Salt . $iv . $encrypted);

        if (!$this->equalHashes($calculatedHmac, $hmac)) {
            trigger_error('HMAC ERROR: Invalid HMAC.', E_USER_ERROR);
        }

        // mcrypt_decrypt() pads the *RETURN STRING* with nulls ('\0') to fill out to n * blocksize.
        // rtrim() is used to delete them.
        return rtrim(
            mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $this->secureEncryptionKey, $encrypted, MCRYPT_MODE_CBC, $iv),
            "\0"
        );
    }
}

Usage:

$c = new McryptCipher('secret key goes here');
$encrypted = $c->encrypt('secret message');

$decrypted = $c->decrypt($encrypted);

Notice about performance

By default mcrypt_create_iv() function uses /dev/random as a source of random values. If server has low entropy this source could be very slow. This is why /dev/urandom is used.

Here is a good explanation what is the difference between them http://www.onkarjoshi.com/blog/191/device-dev-random-vs-urandom/

So, if you are not using this encryption for something critical (I hope you don't) then you can use /dev/urandom to improve encryption performance, otherwise just replace MCRYPT_DEV_URANDOM with MCRYPT_DEV_RANDOM.

Important security update #1

Thanks to @HerrK who pointed out that using a simple hash to create an encryption key is not secure enough - now the PBKDF2 algorithm is used for that (read more about PBKDF2 http://en.wikipedia.org/wiki/PBKDF2).

Implementation of the PBKDF2 algorithm is copied from https://defuse.ca/php-pbkdf2.htm.

Important security update #2

Thanks to @Scott who paid attention that encrypted message should be authenticated - now HMAC is used to verify that message was not changed.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Eugene Fidelin
  • 2,049
  • 23
  • 22
  • 1
    This is not a secure implementation, the key in this case is not generated securely but just hashed out. You need to use a key derivation function like http://en.wikipedia.org/wiki/PBKDF2 – Herr Nov 20 '14 at 12:15
  • Also: you need to keep the salt and IV for decryption. – Herr Nov 20 '14 at 15:53
  • @HerrK, thank you for your notice, I've updated the code to use PBKDF2. – Eugene Fidelin Nov 21 '14 at 15:08
  • Hey @eugenefidelin here is an updated version of your original code. http://stackoverflow.com/a/27068258/288774 – Herr Nov 21 '14 at 18:51
  • 1
    Btw, your new code looks handsome and does the job well :) – Herr Nov 21 '14 at 18:53
  • 1
    "Here is simple but secure implementation" You aren't authenticating your ciphertext. [Unauthenticated encryption can be attacked.](https://paragonie.com/blog/2015/05/using-encryption-and-authentication-correctly) – Scott Arciszewski May 11 '15 at 03:26
  • 2
    @Scott, thank you for pointing this out. I've added HMAC authentication for encrypted text – Eugene Fidelin May 12 '15 at 13:05
  • Awesome. It might also be more prudent to change `strlen($str)` -> `mb_strlen($str, '8bit')`; ditto with `substr()` calls. See `mbstring.func_overload` for more info. Otherwise, excellent improvement. :) – Scott Arciszewski May 12 '15 at 13:13
  • this is an awesome chain here, opened up my eyes to allot of things I could improve for online forms in CMS systems. (protecting emails and phone numbers). Now in 2017, this is still considered a relatively good way to protect an email address or phone number? – Christian Žagarskas Jan 18 '18 at 00:31
  • @ChristianŽagarskas as soon as you are using PHP 7.2+ you can benefit from using built-in cryptographic functionality (read more (here)[https://dev.to/paragonie/php-72-the-first-programming-language-to-add-modern-cryptography-to-its-standard-library]), bu the whole approach is still valid – Eugene Fidelin Jan 19 '18 at 09:01
24

Security Warning: Encryption without authentication is vulnerable to something called a chosen-ciphertext attack. See Eugene's answer for a solution that offers authenticated encryption.

If you are using PHP >= 5.3, the new openssl_encrypt might help you: It allows encryption of data using a wide range of cypher methods.

Those data can later be decrypted with openssl_decrypt, which, obviously, does the exact opposite.

And if you want to know which cypher functions you can use, openssl_get_cipher_methods will be helpful ;-)
There are quite a lot of those, it seems ^^


Here's a portion of code I posted on my blog some time ago, that should demonstrate the usage of those three functions:

$methods = openssl_get_cipher_methods();

var_dump($methods);

$textToEncrypt = "he who doesn't do anything, doesn't go wrong -- Zeev Suraski";
$secretKey = "glop";

echo '<pre>';
foreach ($methods as $method) {
    $encrypted = openssl_encrypt($textToEncrypt, $method, $secretKey);
    $decrypted = openssl_decrypt($encrypted, $method, $secretKey);
    echo $method . ': ' . $encrypted . ' ; ' . $decrypted . "\n";
}
echo '</pre>';

The output I got when writing this was something like that:

bf-ecb: /nyRYCzQPE1sunxSBclxXBd7p7gl1fUnE80gBCS1NM4s3wS1Eho6rFHOOR73V9UtnolYW+flbiCwIKa/DYh5CQ== ; he who doesn't do anything, doesn't go wrong -- Zeev Suraski
bf-ofb: M9wwf140zhwHo98k8sj2MEXdogqXEQ+TjN81pebs2tmhNOsfU3jvMy91MBM76dWM7GVjeh95p8oDybDt ; he who doesn't do anything, doesn't go wrong -- Zeev Suraski
cast5-cbc: xKgdC1y654PFYW1rIjdevu8MsQOegvJoZx0KmMwb8aCHFmznxIQVy1yvAWR3bZztvGCGrM84WkpbG33pZcxUiQ== ; he who doesn't do anything, doesn't go wrong -- Zeev Suraski
cast5-cfb: t8ABR9mPvocRikrX0Kblq2rUXHiVnA/OnjR/mDJDq8+/nn6Z9yfPbpcpRat0lYqfVAcwlypT4A4KNq4S ; he who doesn't do anything, doesn't go wrong -- Zeev Suraski
cast5-ecb: xKgdC1y654NIzRl9gJqbhYKtmJoXBoFpgLhwgdtPtYB7VZ1tRHLX0MjErtfREMJBAonp48zngSiTKlsKV0/WhQ== ; he who doesn't do anything, doesn't go wrong -- Zeev Suraski
cast5-ofb: t8ABR9mPvofCv9+AKTcRO4Q0doYlavn8zRzLvV3dZk0niO7l20KloA4nUll4VN1B5n89T/IuGh9piPte ; he who doesn't do anything, doesn't go wrong -- Zeev Suraski
des-cbc: WrCiOVPU1ipF+0trwXyVZ/6cxiNVft+TK2+vAP0E57b9smf9x/cZlQQ4531aDX778S3YJeP/5/YulADXoHT/+Q== ; he who doesn't do anything, doesn't go wrong -- Zeev Suraski
des-cfb: cDDlaifQN+hGOnGJ2xvGna7y8+qRxwQG+1DJBwQm/4abKgdZYUczC4+aOPGesZM1nKXjgoqB4+KTxGNo ; he who doesn't do anything, doesn't go wrong -- Zeev Suraski


And if you are not using PHP 5.3, you might want to take a look to the Mcrypt section of the manual, and functions such as mcrypt_encrypt ;-)

This is an interface to the mcrypt library, which supports a wide variety of block algorithms such as DES, TripleDES, Blowfish (default), 3-WAY, SAFER-SK64, SAFER-SK128, TWOFISH, TEA, RC2 and GOST in CBC, OFB, CFB and ECB cipher modes.

Tomáš Fejfar
  • 11,129
  • 8
  • 54
  • 82
Pascal MARTIN
  • 395,085
  • 80
  • 655
  • 663
  • That function was great and very easy to use , if i want to use AES256 Should i use openssl_encrypt($texteACrypter, "AES265", $clefSecrete); –  Nov 24 '09 at 23:39
  • 1
    php 5.4.20: I got "Warning: openssl_encrypt(): Using an empty Initialization Vector (iv) is potentially insecure and not recommended" – cenk Apr 18 '14 at 18:57
  • Almost used this example but cenk is correct, this is highly insecure. To understand why check out: http://stackoverflow.com/questions/11821195/use-of-initialization-vector-in-openssl-encrypt – user3586062 Feb 22 '15 at 07:27
  • Please MAC your ciphertexts and compare them in constant-time. :) – Scott Arciszewski May 11 '15 at 03:27
5

I'm not a crypto guy, but I use this kind of things:

function crypt($dataToEncrypt){
  $appKey = '%39d15#13P0£df458asdc%/dfr_A!8792*dskjfzaesdfpopdfo45s4dqd8d4fsd+dfd4s"Z1';
  $td = mcrypt_module_open(MCRYPT_SERPENT, '', MCRYPT_MODE_CBC, '');
  // Creates IV and gets key size
  $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_DEV_RANDOM);
  $ks = mcrypt_enc_get_key_size($td);

  // Creates key from application key
  $key = substr($appKey, 0, $ks);

  // Initialization
  mcrypt_generic_init($td, $key, $iv);

  // Crypt data
  $encrypted = mcrypt_generic($td, $dataToEncrypt);

  // Close
  mcrypt_generic_deinit($td);
  mcrypt_module_close($td);
  return array($encrypted, $iv);
}

To decrypt a string you need the key and the initialization vector ($iv).

function decrypt($encryptedData, $iv){
  $appKey = '%39d15#13P0£df458asdc%/dfr_A!8792*dskjfzaesdfpopdfo45s4dqd8d4fsd+dfd4s"Z1';
  $td = mcrypt_module_open(MCRYPT_SERPENT, '', MCRYPT_MODE_CBC, '');

  // Gets key size
  $ks = mcrypt_enc_get_key_size($td);

  // Creates key from application key
  $key = substr($appKey, 0, $ks);

  // Initialization
  mcrypt_generic_init($td, $key, $iv);

  // Decrypt data
  $decrypted = mdecrypt_generic($td, $encryptedData);

  // Close
  mcrypt_generic_deinit($td);
  mcrypt_module_close($td);

  return trim($decrypted);
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Arkh
  • 8,416
  • 40
  • 45
  • 41
    @Joel No, that's just a random string created by rolling my head on the keyboard. – Arkh Mar 22 '13 at 09:24
3

Here is an updated and secured version to Eugene Fidelin's original code.

Please notice the output has the IV and Salt in it, which you also need to store securely with the decryption key.

class Cipher
{

    /**
        ----------------------------------------------
            Original Code by Eugene Fidelin
        ----------------------------------------------
    **/


    private $key;
    private $salt;
    private $iv;

    function __construct()
    {

    }

    function set_salt( $salt )
    {
        $this->salt = $salt;
    }

    function generate_salt()
    {
        $this->salt = mcrypt_create_iv( 32, MCRYPT_DEV_RANDOM ); // abuse IV function for random salt
    }

    function set_iv( $iv )
    {
        $this->iv = $iv;
    }

    function generate_iv()
    {
        $this->iv = mcrypt_create_iv( mcrypt_get_iv_size( MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC ) );
    }

    function generate_key( $passphrase, $iterations = 10000, $length = 32 )
    {
        $this->key = hash_pbkdf2 ( 'sha256', $passphrase, $this->salt, $iterations, $length );
    }

    function get_key()
    {
        echo $this->key;
    }

    function encrypt( $plaintext )
    {

        $ciphertext = mcrypt_encrypt( MCRYPT_RIJNDAEL_128, $this->key, $plaintext, MCRYPT_MODE_CBC, $this->iv );

        $data_return = array();
        $data_return['iv']          = base64_encode( $this->iv );
        $data_return['salt']        = base64_encode( $this->salt );
        $data_return['ciphertext']  = base64_encode( $ciphertext );

        return json_encode( $data_return );

    }

    function decrypt( $data_enciphered, $passphrase )
    {

        $data_decoded = json_decode( $data_enciphered, TRUE );

        $this->set_iv( base64_decode( $data_decoded['iv'] ) );
        $this->set_salt( base64_decode( $data_decoded['salt'] ) );
        $this->generate_key( $passphrase );         

        $ciphertext = base64_decode( $data_decoded['ciphertext'] );

        return trim( mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $this->key, $ciphertext, MCRYPT_MODE_CBC, $this->iv ) );

    }

}


$cipher = new Cipher();

$cipher->generate_salt();
$cipher->generate_iv();
$cipher->generate_key( '123' ); // the key will be generated from the passphrase "123"
// echo $cipher->get_key();

$data_encrypted = $cipher->encrypt( 'hello' );  


echo 'encrypted:';
echo '<pre>';
print_r( $data_encrypted );
echo '</pre>';


unset( $cipher );

echo 'decrypted:';

$cipher = new Cipher();
$decrypted = $cipher->decrypt( $data_encrypted, '123' );

echo '<pre>';
print_r( $decrypted );
echo '</pre>';


die();
Herr
  • 2,725
  • 3
  • 30
  • 36
1

Here is a good PHP library that can help you encrypt and decrypt strings - available via Composer and easy to use too:

https://github.com/CoreProc/crypto-guard

Here is a sample:

<?php

require 'vendor/autoload.php';

use Coreproc\CryptoGuard\CryptoGuard;

// This passphrase should be consistent and will be used as your key to encrypt/decrypt
// your string
$passphrase = 'whatever-you-want';

// Instantiate the CryptoGuard class
$cryptoGuard = new CryptoGuard($passphrase);

$stringToEncrypt = 'test';

// This will spit out the encrypted text
$encryptedText = $cryptoGuard->encrypt($stringToEncrypt);

// This should give you back the string you encrypted
echo $cryptoGuard->decrypt($encryptedText);
chrisbjr
  • 628
  • 4
  • 10