Benchmarking symmetric cyphers in PHP - OpenSSL vs. Mcrypt

Published on 04.10.2013, Last update on 10.06.2015, by lubosdz

Benchmarking symmetric cyphers in PHP - OpenSSL vs. Mcrypt

Symmetric (two-way) cyphers are the only secure way of transferring unencrypted data over the internet. A typical use case would be sending URL link with parameter called "contractID". In some situations it is undesirable to display what is the actual value of the contract ID - user can be tempted to play with the value, or a competitor may learn how many contracts did you yesterday:-)

While some kind of custom obfuscation routines might be also usable, an advantage of encrypting such a values with standard encryption algorithm is that they are much more difficult to decipher and work right out of the box - just by enabling standard PHP extension.

PHP does not offer too many ways of using symmetric ciphers - the most used are PHP extensions:

Both libraries offer symmetric algorithms, but each at different speed and capabilities.

Bellow are basic tests comparing encryption speed of both extensions for short (1-4 bytes) and longer strings (1 - 4 kB). A synthetic test of all supported OpenSSL encryption methods is included also in TEST 7. Tests were performed on apache with PHP 5.4.26 x86 VC9/TS with mcrypt 2.5.8 and openSSL 0.9.8y.

Mcrypt vs. openSSL - Short string 1-4 bytes in secs. (avg. results of 50 runs)

 
1000 loops / ø 1 cycle
5000 loops / ø 1 cycle
TEST 1 - MCRYPT1 - encrypt only
0,268 / 0.000268
1,296 / 0.000259
TEST 2 - MCRYPT1 - encrypt + decrypt
0,511 / 0.000511
2,540 / 0.000508
TEST 3 - AES 2562 - encrypt only
0,006 / 0.000006
0,029 / 0.000006
TEST 4 - AES 2562 - encrypt + decrypt
0,013 / 0.000013
0,059 / 0.000011
TEST 5 - AES 1282 - encrypt only
0,006 / 0.000006
0,031 / 0.000006
TEST 6 - AES 1282 - encrypt + decrypt
0,013 / 0.000013
0,063 / 0.000012
 

Mcrypt vs. openSSL - Medium string 1-4 kB in secs. (avg. results of 50 runs)

 
1000 loops / ø 1 cycle
5000 loops / ø 1 cycle
TEST 1 - MCRYPT1 - encrypt only
0,504 / 0.000504
2,883 / 0.000577
TEST 2 - MCRYPT1 - encrypt + decrypt
1,004 / 0.001004
5,785 / 0.001157
TEST 3 - AES 2562 - encrypt only
0,033 / 0.000033
0,207 / 0.000041
TEST 4 - AES 2562 - encrypt + decrypt
0,086 / 0.000086
0,547 / 0.000109
TEST 5 - AES 1282 - encrypt only
0,022 / 0.000022
0,133 / 0.000026
TEST 6 - AES 1282 - encrypt + decrypt
0,065 / 0.000065
0,402 / 0.000080
 
1 - Mcrypt Blowfish with CFB mode
2 - OpenSSL AES 128 or (256) with CFB mode

Mcrypt vs. openSSL - 200 kB in secs. (avg. results of 50 runs)

 
avg. ø 1 cycle
TEST 1 - MCRYPT (blowfish, CFB) - encrypt only
0.0182
TEST 2 - MCRYPT (blowfish, CFB) - encrypt + decrypt
0.0344
TEST 3 - OpenSSL (AES 256 CFB) - encrypt only
0.0020
TEST 4 - OpenSSL (AES 256 CFB) - encrypt + decrypt
0.0052
TEST 5 - OpenSSL (AES 128 CBC) - encrypt only
0.0011
TEST 6 - OpenSSL (AES 128 CBC) - encrypt + decrypt
0,0040
   

