Browse Source

Merge pull request #5496 from cakephp/no-mcrypt

Remove hard dependency on mcrypt
José Lorenzo Rodríguez 11 years ago
parent
commit
af1d3ec4fe

+ 0 - 1
composer.json

@@ -20,7 +20,6 @@
 	"require": {
 		"php": ">=5.4.16",
 		"ext-intl": "*",
-		"ext-mcrypt": "*",
 		"ext-mbstring": "*",
 		"nesbot/Carbon": "1.13.*",
 		"ircmaxell/password-compat": "1.0.*",

+ 105 - 0
src/Utility/Crypto/Mcrypt.php

@@ -0,0 +1,105 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Utility\Crypto;
+
+/**
+ * Mcrypt implementation of crypto features for Cake\Utility\Security
+ *
+ * This class is not intended to be used directly and should only
+ * be used in the context of Cake\Utility\Security.
+ *
+ * @internal
+ */
+class Mcrypt {
+
+/**
+ * Encrypts/Decrypts a text using the given key using rijndael method.
+ *
+ * @param string $text Encrypted string to decrypt, normal string to encrypt
+ * @param string $key Key to use as the encryption key for encrypted data.
+ * @param string $operation Operation to perform, encrypt or decrypt
+ * @throws \LogicException When there are errors.
+ * @return string Encrytped binary string data, or decrypted data depending on operation.
+ */
+	public static function rijndael($text, $key, $operation) {
+		$algorithm = MCRYPT_RIJNDAEL_256;
+		$mode = MCRYPT_MODE_CBC;
+		$ivSize = mcrypt_get_iv_size($algorithm, $mode);
+
+		$cryptKey = substr($key, 0, 32);
+
+		if ($operation === 'encrypt') {
+			$iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
+			return $iv . '$$' . mcrypt_encrypt($algorithm, $cryptKey, $text, $mode, $iv);
+		}
+		$iv = substr($text, 0, $ivSize);
+		$text = substr($text, $ivSize + 2);
+		return rtrim(mcrypt_decrypt($algorithm, $cryptKey, $text, $mode, $iv), "\0");
+	}
+
+/**
+ * Encrypt a value using AES-256.
+ *
+ * *Caveat* You cannot properly encrypt/decrypt data with trailing null bytes.
+ * Any trailing null bytes will be removed on decryption due to how PHP pads messages
+ * with nulls prior to encryption.
+ *
+ * @param string $plain The value to encrypt.
+ * @param string $key The 256 bit/32 byte key to use as a cipher key.
+ * @return string Encrypted data.
+ * @throws \InvalidArgumentException On invalid data or key.
+ */
+	public static function encrypt($plain, $key) {
+		$algorithm = MCRYPT_RIJNDAEL_128;
+		$mode = MCRYPT_MODE_CBC;
+
+		$ivSize = mcrypt_get_iv_size($algorithm, $mode);
+		$iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
+
+		// Pad out plain to make it AES compatible.
+		$pad = ($ivSize - (strlen($plain) % $ivSize));
+		$plain .= str_repeat(chr($pad), $pad);
+
+		return $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv);
+	}
+
+/**
+ * Decrypt a value using AES-256.
+ *
+ * @param string $cipher The ciphertext to decrypt.
+ * @param string $key The 256 bit/32 byte key to use as a cipher key.
+ * @return string Decrypted data. Any trailing null bytes will be removed.
+ * @throws InvalidArgumentException On invalid data or key.
+ */
+	public static function decrypt($cipher, $key) {
+		$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);
+
+		// Remove PKCS#7 padding or Null bytes
+		// Newer values will be PKCS#7 padded, while old
+		// mcrypt values will be null byte padded.
+		$padChar = substr($plain, -1);
+		if ($padChar === "\0") {
+			return trim($plain, "\0");
+		}
+		$padLen = ord($padChar);
+		return substr($plain, 0, -$padLen);
+	}
+}

+ 82 - 0
src/Utility/Crypto/OpenSsl.php

