Benchmarking symmetric cyphers in PHP - OpenSSL vs. Mcrypt
Symmetric (two-way) cyphers are simple and 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 make yesterday :-)
Encrypting such values in PHP is actually very easy (though unforgettably rarely used) - works right out of the box just by enabling standard core PHP extension.
PHP does not offer too many ways of using symmetric ciphers - the most used are:
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. |
||
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 |
Conclusion:
- 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).'
';
}