diff --git a/composer.json b/composer.json index 65160e78..e390d981 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "phpunit/phpunit": "^4 || ^5 || ^6 || ^7", "squizlabs/php_codesniffer": "^3.5", "wp-coding-standards/wpcs": "^2.3.0", - "yoast/phpunit-polyfills": "^0.2.0" + "yoast/wp-test-utils": "^0.2.2" }, "scripts": { "cbf": [ diff --git a/includes/class-syndication-encryption.php b/includes/class-syndication-encryption.php new file mode 100644 index 00000000..a9333db9 --- /dev/null +++ b/includes/class-syndication-encryption.php @@ -0,0 +1,47 @@ +encryptor = $encryptor; + } + + /** + * Given $data, encrypt it using a Syndication_Encryptor and return the encrypted string. + * + * @param string|array|object $data the data to be encrypted. + * + * @return false|string + */ + public function encrypt( $data ) { + return $this->encryptor->encrypt( $data ); + } + + /** + * Decrypts an encrypted $data using a Syndication_Encryptor, and returns the decrypted object. + * + * @param string $data The encrypted data. + * @param bool $associative If true, returns as an associative array. Otherwise returns as a class. + * + * @return false|array|object + */ + public function decrypt( $data, $associative = true ) { + return $this->encryptor->decrypt( $data, $associative ); + } + +} diff --git a/includes/class-syndication-encryptor-mcrypt.php b/includes/class-syndication-encryptor-mcrypt.php new file mode 100644 index 00000000..24b48c23 --- /dev/null +++ b/includes/class-syndication-encryptor-mcrypt.php @@ -0,0 +1,36 @@ +get_cipher(); + + if ( ! $cipher ) { + return $data; + } + + $encrypted_data = openssl_encrypt( $data, $cipher['cipher'], $cipher['key'], 0, $cipher['iv'] ); + return base64_encode( $encrypted_data ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + } + + /** + * @inheritDoc + */ + public function decrypt( $data, $associative = true ) { + $cipher = $this->get_cipher(); + + if ( ! $cipher ) { + return $data; + } + + $data = openssl_decrypt( base64_decode( $data ), $cipher['cipher'], $cipher['key'], 0, $cipher['iv'] ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode + + if ( ! $data ) { + return false; + } + + return json_decode( $data, $associative ); + } + + /** + * @inheritDoc + */ + public function get_cipher() { + if ( in_array( $this->cipher, openssl_get_cipher_methods(), true ) ) { + return array( + 'cipher' => $this->cipher, + 'iv' => substr( md5( md5( PUSH_SYNDICATE_KEY ) ), 0, 16 ), + 'key' => md5( PUSH_SYNDICATE_KEY ), + ); + } + + return false; // @TODO: return another default cipher? return exception? + } +} diff --git a/includes/class-wp-push-syndication-server.php b/includes/class-wp-push-syndication-server.php index 5da03d62..84b2d4ad 100644 --- a/includes/class-wp-push-syndication-server.php +++ b/includes/class-wp-push-syndication-server.php @@ -1225,7 +1225,7 @@ public function cron_add_pull_time_interval( $schedules ) { // Adds the custom time interval to the existing schedules. $schedules['syn_pull_time_interval'] = array( - 'interval' => intval( $this->push_syndicate_settings['pull_time_interval'] ), + 'interval' => isset( $this->push_syndicate_settings ) ? intval( $this->push_syndicate_settings['pull_time_interval'] ) : 0, 'display' => __( 'Pull Time Interval', 'push-syndication' ) ); diff --git a/includes/interface-syndication-encryptor.php b/includes/interface-syndication-encryptor.php new file mode 100644 index 00000000..fd1ce2f8 --- /dev/null +++ b/includes/interface-syndication-encryptor.php @@ -0,0 +1,33 @@ +encrypt( $data ); } -function push_syndicate_decrypt( $data ) { - - $data = rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5(PUSH_SYNDICATE_KEY), base64_decode($data), MCRYPT_MODE_CBC, md5(md5(PUSH_SYNDICATE_KEY))), "\0"); - if ( !$data ) - return false; - - return @unserialize( $data ); - -} \ No newline at end of file +/** + * Decrypts data. + * + * @param string $data The encrypted data to decrypt. + * @param bool $associative If true, returns as an associative array. Otherwise returns as a class. + * + * @return array|false|object + */ +function push_syndicate_decrypt( $data, $associative = true ) { + global $push_syndication_encryption; // @todo: move from global to WP_Push_Syndication_Server attribute + return $push_syndication_encryption->decrypt( $data, $associative ); +} diff --git a/push-syndication.php b/push-syndication.php index 0f57d873..f58745fe 100644 --- a/push-syndication.php +++ b/push-syndication.php @@ -11,21 +11,23 @@ define( 'SYNDICATION_VERSION', 2.0 ); -if ( ! defined( 'PUSH_SYNDICATE_KEY' ) ) +if ( ! defined( 'PUSH_SYNDICATE_KEY' ) ) { define( 'PUSH_SYNDICATE_KEY', 'PUSH_SYNDICATE_KEY' ); +} /** * Load syndication logger */ -require_once( dirname( __FILE__ ) . '/includes/class-syndication-logger.php' ); +require_once dirname( __FILE__ ) . '/includes/class-syndication-logger.php'; Syndication_Logger::init(); -require_once( dirname( __FILE__ ) . '/includes/class-wp-push-syndication-server.php' ); +require_once dirname( __FILE__ ) . '/includes/class-wp-push-syndication-server.php'; -if ( defined( 'WP_CLI' ) && WP_CLI ) - require_once( dirname( __FILE__ ) . '/includes/class-wp-cli.php' ); +if ( defined( 'WP_CLI' ) && WP_CLI ) { + require_once dirname( __FILE__ ) . '/includes/class-wp-cli.php'; +} -$GLOBALS['push_syndication_server'] = new WP_Push_Syndication_Server; +$GLOBALS['push_syndication_server'] = new WP_Push_Syndication_Server(); // Create the event counter. require __DIR__ . '/includes/class-syndication-event-counter.php'; @@ -38,3 +40,20 @@ // Create the site auto retry functionality require __DIR__ . '/includes/class-syndication-site-auto-retry.php'; new Failed_Syndication_Auto_Retry(); + +// Load encryption classes. +require_once dirname( __FILE__ ) . '/includes/class-syndication-encryption.php'; +require_once dirname( __FILE__ ) . '/includes/interface-syndication-encryptor.php'; +require_once dirname( __FILE__ ) . '/includes/class-syndication-encryptor-mcrypt.php'; +require_once dirname( __FILE__ ) . '/includes/class-syndication-encryptor-openssl.php'; + +// On PHP 7.1 mcrypt is available, but will throw a deprecated error if its used. Therefore, checking for the +// PHP version, instead of checking for mcrypt is a better approach. +if ( ! defined( 'PHP_VERSION_ID' ) || PHP_VERSION_ID < 70100 ) { + $syndication_encryption = new Syndication_Encryption( new Syndication_Encryptor_MCrypt() ); +} else { + $syndication_encryption = new Syndication_Encryption( new Syndication_Encryptor_OpenSSL() ); +} + +// @TODO: instead of saving this as a global, have it as an attribute of WP_Push_Syndication_Server +$GLOBALS['push_syndication_encryption'] = $syndication_encryption; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f9ca33c8..35c1f71a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,9 +1,12 @@ simple_string = 'this is a simple string!'; + $this->complex_array = array( + 'element' => 'this is a element', + 'group' => array( + 'another', + 'sub', + 'array', + 'info' => 'test', + ), + '', + 145, + 1 => 20.04, + 3 => true, + ); + } + + /** + * Test a simple string encryption + */ + public function test_simple_encryption() { + $encrypted = $this->encryptor->encrypt( $this->simple_string ); + + self::assertIsString( $encrypted, 'assert if the string encryption returns string' ); + self::assertEquals( base64_encode( base64_decode( $encrypted ) ), $encrypted, 'assert if the encrypted data is encoded in base64' ); + + return $encrypted; + } + + /** + * Test a simple string decryption. + * + * @param string $encrypted The encrypted string from the previous test. + * + * @depends test_simple_encryption + */ + public function test_simple_decryption( $encrypted ) { + $decrypted = $this->encryptor->decrypt( $encrypted ); + self::assertEquals( $this->simple_string, $decrypted ); + } + + /** + * Test a complex (array) encryption. + */ + public function test_complex_encryption() { + $encrypted = $this->encryptor->encrypt( $this->complex_array ); + + self::assertIsString( $encrypted, 'assert if the array encryption returns string' ); + self::assertEquals( base64_encode( base64_decode( $encrypted ) ), $encrypted, 'assert if the encrypted data is encoded in base64' ); + + return $encrypted; + } + + /** + * Test an array decryption. + * + * @param string $encrypted The encrypted string from the previous test. + * + * @depends test_complex_encryption + */ + public function test_complex_decryption( $encrypted ) { + $decrypted = $this->encryptor->decrypt( $encrypted ); + + self::assertIsArray( $decrypted, 'assert if the decrypted data is an array' ); + self::assertEquals( $this->complex_array, $decrypted, 'assert if the decrypted array is equal to the original array' ); + + // Test without associative set to true. + $decrypted = $this->encryptor->decrypt( $encrypted, false ); + self::assertIsObject( $decrypted, 'assert if the decrypted data is an object' ); + self::assertEquals( $decrypted->element, $this->complex_array['element'], 'assert if the first element is the same' ); + + } + + /** + * Test the expected cipher + */ + abstract public function test_cipher(); + +} diff --git a/tests/test-encryption.php b/tests/test-encryption.php new file mode 100644 index 00000000..2714234f --- /dev/null +++ b/tests/test-encryption.php @@ -0,0 +1,96 @@ +simple_string = 'this is a simple string!'; + $this->complex_array = array( + 'element' => 'this is a element', + 'group' => array( + 'another', + 'sub', + 'array', + 'info' => 'test', + ), + '', + 145, + 1 => 20.04, + 3 => true, + ); + } + + /** + * Test if the `encrypt` method on Syndication_Encryption calls the `encrypt` method on the specific Syndication_Encryptor + */ + public function test_encrypt_method_is_called_on_encryptor_object() { + $fake_encrypted_string = 'I\'m an encrypted string.'; + + $mock_encryptor = $this->createMock( \Syndication_Encryptor::class ); + $mock_encryptor->method( 'encrypt' )->will( $this->returnValue( $fake_encrypted_string ) ); + + $syndication_encryption = new \Syndication_Encryption( $mock_encryptor ); + + self::assertSame( $fake_encrypted_string, $syndication_encryption->encrypt( 'I am a plain-text string' ) ); + } + + /** + * Test if the `decrypt` method on Syndication_Encryption calls the `decrypt` method on the specific Syndication_Encryptor + */ + public function test_decrypt_method_is_called_on_encryptor_object() { + $fake_plain_text_string = 'I am a plain-text string.'; + + $mock_encryptor = $this->createMock( \Syndication_Encryptor::class ); + $mock_encryptor->method( 'decrypt' )->will( $this->returnValue( $fake_plain_text_string ) ); + + $syndication_encryption = new \Syndication_Encryption( $mock_encryptor ); + + self::assertSame( $fake_plain_text_string, $syndication_encryption->decrypt( 'I\'m an encrypted string.' ) ); + } + + /** + * Tests the encryption functions + */ + public function test_encryption() { + $encrypted_simple = push_syndicate_encrypt( $this->simple_string ); + $encrypted_simple_different = push_syndicate_encrypt( $this->simple_string . '1' ); + $encrypted_complex = push_syndicate_encrypt( $this->complex_array ); + + self::assertIsString( $encrypted_simple, 'assert if the string is encrypted' ); + self::assertIsString( $encrypted_complex, 'assert if the array is encrypted' ); + + self::assertNotEquals( $encrypted_simple, $encrypted_complex, 'assert that the two different objects have different results' ); + self::assertNotEquals( $encrypted_simple, $encrypted_simple_different, 'assert that the two different strings have different results' ); + + return array( $encrypted_simple, $encrypted_complex ); + } + + /** + * Tests the decryption functions + * + * @param array[2] $array_encrypted Array with the encrypted data. First element is a string, second element is array. + * + * @depends test_encryption + */ + public function test_decryption( $array_encrypted ) { + $decrypted_simple = push_syndicate_decrypt( $array_encrypted[0] ); + $decrypted_complex_array = push_syndicate_decrypt( $array_encrypted[1] ); + + self::assertEquals( $this->simple_string, $decrypted_simple, 'asserts if the decrypted string is the same as the original' ); + + self::assertIsArray( $decrypted_complex_array, 'asserts if the decrypted complex data was decrypted as an array' ); + self::assertEquals( $this->complex_array, $decrypted_complex_array, 'check if the decrypted array is the same as the original' ); + } + +} diff --git a/tests/test-encryptor-mcrypt.php b/tests/test-encryptor-mcrypt.php new file mode 100644 index 00000000..4480982e --- /dev/null +++ b/tests/test-encryptor-mcrypt.php @@ -0,0 +1,67 @@ +encryptor = new \Syndication_Encryptor_MCrypt(); + + // Disable deprecation warning for this test, as it will run on PHP 7.1. This test will only ensure functionality of the + // Syndication_Encryptor_MCrypt class. + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting + $this->error_reporting = error_reporting( error_reporting() & ~E_DEPRECATED ); + } + + /** + * Runs after the test. + */ + public function tearDown() { + // Restore original error reporting. + error_reporting( $this->error_reporting ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting + parent::tearDown(); + } + + /** + * Test a complex (array) encryption. + */ + public function test_complex_encryption() { + return parent::test_complex_encryption(); + } + + /** + * Test an array decryption. + * + * @param string $encrypted The encrypted string from the previous test. + * + * @depends test_complex_encryption + */ + public function test_complex_decryption( $encrypted ) { + $decrypted = $this->encryptor->decrypt( $encrypted ); + + self::assertIsArray( $decrypted, 'assert if the decrypted data is an array' ); + self::assertEquals( $this->complex_array, $decrypted, 'assert if the decrypted array is equal to the original array' ); + } + + /** + * Test the expected cipher for mcrypt + */ + public function test_cipher() { + $expected_cipher = MCRYPT_RIJNDAEL_256; // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.mcrypt_rijndael_256DeprecatedRemoved + $cipher = $this->encryptor->get_cipher(); + + self::assertSame( $expected_cipher, $cipher ); + } + +} diff --git a/tests/test-encryptor-openssl.php b/tests/test-encryptor-openssl.php new file mode 100644 index 00000000..fd5712d8 --- /dev/null +++ b/tests/test-encryptor-openssl.php @@ -0,0 +1,47 @@ +encryptor = new \Syndication_Encryptor_OpenSSL(); + } + + /** + * Test the expected cipher for openssl + */ + public function test_cipher() { + // Test the cipher. + $cipher_data = $this->encryptor->get_cipher(); + + // Test if is an array. + self::assertIsArray( $cipher_data, 'assert if the cipher data is array' ); + self::assertCount( 3, $cipher_data, 'assert if cipher data have three elements' ); + + $cipher = $cipher_data['cipher']; + $iv = $cipher_data['iv']; + $key = $cipher_data['key']; + + // test cipher. + $expected_cipher = 'aes-256-cbc'; + self::assertEquals( $expected_cipher, $cipher, 'assert if cipher is available' ); + + // test key. + self::assertEquals( $key, md5( PUSH_SYNDICATE_KEY ), 'assert if the key is generated as expected' ); + + // test iv. + self::assertEquals( 16, strlen( $iv ), 'assert iv size (must be 16)' ); + $generated_iv = substr( md5( md5( PUSH_SYNDICATE_KEY ) ), 0, 16 ); + self::assertEquals( $generated_iv, $iv, 'assert if generated iv is as expected' ); + } +}