Mcrypt vs. openSSL - 20 MB in secs. (avg. results of 50 runs)

 
avg. ø 1 cycle
TEST 1 - MCRYPT (blowfish, CFB) - encrypt only
1.7284
TEST 2 - MCRYPT (blowfish, CFB) - encrypt + decrypt
3.4628
TEST 3 - OpenSSL (AES 256 CFB) - encrypt only
0.1958
TEST 4 - OpenSSL (AES 256 CFB) - encrypt + decrypt
0.5648
TEST 5 - OpenSSL (AES 128 CBC) - encrypt only
0.1156
TEST 6 - OpenSSL (AES 128 CBC) - encrypt + decrypt
0,4062

TEST 7 - Fastest OpenSSL algorithms are at the top (in secs, avg. 50 runs)

Fastest OpenSSL algorithms - 200 kB

Fastest OpenSSL algorithms - 2 MB

    [AES-192-OFB] => 0.000*
    [AES-256-OFB] => 0.000*
    [AES-128-CBC] => 0.000*
    [AES-256-CFB] => 0.000*
    [BF-CFB] => 0.000*
    [IDEA-ECB] => 0.000*
    [AES-192-CFB] => 0.000*
    [CAST5-CBC] => 0.000*
    [RC4] => 0.000*
    [CAST5-OFB] => 0.000*
    [AES-128-OFB] => 0.000*
    [AES-128-ECB] => 0.000*
    [DESX-CBC] => 0.010
    [DES-EDE3-CFB] => 0.010
    [DES-OFB] => 0.010
    [DES-ECB] => 0.010
    [DES-EDE-CFB] => 0.010
    [IDEA-OFB] => 0.010
    [RC2-ECB] => 0.010
    [RC2-OFB] => 0.010
    [RC4-40] => 0.010
    [RC2-CBC] => 0.010
    [RC2-64-CBC] => 0.010
    [IDEA-CFB] => 0.010
    [RC2-40-CBC] => 0.010
    [IDEA-CBC] => 0.010
    [DES-CFB] => 0.010
    [AES-256-ECB] => 0.010
    [BF-CBC] => 0.010
    [BF-ECB] => 0.010
    [BF-OFB] => 0.010
    [CAST5-CFB] => 0.010
    [AES-256-CBC] => 0.010
    [AES-128-CFB] => 0.010
    [AES-192-CBC] => 0.010
    [CAST5-ECB] => 0.010
    [AES-192-ECB] => 0.010
    [DES-CBC] => 0.010
    [DES-EDE3-OFB] => 0.011
    [DES-EDE] => 0.020
    [RC2-CFB] => 0.020
    [DES-EDE3] => 0.020
    [DES-EDE-CBC] => 0.020
    [DES-EDE3-CBC] => 0.020
    [DES-EDE-OFB] => 0.022
    [AES-128-CFB8] => 0.040
    [AES-192-CFB8] => 0.040
    [DES-CFB8] => 0.050
    [AES-256-CFB8] => 0.050
    [DES-CFB1] => 0.052
    [DES-EDE3-CFB8] => 0.131
    [DES-EDE3-CFB1] => 0.142
    [AES-128-CFB1] => 0.344
    [AES-192-CFB1] => 0.392
    [AES-256-CFB1] => 0.428

    * 0.000 - not measurable for the setup
    
    [RC4-40] => 0.030
    [RC4] => 0.030
    [AES-192-CBC] => 0.040
    [AES-256-CBC] => 0.040
    [AES-128-ECB] => 0.040
    [AES-192-ECB] => 0.050
    [AES-192-OFB] => 0.050
    [AES-128-OFB] => 0.050
    [AES-128-CFB] => 0.050
    [AES-256-OFB] => 0.050
    [BF-ECB] => 0.060
    [BF-CBC] => 0.060
    [AES-128-CBC] => 0.060
    [AES-256-ECB] => 0.060
    [CAST5-CBC] => 0.060
    [BF-OFB] => 0.060
    [AES-256-CFB] => 0.060
    [AES-192-CFB] => 0.060
    [CAST5-CFB] => 0.070
    [CAST5-ECB] => 0.070
    [IDEA-CBC] => 0.070
    [DES-CBC] => 0.070
    [BF-CFB] => 0.070
    [CAST5-OFB] => 0.070
    [IDEA-ECB] => 0.076
    [IDEA-OFB] => 0.080
    [DESX-CBC] => 0.080
    [DES-OFB] => 0.090
    [IDEA-CFB] => 0.090
    [DES-ECB] => 0.090
    [DES-CFB] => 0.090
    [RC2-CBC] => 0.100
    [RC2-64-CBC] => 0.100
    [RC2-ECB] => 0.100
    [RC2-40-CBC] => 0.110
    [RC2-CFB] => 0.140
    [RC2-OFB] => 0.140
    [DES-EDE-CBC] => 0.170
    [DES-EDE3-CBC] => 0.170
    [DES-EDE] => 0.170
    [DES-EDE-OFB] => 0.180
    [DES-EDE3-CFB] => 0.180
    [DES-EDE3] => 0.180
    [DES-EDE-CFB] => 0.190
    [DES-EDE3-OFB] => 0.190
    [AES-128-CFB8] => 0.370
    [AES-192-CFB8] => 0.420
    [AES-256-CFB8] => 0.470
    [DES-CFB8] => 0.489
    [DES-CFB1] => 0.570
    [DES-EDE3-CFB8] => 1.270
    [DES-EDE3-CFB1] => 1.380
    [AES-128-CFB1] => 3.450
    [AES-192-CFB1] => 3.870
    [AES-256-CFB1] => 4.327
    

