From 1f1f06fcd6575c9d7d3f37e2abddda71629e1287 Mon Sep 17 00:00:00 2001 From: William Jehanne Date: Wed, 28 Aug 2019 17:46:56 +0200 Subject: [PATCH] feat(encryption): add EncryptionException to detect openssl_encrypt and openssl_decrypt failures --- Controller/LogsAdminController.php | 9 +- Encryptor/Encryptor.php | 13 +- Encryptor/EncryptorInterface.php | 6 + Exception/EncryptionException.php | 23 ++++ .../EkinoDataProtectionBundle.en.xliff | 8 ++ .../EkinoDataProtectionBundle.fr.xliff | 8 ++ Tests/Controller/LogsAdminControllerTest.php | 115 ++++++++++++++++++ Tests/Encryptor/EncryptorTest.php | 60 ++++++++- 8 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 Exception/EncryptionException.php create mode 100644 Tests/Controller/LogsAdminControllerTest.php diff --git a/Controller/LogsAdminController.php b/Controller/LogsAdminController.php index 93e5895..b55853f 100644 --- a/Controller/LogsAdminController.php +++ b/Controller/LogsAdminController.php @@ -14,6 +14,7 @@ namespace Ekino\DataProtectionBundle\Controller; use Ekino\DataProtectionBundle\Encryptor\EncryptorInterface; +use Ekino\DataProtectionBundle\Exception\EncryptionException; use Ekino\DataProtectionBundle\Form\Type\LogType; use Sonata\AdminBundle\Controller\CRUDController as Controller; use Symfony\Component\HttpFoundation\Request; @@ -56,7 +57,13 @@ public function decryptEncryptAction(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { $log = $form->getData(); $content = $log->getContent(); - $results = $log->isDecryptAction() ? $this->getDecryptedResults($content) : $this->getEncryptedResult($content); + try { + $results = $log->isDecryptAction() ? $this->getDecryptedResults($content) : $this->getEncryptedResult($content); + } catch (EncryptionException $e) { + $message = $log->isDecryptAction() ? 'admin.logs.decrypt.error' : 'admin.logs.encrypt.error'; + + $this->addFlash('error', $this->trans($message, [], 'EkinoDataProtectionBundle')); + } } return $this->renderWithExtraParams('@EkinoDataProtection/LogsAdmin/decrypt.html.twig', [ diff --git a/Encryptor/Encryptor.php b/Encryptor/Encryptor.php index a28ca70..6556063 100644 --- a/Encryptor/Encryptor.php +++ b/Encryptor/Encryptor.php @@ -13,6 +13,8 @@ namespace Ekino\DataProtectionBundle\Encryptor; +use Ekino\DataProtectionBundle\Exception\EncryptionException; + /** * Encrypt data using the given cipher method. * @@ -52,6 +54,10 @@ public function encrypt(string $data): string $iv = openssl_random_pseudo_bytes($ivSize); $cipherText = openssl_encrypt($data, $this->method, $this->secret, OPENSSL_RAW_DATA, $iv); + if ($cipherText === false) { + throw new EncryptionException('Unexpected failure in openssl_encrypt.'); + } + return base64_encode($iv.$cipherText); } @@ -64,7 +70,12 @@ public function decrypt(string $data): string $ivSize = openssl_cipher_iv_length($this->method); $iv = mb_substr($data, 0, $ivSize, '8bit'); $cipherText = mb_substr($data, $ivSize, null, '8bit'); + $decrypt = openssl_decrypt($cipherText, $this->method, $this->secret, OPENSSL_RAW_DATA, $iv); + + if ($decrypt === false) { + throw new EncryptionException('Unexpected failure in openssl_decrypt.'); + } - return openssl_decrypt($cipherText, $this->method, $this->secret, OPENSSL_RAW_DATA, $iv); + return $decrypt; } } diff --git a/Encryptor/EncryptorInterface.php b/Encryptor/EncryptorInterface.php index c5e9fca..bc7f1ea 100644 --- a/Encryptor/EncryptorInterface.php +++ b/Encryptor/EncryptorInterface.php @@ -13,6 +13,8 @@ namespace Ekino\DataProtectionBundle\Encryptor; +use Ekino\DataProtectionBundle\Exception\EncryptionException; + /** * @author Rémi Marseille */ @@ -21,6 +23,8 @@ interface EncryptorInterface /** * @param string $data * + * @throws EncryptionException + * * @return string */ public function encrypt(string $data): string; @@ -28,6 +32,8 @@ public function encrypt(string $data): string; /** * @param string $data * + * @throws EncryptionException + * * @return string */ public function decrypt(string $data): string; diff --git a/Exception/EncryptionException.php b/Exception/EncryptionException.php new file mode 100644 index 0000000..5ccc2d1 --- /dev/null +++ b/Exception/EncryptionException.php @@ -0,0 +1,23 @@ + + */ +class EncryptionException extends \RuntimeException +{ +} diff --git a/Resources/translations/EkinoDataProtectionBundle.en.xliff b/Resources/translations/EkinoDataProtectionBundle.en.xliff index 34d8c95..e5b5d49 100644 --- a/Resources/translations/EkinoDataProtectionBundle.en.xliff +++ b/Resources/translations/EkinoDataProtectionBundle.en.xliff @@ -34,6 +34,14 @@ admin.logs.title Logs + + admin.logs.decrypt.error + An error occurred during the decryption, please check the content you submitted. + + + admin.logs.encrypt.error + An error occurred during the encryption, please check the content you submitted. + diff --git a/Resources/translations/EkinoDataProtectionBundle.fr.xliff b/Resources/translations/EkinoDataProtectionBundle.fr.xliff index 5907064..21a9003 100644 --- a/Resources/translations/EkinoDataProtectionBundle.fr.xliff +++ b/Resources/translations/EkinoDataProtectionBundle.fr.xliff @@ -34,6 +34,14 @@ admin.logs.title Logs + + admin.logs.decrypt.error + Une erreur s'est produite pendant le déchiffrement, veuillez vérifier le contenu que vous avez soumis. + + + admin.logs.encrypt.error + Une erreur s'est produite pendant le chiffrement, veuillez vérifier le contenu que vous avez soumis. + diff --git a/Tests/Controller/LogsAdminControllerTest.php b/Tests/Controller/LogsAdminControllerTest.php new file mode 100644 index 0000000..d70012b --- /dev/null +++ b/Tests/Controller/LogsAdminControllerTest.php @@ -0,0 +1,115 @@ + + */ +class LogsAdminControllerTest extends TestCase +{ + /** + * @var LogsAdminController|MockObject + */ + private $controller; + + /** + * @var EncryptorInterface|MockObject + */ + private $encryptor; + + /** + * Initialize test LogsAdminControllerTest. + */ + protected function setUp(): void + { + $this->encryptor = $this->createMock(EncryptorInterface::class); + $this->controller = $this->getMockBuilder(LogsAdminController::class) + ->setConstructorArgs([$this->encryptor]) + ->disableOriginalClone() + ->disableArgumentCloning() + ->disallowMockingUnknownTypes() + ->setMethods(['addFlash', 'createForm', 'get', 'renderWithExtraParams']) + ->getMock(); + } + + /** + * Test decryptEncryptAction method of LogsAdminController. + */ + public function testDecryptEncryptAction(): void + { + $form = $this->createMock(Form::class); + $log = $this->createMock(Log::class); + $log->expects($this->once())->method('getContent')->willReturn('foo'); + + $response = $this->createMock(Response::class); + $this->controller->expects($this->never())->method('addFlash'); + $this->controller->expects($this->once())->method('renderWithExtraParams')->willReturn($response); + + $form->expects($this->once())->method('handleRequest'); + $form->expects($this->once())->method('isSubmitted')->willReturn(true); + $form->expects($this->once())->method('isValid')->willReturn(true); + $form->expects($this->once())->method('getData')->willReturn($log); + + $this->controller->expects($this->once())->method('createForm')->willReturn($form); + + $request = $this->createMock(Request::class); + + $this->controller->decryptEncryptAction($request); + } + + /** + * Test decryptEncryptAction method of LogsAdminController not ok. + */ + public function testDecryptEncryptActionNok(): void + { + $this->encryptor->expects($this->any())->method('encrypt')->willThrowException(new EncryptionException()); + + $form = $this->createMock(Form::class); + $log = $this->createMock(Log::class); + $log->expects($this->once())->method('getContent')->willReturn('foo'); + $log->expects($this->any())->method('isDecryptAction')->willReturn(false); + + $response = $this->createMock(Response::class); + $this->controller->expects($this->once())->method('addFlash')->with('error'); + $this->controller->expects($this->once())->method('renderWithExtraParams')->willReturn($response); + + $form->expects($this->once())->method('handleRequest'); + $form->expects($this->once())->method('isSubmitted')->willReturn(true); + $form->expects($this->once())->method('isValid')->willReturn(true); + $form->expects($this->once())->method('getData')->willReturn($log); + + $this->controller->expects($this->once())->method('createForm')->willReturn($form); + + $request = $this->createMock(Request::class); + + $translator = $this->createMock(TranslatorInterface::class); + $translator->expects($this->once())->method('trans')->willReturn('admin.logs.encrypt.error'); + $this->controller->expects($this->once())->method('get')->with($this->equalTo('translator'))->willReturn($translator); + + $this->controller->decryptEncryptAction($request); + } +} diff --git a/Tests/Encryptor/EncryptorTest.php b/Tests/Encryptor/EncryptorTest.php index 4764dbe..6ab3e17 100644 --- a/Tests/Encryptor/EncryptorTest.php +++ b/Tests/Encryptor/EncryptorTest.php @@ -14,6 +14,7 @@ namespace Ekino\DataProtectionBundle\Tests\Encryptor; use Ekino\DataProtectionBundle\Encryptor\Encryptor; +use Ekino\DataProtectionBundle\Exception\EncryptionException; use PHPUnit\Framework\TestCase; /** @@ -24,15 +25,66 @@ */ class EncryptorTest extends TestCase { + /** + * @var string|bool $encryptData + */ + public static $encryptData = true; + + /** + * @var string + */ + private $rawData = 'my raw data'; + + /** + * @var Encryptor + */ + private $encryptor; + + /** + * Initialize test EncryptorTest. + */ + protected function setUp(): void + { + $this->encryptor = new Encryptor('aes-256-cbc', 'foo'); + } + /** * Test encrypt & decrypt. */ public function testEncryptAndDecrypt(): void { - $rawData = 'my raw data'; - $encryptor = new Encryptor('aes-256-cbc', 'foo'); - $encryptedData = $encryptor->encrypt($rawData); + $encryptedData = $this->encryptor->encrypt($this->rawData); + + $this->assertSame($this->rawData, $this->encryptor->decrypt($encryptedData)); + } + + /** + * Test encrypt not ok. + */ + public function testEncryptNok(): void + { + self::$encryptData = false; + + $this->expectException(EncryptionException::class); + $this->expectExceptionMessage('Unexpected failure in openssl_encrypt.'); - $this->assertSame($rawData, $encryptor->decrypt($encryptedData)); + $encryptedData = $this->encryptor->encrypt($this->rawData); } + + /** + * Test decrypt not ok. + */ + public function testDecryptNok(): void + { + $this->expectException(EncryptionException::class); + $this->expectExceptionMessage('Unexpected failure in openssl_decrypt.'); + + $this->encryptor->decrypt('dummy-example-for-testing-purpose'); + } +} + +namespace Ekino\DataProtectionBundle\Encryptor; + +function openssl_encrypt($data, $method, $key, $options, $iv) { + return \Ekino\DataProtectionBundle\Tests\Encryptor\EncryptorTest::$encryptData ? \openssl_encrypt($data, $method, $key, $options, $iv) : false; }