Make mcrypt optional

Now Security::encrypt() and Security::decrypt() works with openssl
if the mcrypt extension is unavailable.
Note that Security::rijndael() doesn't work with openssl.
This commit is contained in:
chinpei215 2018-01-19 23:35:39 +09:00
parent ab272b09c7
commit d7ed0339b1
3 changed files with 151 additions and 14 deletions

View file

@ -18,8 +18,11 @@
"source": "https://github.com/cakephp/cakephp" "source": "https://github.com/cakephp/cakephp"
}, },
"require": { "require": {
"php": ">=5.3.0", "php": ">=5.3.0"
"ext-mcrypt": "*" },
"suggest": {
"ext-openssl": "You need to install ext-openssl or ext-mcrypt to use AES-256 encryption",
"ext-mcrypt": "You need to install ext-openssl or ext-mcrypt to use AES-256 encryption"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "<6.0.0", "phpunit/phpunit": "<6.0.0",

View file

@ -29,6 +29,45 @@ class SecurityTest extends CakeTestCase {
*/ */
public $sut = null; public $sut = null;
/**
* setUp method
*
* @return void
*/
public function setUp() {
parent::setUp();
Security::engine(null);
}
/**
* tearDown method
*
* @return void
*/
public function tearDown() {
parent::tearDown();
Security::engine(null);
}
/**
* Tests that Security::engine() works
*
* @return void
*/
public function testEngine() {
if (extension_loaded('mcrypt')) {
$this->assertEquals('mcrypt', Security::engine());
}
$this->assertContains(Security::engine(), array('mcrypt', 'openssl'));
Security::engine('mcrypt');
$this->assertEquals('mcrypt', Security::engine());
Security::engine('openssl');
$this->assertEquals('openssl', Security::engine());
}
/** /**
* testInactiveMins method * testInactiveMins method
* *
@ -337,6 +376,54 @@ class SecurityTest extends CakeTestCase {
$this->assertEquals($txt, Security::decrypt($result, $key)); $this->assertEquals($txt, Security::decrypt($result, $key));
} }
/**
* Tests that encrypted strings are comatible between the mcrypt and openssl engine.
*
* @dataProvider plainTextProvider
* @param string $txt Plain text to be encrypted.
* @return void
*/
public function testEncryptDecryptCompatibility($txt) {
$this->skipIf(!extension_loaded('mcrypt'), 'This test requires mcrypt to be installed');
$this->skipIf(!extension_loaded('openssl'), 'This test requires oepnssl to be installed');
$this->skipIf(version_compare(PHP_VERSION, '5.3.3', '<'), 'This test requires PHP 5.3.3 or grater');
$key = '12345678901234567890123456789012';
Security::engine('mcrypt');
$mcrypt = Security::encrypt($txt, $key);
Security::engine('openssl');
$openssl = Security::encrypt($txt, $key);
$this->assertEquals(strlen($mcrypt), strlen($openssl));
Security::engine('mcrypt');
$this->assertEquals($txt, Security::decrypt($mcrypt, $key));
$this->assertEquals($txt, Security::decrypt($openssl, $key));
Security::engine('openssl');
$this->assertEquals($txt, Security::decrypt($mcrypt, $key));
$this->assertEquals($txt, Security::decrypt($openssl, $key));
}
/**
* Data provider for testEncryptDecryptCompatibility
*
* @return array
*/
public function plainTextProvider() {
return array(
array(''),
array('abcdefg'),
array('1234567890123456'),
array('The quick brown fox'),
array('12345678901234567890123456789012'),
array('The quick brown fox jumped over the lazy dog.'),
array('何らかのマルチバイト文字列'),
);
}
/** /**
* Test that changing the key causes decryption to fail. * Test that changing the key causes decryption to fail.
* *

View file

@ -25,6 +25,13 @@ App::uses('CakeText', 'Utility');
*/ */
class Security { class Security {
/**
* The encryption engine
*
* @var string
*/
protected static $_engine = null;
/** /**
* Default hash method * Default hash method
* *
@ -206,7 +213,7 @@ class Security {
* for sensitive data. Additionally this method does *not* work in environments * for sensitive data. Additionally this method does *not* work in environments
* where suhosin is enabled. * where suhosin is enabled.
* *
* Instead you should use Security::rijndael() when you need strong * Instead you should use Security::encrypt() when you need strong
* encryption. * encryption.
* *
* @param string $text Encrypted string to decrypt, normal string to encrypt * @param string $text Encrypted string to decrypt, normal string to encrypt
@ -349,12 +356,24 @@ class Security {
// Generate the encryption and hmac key. // Generate the encryption and hmac key.
$key = substr(hash('sha256', $key . $hmacSalt), 0, 32); $key = substr(hash('sha256', $key . $hmacSalt), 0, 32);
$algorithm = MCRYPT_RIJNDAEL_128; if (static::engine() === 'openssl') {
$mode = MCRYPT_MODE_CBC; $method = 'AES-256-CBC';
$ivSize = openssl_cipher_iv_length($method);
$iv = openssl_random_pseudo_bytes($ivSize);
$padLength = (int)ceil((strlen($plain) ?: 1) / $ivSize) * $ivSize;
$ciphertext = openssl_encrypt(str_pad($plain, $padLength, "\0"), $method, $key, true, $iv);
// Remove the PKCS#7 padding block for compatibility with mcrypt.
// Since we have padded the provided data with \0, the final block contains only padded bytes.
// So it can be removed safely.
$ciphertext = $iv . substr($ciphertext, 0, -$ivSize);
} else {
$algorithm = MCRYPT_RIJNDAEL_128;
$mode = MCRYPT_MODE_CBC;
$ivSize = mcrypt_get_iv_size($algorithm, $mode);
$iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
$ciphertext = $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv);
}
$ivSize = mcrypt_get_iv_size($algorithm, $mode);
$iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
$ciphertext = $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv);
$hmac = hash_hmac('sha256', $ciphertext, $key); $hmac = hash_hmac('sha256', $ciphertext, $key);
return $hmac . $ciphertext; return $hmac . $ciphertext;
} }
@ -404,14 +423,42 @@ class Security {
return false; return false;
} }
$algorithm = MCRYPT_RIJNDAEL_128; if (static::engine() === 'openssl') {
$mode = MCRYPT_MODE_CBC; $method = 'AES-256-CBC';
$ivSize = mcrypt_get_iv_size($algorithm, $mode); $ivSize = openssl_cipher_iv_length($method);
$iv = substr($cipher, 0, $ivSize);
$cipher = substr($cipher, $ivSize);
// Regenerate PKCS#7 padding block
$padding = openssl_encrypt('', $method, $key, true, substr($cipher, -$ivSize));
$plain = openssl_decrypt($cipher . $padding, $method, $key, true, $iv);
} else {
$algorithm = MCRYPT_RIJNDAEL_128;
$mode = MCRYPT_MODE_CBC;
$ivSize = mcrypt_get_iv_size($algorithm, $mode);
$iv = substr($cipher, 0, $ivSize);
$cipher = substr($cipher, $ivSize);
$plain = mcrypt_decrypt($algorithm, $key, $cipher, $mode, $iv);
}
$iv = substr($cipher, 0, $ivSize);
$cipher = substr($cipher, $ivSize);
$plain = mcrypt_decrypt($algorithm, $key, $cipher, $mode, $iv);
return rtrim($plain, "\0"); return rtrim($plain, "\0");
} }
/**
* Set or get the encryption engine
*
* @param string $engine The encryption engine to use
* @return string
*/
public static function engine($engine = null) {
if (func_num_args() > 0) {
static::$_engine = $engine;
} elseif (static::$_engine === null) {
static::$_engine = 'mcrypt';
if (!extension_loaded('mcrypt') && extension_loaded('openssl') && version_compare(PHP_VERSION, '5.3.3', '>=')) {
static::$_engine = 'openssl';
}
}
return static::$_engine;
}
} }