Conclusions:

  • OpenSSL offers significantly faster algorithms. This is the right choice for heavy traffic sites.
  • Mcrypt can use rotating ciphers when using random seed in initiation vector (IV). This is GREAT feature because makes attacker's job much tougher. In another words - that same string results into different encrypted string. This feature is similar to SSHA hashes used e.g. by LDAP servers.
  • Choosing AES 256 over AES 128 should be safer choice while loosing only insignificant overhead.
  • The fastest openSSL algorithms are RC-*, AES-*-OFB, AES-*-CFB algorithms, even though the performance difference amongst first 20 ciphers is negligable.
  • two openSSL algorithms seems to be broken (DES-CFB1, DES-EDE3-CFB1) while returning invalid decrypted text. I am not sure if I did not set some parameter correctly or this is really a bug or has been fixed in newer versions of PHP. The CFB1 based algos seems to be the slowest.
  • [edit 2015-06] Mcrypt actually uses long time abandoned C-library, while OpenSSL is actively maintained.

Testing scenario

For those interested, bellow is a fully reproducable testing scenario.

PHP Encryption / Decryption class


/**
* Encryption / decryption PHP class for symmetric (two-way) ciphers
*/
class Data{

    const
        CYPHER = 'blowfish',
        MODE   = 'cfb',
        SALT   = '7d9!y8y4=h7v2v8v*1|1';

    /**
    * Encrypt string using mcrypt module
    * @param string $plaintext Text to be encrypted
    * @param string $password User entered password
    */
    public static function encryptMcrypt($plaintext, $password = ''){
        $td = mcrypt_module_open(self::CYPHER, '', self::MODE, '');
        $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
        $key = self::SALT.trim($password);
        mcrypt_generic_init($td, $key, $iv);
        $crypttext = mcrypt_generic($td, $plaintext);
        mcrypt_generic_deinit($td);
        $crypttext = $iv.$crypttext;
        return $crypttext;
    }

