diff --git a/src/Bundle/ChillDocStoreBundle/Exception/StoredObjectManagerException.php b/src/Bundle/ChillDocStoreBundle/Exception/StoredObjectManagerException.php new file mode 100644 index 000000000..b5d0bf679 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Exception/StoredObjectManagerException.php @@ -0,0 +1,40 @@ +client = $client; + $this->tempUrlGenerator = $tempUrlGenerator; + } + + public function read(StoredObject $document): string + { + try { + $response = $this + ->client + ->request( + Request::METHOD_GET, + $this + ->tempUrlGenerator + ->generate( + Request::METHOD_GET, + $document->getFilename() + ) + ->url + ); + } catch (Throwable $e) { + throw StoredObjectManagerException::errorDuringHttpRequest($e); + } + + if ($response->getStatusCode() !== Response::HTTP_OK) { + throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode()); + } + + try { + $data = $response->getContent(); + } catch (Throwable $e) { + throw StoredObjectManagerException::unableToGetResponseContent($e); + } + + if (false === $this->hasKeysAndIv($document)) { + return $data; + } + + $clearData = openssl_decrypt( + $data, + self::ALGORITHM, + // TODO: Why using this library and not use base64_decode() ? + Base64Url::decode($document->getKeyInfos()['k']), + OPENSSL_RAW_DATA, + pack('C*', ...$document->getIv()) + ); + + if (false === $clearData) { + throw StoredObjectManagerException::unableToDecrypt(openssl_error_string()); + } + + return $clearData; + } + + public function write(StoredObject $document, string $clearContent): void + { + $encryptedContent = $this->hasKeysAndIv($document) + ? openssl_encrypt( + $clearContent, + self::ALGORITHM, + // TODO: Why using this library and not use base64_decode() ? + Base64Url::decode($document->getKeyInfos()['k']), + OPENSSL_RAW_DATA, + pack('C*', ...$document->getIv()) + ) + : $clearContent; + + try { + $response = $this + ->client + ->request( + Request::METHOD_PUT, + $this + ->tempUrlGenerator + ->generate( + Request::METHOD_PUT, + $document->getFilename() + ) + ->url, + [ + 'body' => $encryptedContent, + ] + ); + } catch (TransportExceptionInterface $exception) { + throw StoredObjectManagerException::errorDuringHttpRequest($exception); + } + + if ($response->getStatusCode() !== Response::HTTP_CREATED) { + throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode()); + } + } + + private function hasKeysAndIv(StoredObject $storedObject): bool + { + return ([] !== $storedObject->getKeyInfos()) && ([] !== $storedObject->getIv()); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php new file mode 100644 index 000000000..3cf67cb0c --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php @@ -0,0 +1,34 @@ +setFilename('encrypted.txt') + ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) + ->setIv(unpack('C*', 'abcdefghijklmnop')), + hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string + 'The quick brown fox jumps over the lazy dog', // clear + ]; + + // Non-encrypted object + yield [ + (new StoredObject())->setFilename('non-encrypted.txt'), // The StoredObject + 'The quick brown fox jumps over the lazy dog', // Encrypted + 'The quick brown fox jumps over the lazy dog', // Clear + ]; + + /* UNHAPPY SCENARIO */ + + // Encrypted object with issue during HTTP communication + yield [ + (new StoredObject()) + ->setFilename('error_during_http_request.txt') + ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) + ->setIv(unpack('C*', 'abcdefghijklmnop')), + hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string + 'The quick brown fox jumps over the lazy dog', // clear + StoredObjectManagerException::class, + ]; + + // Encrypted object with issue during HTTP communication: Invalid status code + yield [ + (new StoredObject()) + ->setFilename('invalid_statuscode.txt') + ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) + ->setIv(unpack('C*', 'abcdefghijklmnop')), + hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string + 'The quick brown fox jumps over the lazy dog', // clear + StoredObjectManagerException::class, + ]; + + // Erroneous encrypted: Unable to decrypt exception. + yield [ + (new StoredObject()) + ->setFilename('unable_to_decrypt.txt') + ->setKeyInfos(['k' => base64_encode('WRONG_PASS_PHRASE')]) + ->setIv(unpack('C*', 'abcdefghijklmnop')), + 'WRONG_ENCODED_VALUE', // Binary encoded string + 'The quick brown fox jumps over the lazy dog', // clear + StoredObjectManagerException::class, + ]; + } + + /** + * @dataProvider getDataProvider + */ + public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null) + { + if (null !== $exceptionClass) { + $this->expectException($exceptionClass); + } + + $storedObjectManager = $this->getSubject($storedObject, $encodedContent); + + self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); + } + + /** + * @dataProvider getDataProvider + */ + public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null) + { + if (null !== $exceptionClass) { + $this->expectException($exceptionClass); + } + + $storedObjectManager = $this->getSubject($storedObject, $encodedContent); + + $storedObjectManager->write($storedObject, $clearContent); + + self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); + } + + private function getHttpClient(string $encodedContent): HttpClientInterface + { + $callback = static function ($method, $url, $options) use ($encodedContent) { + if (Request::METHOD_GET === $method) { + switch ($url) { + case 'https://example.com/non-encrypted.txt': + case 'https://example.com/encrypted.txt': + return new MockResponse($encodedContent, ['http_code' => 200]); + + case 'https://example.com/error_during_http_request.txt': + return new TransportException('error_during_http_request.txt'); + + case 'https://example.com/invalid_statuscode.txt': + return new MockResponse($encodedContent, ['http_code' => 404]); + } + } + + if (Request::METHOD_PUT === $method) { + switch ($url) { + case 'https://example.com/non-encrypted.txt': + case 'https://example.com/encrypted.txt': + return new MockResponse($encodedContent, ['http_code' => 201]); + + case 'https://example.com/error_during_http_request.txt': + throw new TransportException('error_during_http_request.txt'); + + case 'https://example.com/invalid_statuscode.txt': + return new MockResponse($encodedContent, ['http_code' => 404]); + } + } + + return new MockResponse('Not found'); + }; + + return new MockHttpClient($callback); + } + + private function getSubject(StoredObject $storedObject, string $encodedContent): StoredObjectManagerInterface + { + return new StoredObjectManager( + $this->getHttpClient($encodedContent), + $this->getTempUrlGenerator($storedObject) + ); + } + + private function getTempUrlGenerator(StoredObject $storedObject): TempUrlGeneratorInterface + { + $response = new stdClass(); + $response->url = $storedObject->getFilename(); + + $tempUrlGenerator = $this + ->getMockBuilder(TempUrlGeneratorInterface::class) + ->getMock(); + + $tempUrlGenerator + ->method('generate') + ->withAnyParameters() + ->willReturn($response); + + return $tempUrlGenerator; + } +}