@@ -0,0 +1,82 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Utility\Crypto;
+
+/**
+ * OpenSSL implementation of crypto features for Cake\Utility\Security
+ *
+ * OpenSSL should be favored over mcrypt as it is actively maintained and
+ * more widely available.
+ *
+ * This class is not intended to be used directly and should only
+ * be used in the context of Cake\Utility\Security.
+ *
+ * @internal
+ */
+class OpenSsl {
+
+/**
+ * Not implemented
+ *
+ * @param string $text Encrypted string to decrypt, normal string to encrypt
+ * @param string $key Key to use as the encryption key for encrypted data.
+ * @param string $operation Operation to perform, encrypt or decrypt
+ * @throws \LogicException Rijndael compatibility does not exist with Openssl.
+ * @return void
+ */
+	public static function rijndael($text, $key, $operation) {
+		throw new \LogicException('rijndael is not compatible with OpenSSL. Use mcrypt instead.');
+	}
+
+/**
+ * Encrypt a value using AES-256.
+ *
+ * *Caveat* You cannot properly encrypt/decrypt data with trailing null bytes.
+ * Any trailing null bytes will be removed on decryption due to how PHP pads messages
+ * with nulls prior to encryption.
+ *
+ * @param string $plain The value to encrypt.
+ * @param string $key The 256 bit/32 byte key to use as a cipher key.
+ * @param string|null $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt.
+ * @return string Encrypted data.
+ * @throws \InvalidArgumentException On invalid data or key.
+ */
+	public static function encrypt($plain, $key, $hmacSalt = null) {
+		$method = 'AES-256-CBC';
+		$ivSize = openssl_cipher_iv_length($method);
+
+		$iv = openssl_random_pseudo_bytes($ivSize);
+		return $iv . openssl_encrypt($plain, $method, $key, true, $iv);
+	}
+
+/**
+ * Decrypt a value using AES-256.
+ *
+ * @param string $cipher The ciphertext to decrypt.
+ * @param string $key The 256 bit/32 byte key to use as a cipher key.
+ * @return string Decrypted data. Any trailing null bytes will be removed.
+ * @throws \InvalidArgumentException On invalid data or key.
+ */
+	public static function decrypt($cipher, $key) {
+		$method = 'AES-256-CBC';
+		$ivSize = openssl_cipher_iv_length($method);
+
+		$iv = substr($cipher, 0, $ivSize);
+
+		$cipher = substr($cipher, $ivSize);
+		return openssl_decrypt($cipher, $method, $key, true, $iv);
+	}
+}
+

+ 43 - 28
src/Utility/Security.php

@@ -14,6 +14,8 @@
  */
 namespace Cake\Utility;
 