    /**
    * Decrypt string using mcrypt module
    * @param string $crypttext Text to be decrypted
    * @param string $password User entered password
    */
    public static function decryptMcrypt($crypttext, $password = ''){
        $td        = mcrypt_module_open(self::CYPHER, '', self::MODE, '');
        $ivsize    = mcrypt_enc_get_iv_size($td);
        $iv        = substr($crypttext, 0, $ivsize);
        $crypttext = substr($crypttext, $ivsize);
        $key = self::SALT.trim($password);
        mcrypt_generic_init($td, $key, $iv);
        return mdecrypt_generic($td, $crypttext);
    }

    /**
    * Encrypt string using openSSL module
    * @param string $textToEncrypt
    * @param string $encryptionMethod One of built-in 50 encryption algorithms
    * @param string $secretHash Any random secure SALT string for your website
    * @param bool $raw If TRUE return base64 encoded string
    * @param string $password User's optional password
    */
    public static function encryptOpenssl($textToEncrypt, $encryptionMethod = 'AES-256-CFB', $secretHash = self::SALT, $raw = false, $password = ''){
        $length = openssl_cipher_iv_length($encryptionMethod);
        $iv = substr(md5($password), 0, $length);
        return openssl_encrypt($textToEncrypt, $encryptionMethod, $secretHash, $raw, $iv);
    }

    /**
    * Decrypt string using openSSL module
    * @param string $textToDecrypt
    * @param string $encryptionMethod One of built-in 50 encryption algorithms
    * @param string $secretHash Any random secure SALT string for your website
    * @param bool $raw If TRUE return base64 encoded string
    * @param string $password User's optional password
    */
    public static function decryptOpenssl($textToDecrypt, $encryptionMethod = 'AES-256-CFB', $secretHash = self::SALT, $raw = false, $password = ''){
        $length = openssl_cipher_iv_length($encryptionMethod);
        $iv = substr(md5($password), 0, $length);
        return openssl_decrypt($textToDecrypt, $encryptionMethod, $secretHash, $raw, $iv);
    }
}

Following scripts were used to generate results:


// adjust test parameters as needed
$loops = 1000;
$stringMultiplyFactor = 1;

// show test settings
echo 'LOOPS: '.$loops;
echo '
MULTIPLY STRING LENGTH FACTOR: '.$stringMultiplyFactor; echo '
======================
'; echo '
TEST 1. mcrypt - encrypt only'; $t = microtime(true); for($i=0;$i < $loops;++$i){ $txt = Data::encryptMcrypt(str_repeat($i, $stringMultiplyFactor)); } echo '
result: '.round(microtime(true)-$t, 3).'
'; echo '
TEST 2. mcrypt - encrypt + decrypt'; $t = microtime(true); for($i=0 ;$i < $loops; ++$i){ $txt = Data::encryptMcrypt(str_repeat($i, $stringMultiplyFactor)); $txt2 = Data::decryptMcrypt($txt); if(str_repeat($i, $stringMultiplyFactor)!=$txt2){ exit('error: '.str_repeat($i, $stringMultiplyFactor).' != '.$txt2); } } echo '
result: '.round(microtime(true)-$t, 3).'
'; echo '
TEST 3. openssl - encrypt only AES 256'; $t = microtime(true); for($i=0; $i < $loops; ++$i){ $txt = Data::encryptOpenssl(str_repeat($i, $stringMultiplyFactor)); } echo '
result: '.round(microtime(true)-$t, 3).'
'; echo '
TEST 4. openssl - encrypt + decrypt'; $t = microtime(true); for($i=0;$i < $loops; ++$i){ $txt = Data::encryptOpenssl(str_repeat($i, $stringMultiplyFactor)); $txt2 = Data::decryptOpenssl($txt); // .9 if(str_repeat($i, $stringMultiplyFactor)!=$txt2){ exit('error: '.str_repeat($i, $stringMultiplyFactor).' != '.$txt2); } } echo '
result: '.round(microtime(true)-$t, 3).'
'; echo '
TEST 5. openssl - encrypt only / AES 128'; $t = microtime(true); for($i=0; $i < $loops; ++$i){ $txt = Data::encryptOpenssl(str_repeat($i, $stringMultiplyFactor), 'AES-128-CBC'); } echo '
result: '.round(microtime(true)-$t, 3).'
'; echo '
TEST 6. openssl - encrypt + decrypt'; $t = microtime(true); for($i=0; $i < $loops; ++$i){ $txt = Data::encryptOpenssl(str_repeat($i, $stringMultiplyFactor), 'AES-128-CBC'); $txt2 = Data::decryptOpenssl($txt, 'AES-128-CBC'); if(str_repeat($i, $stringMultiplyFactor)!=$txt2){ exit('error: '.str_repeat($i, $stringMultiplyFactor).' != '.$txt2); } } echo '
result: '.round(microtime(true)-$t, 3).'
'; echo '
TEST 7 - syntethic test - loop benchmark all openSSL AES supported algorithms
'; // collect all supported algorithms $a = openssl_get_cipher_methods(); $a = array_flip($a); $a = array_change_key_case($a, CASE_UPPER); $a = array_keys($a); $failing = array(); $sort = array(); foreach($a as $c => $method){ echo '
'.++$c.'. openssl - encrypt + decrypt ... ['.$method.']'; $t = microtime(true); for($i=0;$i < $loops; ++$i){ $txt = Data::encryptOpenssl(str_repeat($i, $stringMultiplyFactor), $method); $txt2 = Data::decryptOpenssl($txt.'*', $method); if(str_repeat($i, $stringMultiplyFactor)!=$txt2){ $failing[$method] ='
failed checksum ----> error: '.str_repeat($i, $stringMultiplyFactor).' != '.$txt2; } } $t = microtime(true)-$t; echo ' ... result: '.number_format($t, 5); $sort[$method] = number_format($t,3); } asort($sort); // fastest at the top echo '
Sorted results from TEST 7:
'.print_r($sort, true).'
'; // any errors? if($failing){ echo '
TEST 7 - Crashing openSSL ciphers:
'.print_r($failing, true).'
'; }


Comments...

David

06.03.2014 03:27
# 1 Reply to David    
 

Great Job! I am converting my site to use encryption, and I will definitely be using this as a reference. Thanks again.

philios

10.06.2015 00:36
# 2 Reply to philios    
 

Hi Lubos

I have a few critical points.

When doing any kind of benchmarking, you should do the same tests many times and take an average figure. You should start to see consistancy between your 1000 and 5000 figures.

Also, I feel your size choice is a bit off. Who is going to encrypt 4 bytes of data? It would have been nice to see comparisons between say 2K, 200K, and 20M.

I assume you meant to say Tests 3 - 6 are using openssl.

Thanks

lubosdz

10.06.2015 15:16
# 3 Reply to lubosdz    
 

philios wrote on 10.06.2015 00:36:
Hi Lubos

I have a few critical points.

When doing any kind of benchmarking, you should do the same tests many times and take an average figure. You should start to see consistancy between your 1000 and 5000 figures.

Also, I feel your size choice is a bit off. Who is going to encrypt 4 bytes of data? It would have been nice to see comparisons between say 2K, 200K, and 20M.

I assume you meant to say Tests 3 - 6 are using openssl.

Thanks
Hi Philios,

thanx for comments.

You are actually right, interpretation was a little difficult and unclear.
So I updated the benchmarks and also added new results for 200 kB and 20 MB strings. Only synthetic test for all openSSL algos was performed with 2 MB string rather than 20 MB.

All results ran about 50 times, so the standard deviation is really insignificant.

test

09.03.2016 15:26
# 4 Reply to test    
 

Nice Job!

Eugene S

19.10.2016 14:36
# 5 Reply to Eugene S    
 

Great gob.

Thank you for this valuable article.
Please more of this.

Kind Regards
Eugene.

Leave your comment..
Email will be converted into something like [michael AT gmail DOT com]
Note: Offensive and unrelated comments will be deleted.
Please enter result from the picture above.