+use Cake\Utility\Crypto\Mcrypt;
+use Cake\Utility\Crypto\OpenSsl;
 use InvalidArgumentException;
 
 /**
@@ -38,6 +40,13 @@ class Security {
 	protected static $_salt;
 
 /**
+ * The crypto implementation to use.
+ *
+ * @var object
+ */
+	protected static $_instance;
+
+/**
  * Generate authorization hash.
  *
  * @return string Hash
@@ -87,6 +96,34 @@ class Security {
 	}
 
 /**
+ * Get the crypto implementation based on the loaded extensions.
+ *
+ * You can use this method to forcibly decide between mcrypt/openssl/custom implementations.
+ *
+ * @param object $instance The crypto instance to use.
+ * @return object Crypto instance.
+ * @throws \InvalidArgumentException When no compatible crypto extension is available.
+ */
+	public static function engine($instance = null) {
+		if ($instance === null && static::$_instance === null) {
+			if (extension_loaded('openssl')) {
+				$instance = new OpenSsl();
+			} elseif (extension_loaded('mcrypt')) {
+				$instance = new Mcrypt();
+			}
+		}
+		if ($instance) {
+			static::$_instance = $instance;
+		}
+		if (isset(static::$_instance)) {
+			return static::$_instance;
+		}
+		throw new InvalidArgumentException(
+			'No compatible crypto engine available. ' .
+			'Load either the openssl or mcrypt extensions');
+	}
+
+/**
  * Encrypts/Decrypts a text using the given key using rijndael method.
  *
  * @param string $text Encrypted string to decrypt, normal string to encrypt
@@ -105,19 +142,8 @@ class Security {
 		if (strlen($key) < 32) {
 			throw new InvalidArgumentException('You must use a key larger than 32 bytes for Security::rijndael()');
 		}
-		$algorithm = MCRYPT_RIJNDAEL_256;
-		$mode = MCRYPT_MODE_CBC;
-		$ivSize = mcrypt_get_iv_size($algorithm, $mode);
-
-		$cryptKey = substr($key, 0, 32);
-
-		if ($operation === 'encrypt') {
-			$iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
-			return $iv . '$$' . mcrypt_encrypt($algorithm, $cryptKey, $text, $mode, $iv);
-		}
-		$iv = substr($text, 0, $ivSize);
-		$text = substr($text, $ivSize + 2);
-		return rtrim(mcrypt_decrypt($algorithm, $cryptKey, $text, $mode, $iv), "\0");
+		$crypto = static::engine();
+		return $crypto->rijndael($text, $key, $operation);
 	}
 
 /**
@@ -139,16 +165,11 @@ class Security {
 		if ($hmacSalt === null) {
 			$hmacSalt = static::$_salt;
 		}
-
 		// Generate the encryption and hmac key.
 		$key = substr(hash('sha256', $key . $hmacSalt), 0, 32);
 
-		$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);
+		$crypto = static::engine();
+		$ciphertext = $crypto->encrypt($plain, $key);
 		$hmac = hash_hmac('sha256', $ciphertext, $key);
 		return $hmac . $ciphertext;
 	}
@@ -200,14 +221,8 @@ class Security {
 			return false;
 		}
 
-		$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);
-		return rtrim($plain, "\0");
+		$crypto = static::engine();
+		return $crypto->decrypt($cipher, $key);
 	}
 
 /**

+ 108 - 0
tests/TestCase/Utility/Crypto/McryptTest.php

@@ -0,0 +1,108 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Utility\Crypto;
+
+use Cake\TestSuite\TestCase;
+use Cake\Utility\Crypto\Mcrypt;
+
+/**
+ * Mcrypt engine tests.
+ */
+class McryptTest extends TestCase {
+
+/**
+ * Setup function.
+ *
+ * @return void
+ */
+	public function setUp() {
+		parent::setUp();
+		$this->skipIf(!function_exists('mcrypt_encrypt'), 'No mcrypt skipping tests');
+		$this->crypt = new Mcrypt();
+	}
+
+/**
+ * testRijndael method
+ *
+ * @return void
+ */
+	public function testRijndael() {
+		$txt = 'The quick brown fox jumped over the lazy dog.';
+		$key = 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi';
+
+		$result = $this->crypt->rijndael($txt, $key, 'encrypt');
+		$this->assertEquals($txt, $this->crypt->rijndael($result, $key, 'decrypt'));
+
+		$result = $this->crypt->rijndael($key, $txt, 'encrypt');
+		$this->assertEquals($key, $this->crypt->rijndael($result, $txt, 'decrypt'));
+
+		$result = $this->crypt->rijndael('', $key, 'encrypt');
+		$this->assertEquals('', $this->crypt->rijndael($result, $key, 'decrypt'));
+
+		$key = 'this is my key of over 32 chars, yes it is';
+		$result = $this->crypt->rijndael($txt, $key, 'encrypt');
+		$this->assertEquals($txt, $this->crypt->rijndael($result, $key, 'decrypt'));
+	}
+
+/**
+ * Test encrypt/decrypt.
+ *
+ * @return void
+ */
+	public function testEncryptDecrypt() {
+		$txt = 'The quick brown fox';
+		$key = 'This key is enough bytes';
+		$result = $this->crypt->encrypt($txt, $key);
+		$this->assertNotEquals($txt, $result, 'Should be encrypted.');
+		$this->assertNotEquals($result, $this->crypt->encrypt($txt, $key), 'Each result is unique.');
+		$this->assertEquals($txt, $this->crypt->decrypt($result, $key));
+	}
+
+/**
+ * Test that changing the key causes decryption to fail.
+ *
+ * @return void
+ */
+	public function testDecryptKeyFailure() {
+		$txt = 'The quick brown fox';
+
+		$key = substr(hash('sha256', 'This key is enough bytes'), 0, 32);
+		$result = $this->crypt->encrypt($txt, $key);
+
+		$key = substr(hash('sha256', 'Not the same key.'), 0, 32);
+		$this->assertFalse($this->crypt->decrypt($txt, $key), 'Modified key will fail.');
+	}
+
+/**
+ * Ensure that data encrypted with 2.x encrypt() function can be decrypted with mcrypt engine.
+ *
+ * The $cipher variable is base64 encoded data from 2.x encrypt()
+ *
+ * @return
+ */
+	public function testDecryptOldData() {
+		$key = 'My password is nice and long really it is';
+		$key = substr(hash('sha256', $key), 0, 32);
+
+		$cipher = 'ZmFkMjdmY2U2NjgzOTkwMGZmMWJiMzY0ZDA5ZDUwZmNjYTdjNWVkZThkMzhmNzdiY' .
+			'Tg3ZDFjMzNjNmViMDljMnk9k0LmYpwSZH5eq7GmDozMwHxzh37YaXFQ2TK5gXb5OfTKXv83K+NjAS9lIo/Zvw==';
+		$data = base64_decode($cipher);
+		$cipher = substr($data, 64);
+
+		$result = $this->crypt->decrypt($cipher, $key);
+		$this->assertEquals('This is a secret message', $result);
+	}
+
+}

+ 77 - 0
tests/TestCase/Utility/Crypto/OpenSslTest.php

@@ -0,0 +1,77 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Utility\Crypto;
+
+use Cake\TestSuite\TestCase;
+use Cake\Utility\Crypto\OpenSsl;
+
+/**
+ * Openssl engine tests.
+ */
+class OpenSslTest extends TestCase {
+
+/**
+ * Setup function.
+ *
+ * @return void
+ */
+	public function setUp() {
+		parent::setUp();
+		$this->skipIf(!function_exists('openssl_encrypt'), 'No openssl skipping tests');
+		$this->crypt = new OpenSsl();
+	}
+
+/**
+ * testRijndael method
+ *
+ * @expectedException \LogicException
+ * @return void
+ */
+	public function testRijndael() {
+		$txt = 'The quick brown fox jumped over the lazy dog.';
+		$key = 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi';
+
+		$this->crypt->rijndael($txt, $key, 'encrypt');
+	}
+
+/**
+ * Test encrypt/decrypt.
+ *
+ * @return void
+ */
+	public function testEncryptDecrypt() {
+		$txt = 'The quick brown fox';
+		$key = 'This key is enough bytes';
+		$result = $this->crypt->encrypt($txt, $key);
+		$this->assertNotEquals($txt, $result, 'Should be encrypted.');
+		$this->assertNotEquals($result, $this->crypt->encrypt($txt, $key), 'Each result is unique.');
+		$this->assertEquals($txt, $this->crypt->decrypt($result, $key));
+	}
+
+/**
+ * Test that changing the key causes decryption to fail.
+ *
+ * @return void
+ */
+	public function testDecryptKeyFailure() {
+		$txt = 'The quick brown fox';
+		$key = 'This key is enough bytes';
+		$result = $this->crypt->encrypt($txt, $key);
+
+		$key = 'Not the same key.';
+		$this->assertFalse($this->crypt->decrypt($txt, $key), 'Modified key will fail.');
+	}
+
+}

+ 33 - 3
tests/TestCase/Utility/SecurityTest.php

@@ -15,6 +15,8 @@
 namespace Cake\Test\TestCase\Utility;
 
 use Cake\TestSuite\TestCase;
+use Cake\Utility\Crypto\Mcrypt;
+use Cake\Utility\Crypto\OpenSsl;
 use Cake\Utility\Security;
 
 /**
@@ -82,21 +84,23 @@ class SecurityTest extends TestCase {
  */
 	public function testRijndael() {
 		$this->skipIf(!function_exists('mcrypt_encrypt'));
+		$engine = Security::engine();
+
+		Security::engine(new Mcrypt());
 		$txt = 'The quick brown fox jumped over the lazy dog.';
 		$key = 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi';
 
 		$result = Security::rijndael($txt, $key, 'encrypt');
 		$this->assertEquals($txt, Security::rijndael($result, $key, 'decrypt'));
 
-		$result = Security::rijndael($key, $txt, 'encrypt');
-		$this->assertEquals($key, Security::rijndael($result, $txt, 'decrypt'));
-
 		$result = Security::rijndael('', $key, 'encrypt');
 		$this->assertEquals('', Security::rijndael($result, $key, 'decrypt'));
 
 		$key = 'this is my key of over 32 chars, yes it is';
 		$result = Security::rijndael($txt, $key, 'encrypt');
 		$this->assertEquals($txt, Security::rijndael($result, $key, 'decrypt'));
+
+		Security::engine($engine);
 	}
 
 /**
@@ -246,6 +250,32 @@ class SecurityTest extends TestCase {
 	}
 
 /**
+ * Test that values encrypted with open ssl can be decrypted with mcrypt and the reverse.
+ *
+ * @return void
+ */
+	public function testEngineEquivalence() {
+		$restore = Security::engine();
+		$txt = "Obi-wan you're our only hope";
+		$key = 'This is my secret key phrase it is quite long.';
+		$salt = 'A tasty salt that is delicious';
+
+		Security::engine(new Mcrypt());
+		$cipher = Security::encrypt($txt, $key, $salt);
+		$this->assertEquals($txt, Security::decrypt($cipher, $key, $salt));
+
+		Security::engine(new OpenSsl());
+		$this->assertEquals($txt, Security::decrypt($cipher, $key, $salt));
+
+		Security::engine(new OpenSsl());
+		$cipher = Security::encrypt($txt, $key, $salt);
+		$this->assertEquals($txt, Security::decrypt($cipher, $key, $salt));
+
+		Security::engine(new Mcrypt());
+		$this->assertEquals($txt, Security::decrypt($cipher, $key, $salt));
+	}
+
+/**
  * Tests that the salt can be set and retrieved
  *
  * @return void