diff --git a/.github/workflows/functionaltests.yml b/.github/workflows/functionaltests.yml index bbf9341..d85400f 100644 --- a/.github/workflows/functionaltests.yml +++ b/.github/workflows/functionaltests.yml @@ -10,12 +10,10 @@ jobs: strategy: fail-fast: false matrix: - php-version: [ 7.3, 7.4, 8.0, 8.1 ] + php-version: [ 7.4, 8.0, 8.1 ] flow-version: [ 7.3, 8.0 ] exclude: # Disable Flow 8.0 on PHP 7, as 8.0 is required - - php-version: 7.3 - flow-version: 8.0 - php-version: 7.4 flow-version: 8.0 diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index c840ccb..f0a5d02 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -10,12 +10,10 @@ jobs: strategy: fail-fast: false matrix: - php-version: [ 7.3, 7.4, 8.0, 8.1 ] + php-version: [ 7.4, 8.0, 8.1 ] flow-version: [ 7.3, 8.0 ] exclude: # Disable Flow 8.0 on PHP 7, as 8.0 is required - - php-version: 7.3 - flow-version: 8.0 - php-version: 7.4 flow-version: 8.0 diff --git a/Classes/Controller/EncryptedPayloadController.php b/Classes/Controller/EncryptedPayloadController.php new file mode 100644 index 0000000..9a61ee1 --- /dev/null +++ b/Classes/Controller/EncryptedPayloadController.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\Controller; + +use function json_decode; +use function json_encode; +use const JSON_PRETTY_PRINT; +use Neos\Flow\Mvc\Controller\ActionController; +use Netlogix\Sentry\Encryption\EncryptionService; +use Netlogix\Sentry\Encryption\Sealed; + +class EncryptedPayloadController extends ActionController +{ + protected EncryptionService $encryptionService; + + public function injectEncryptionService(EncryptionService $encryptionService): void + { + $this->encryptionService = $encryptionService; + } + + /** + * @param string $encryptedData + * @param string $initializationVector + * @param string $envelopeKey + */ + public function decryptAction( + string $encryptedData, + string $initializationVector, + string $envelopeKey + ): string { + $sealed = Sealed::fromArray([ + 'encryptedData' => $encryptedData, + 'initializationVector' => $initializationVector, + 'envelopeKey' => $envelopeKey, + ]); + $unencrypted = $this->encryptionService->open($sealed); + $this->response->setContentType('application/json'); + + return json_encode(json_decode($unencrypted, true), JSON_PRETTY_PRINT); + } +} diff --git a/Classes/Encryption/EncryptionService.php b/Classes/Encryption/EncryptionService.php new file mode 100644 index 0000000..4bd2e3b --- /dev/null +++ b/Classes/Encryption/EncryptionService.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\Encryption; + +use function http_build_query; +use Neos\Flow\Http\BaseUriProvider; +use Neos\Flow\Security\Cryptography\RsaWalletServiceInterface; +use Neos\Flow\Security\Cryptography\RsaWalletServicePhp; +use Neos\Flow\Security\Exception\InvalidKeyPairIdException; +use Neos\Utility\ObjectAccess; +use function openssl_open; +use function openssl_random_pseudo_bytes; +use function openssl_seal; +use Psr\Http\Message\UriInterface; + +class EncryptionService +{ + const ALGORITHM = 'AES256'; + + private string $rsaKeyFingerprint; + + private string $encryptionModuleUri; + + private RsaWalletServiceInterface $rsaWallet; + + private BaseUriProvider $baseUriProvider; + + public function injectSettings(array $settings): void + { + $privacySettings = $settings['privacy'] ?? []; + $this->rsaKeyFingerprint = (string) ($privacySettings['rsaKeyFingerprint'] ?? ''); + $this->encryptionModuleUri = (string) ($privacySettings['encryptionModuleUri'] ?? ''); + } + + public function injectRsaWalletService(RsaWalletServiceInterface $rsaWallet): void + { + $this->rsaWallet = $rsaWallet; + } + + public function injectBaseUriProvider(BaseUriProvider $baseUriProvider): void + { + $this->baseUriProvider = $baseUriProvider; + } + + public function seal(string $unencryptedData): Sealed + { + $publicKeyString = $this->getKeyString('publicKey'); + + $initializationVector = openssl_random_pseudo_bytes(32); + + openssl_seal( + $unencryptedData, + $encryptedData, + $envelopeKeys, + [$publicKeyString], + self::ALGORITHM, + $initializationVector + ); + + return new Sealed($encryptedData, $initializationVector, $envelopeKeys[0]); + } + + public function open(Sealed $package): string + { + $privateKeyString = $this->getKeyString('privateKey'); + + $encryptedData = $package->getEncryptedData(); + $envelopeKey = $package->getEnvelopeKey(); + $initializationVector = $package->getInitializationVector(); + + openssl_open( + $encryptedData, + $unencryptedData, + $envelopeKey, + $privateKeyString, + self::ALGORITHM, + $initializationVector + ); + + return $unencryptedData; + } + + public function getEncryptionUriForSealedPayload(Sealed $sealed): UriInterface + { + return $this->baseUriProvider + ->getConfiguredBaseUriOrFallbackToCurrentRequest() + ->withPath($this->encryptionModuleUri) + ->withQuery(http_build_query($sealed->toArray())); + } + + /** + * @param 'privateKey' | 'publicKey' $slotName + */ + private function getKeyString(string $slotName): string + { + assert($this->rsaWallet instanceof RsaWalletServicePhp); + // Prime key pair, male rsaWallet load the key pair + $this->rsaWallet->getPublicKey($this->rsaKeyFingerprint); + // Private property + $keys = ObjectAccess::getProperty($this->rsaWallet, 'keys', true); + // Property path in plain array + $keyString = ObjectAccess::getPropertyPath( + $keys, + sprintf('%s.%s.keyString', $this->rsaKeyFingerprint, $slotName) + ); + if (!\is_string($keyString)) { + throw new InvalidKeyPairIdException('Invalid key fingerprint given', 1693231337); + } + + return $keyString; + } +} diff --git a/Classes/Encryption/Sealed.php b/Classes/Encryption/Sealed.php new file mode 100644 index 0000000..99012ab --- /dev/null +++ b/Classes/Encryption/Sealed.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\Encryption; + +use function base64_decode; +use function base64_encode; +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class Sealed +{ + private string $encryptedData; + + private string $initializationVector; + + private string $envelopeKey; + + public function __construct(string $encryptedData, string $initializationVector, string $envelopeKeys) + { + $this->encryptedData = $encryptedData; + $this->initializationVector = $initializationVector; + $this->envelopeKey = $envelopeKeys; + } + + public static function fromArray(array $package): self + { + return new self( + $package['encryptedData'] ? base64_decode($package['encryptedData'], true) : '', + $package['initializationVector'] ? base64_decode($package['initializationVector'], true) : '', + $package['envelopeKey'] ? base64_decode($package['envelopeKey'], true) : '' + ); + } + + public function toArray(): array + { + return [ + 'encryptedData' => base64_encode($this->encryptedData), + 'initializationVector' => base64_encode($this->initializationVector), + 'envelopeKey' => base64_encode($this->envelopeKey), + ]; + } + + public function getEncryptedData(): string + { + return $this->encryptedData; + } + + public function getInitializationVector(): string + { + return $this->initializationVector; + } + + public function getEnvelopeKey(): string + { + return $this->envelopeKey; + } +} diff --git a/Classes/EventProcessor/EncryptedPayload.php b/Classes/EventProcessor/EncryptedPayload.php new file mode 100644 index 0000000..cfcc151 --- /dev/null +++ b/Classes/EventProcessor/EncryptedPayload.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\EventProcessor; + +use function json_encode; +use Neos\Flow\Annotations as Flow; +use Netlogix\Sentry\Encryption\EncryptionService; +use Sentry\Event; +use Sentry\EventHint; + +/** + * @Flow\Scope("singleton") + */ +final class EncryptedPayload implements EventProcessor +{ + private EncryptionService $encryption; + + private bool $encryptPostBody; + + public function injectEncryptionService(EncryptionService $encryption): void + { + $this->encryption = $encryption; + } + + public function injectSettings(array $settings): void + { + $privacyCettings = $settings['privacy'] ?? []; + $this->encryptPostBody = (bool) ($privacyCettings['encryptPostBody'] ?? false); + } + + public function rewriteEvent(Event $event, EventHint $hint): Event + { + if (!$this->encryptPostBody) { + return $event; + } + + $request = $event->getRequest(); + + $data = $request['data'] ?? []; + if (!$data || !is_array($data)) { + return $event; + } + + $unencrypted = (string) json_encode($data, \JSON_PRETTY_PRINT); + + $encrypted = $this->encryption + ->seal($unencrypted); + $uri = (string) $this->encryption + ->getEncryptionUriForSealedPayload($encrypted); + + $request['data'] = [ + '__ENCRYPTED__DATA__' => $encrypted->toArray(), + ]; + $event->setRequest($request); + + $extra = $event->getExtra() ?? []; + $extra['Encrypted POST Data'] = $uri; + $event->setExtra($extra); + + return $event; + } +} diff --git a/Classes/EventProcessor/EventProcessor.php b/Classes/EventProcessor/EventProcessor.php new file mode 100644 index 0000000..63ce59c --- /dev/null +++ b/Classes/EventProcessor/EventProcessor.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\EventProcessor; + +use Sentry\Event; +use Sentry\EventHint; + +interface EventProcessor +{ + public function rewriteEvent(Event $event, EventHint $hint): Event; +} diff --git a/Classes/Integration/NetlogixIntegration.php b/Classes/Integration/NetlogixIntegration.php index 3bd43bc..8eecd9f 100644 --- a/Classes/Integration/NetlogixIntegration.php +++ b/Classes/Integration/NetlogixIntegration.php @@ -45,7 +45,10 @@ public function setupOnce(): void return $event; } - return self::handleEvent($event, $hint); + $event = self::handleEvent($event, $hint); + $event = self::encryptPostBody($event, $hint); + + return $event; }); } @@ -178,4 +181,22 @@ private static function configureScopeForEvent(Event $event, EventHint $hint): v } } + /** + * FIXME: Remove this method, replace by yaml configuration once `EventProcessor` chain is implemented + * + * @see https://github.com/netlogix/Netlogix.Sentry/issues/29 + * + * TODO: + * - Crate `EventProcessor` chain as YAML configuration + * - Execute `EventProcessor` (instanciate and call) in self::configureScopeForEvent() + * - Replace Every ScopeProvider::collect*() method by individual classes + * - Configure `EncryptedPayload` as `EventProcessor` in YAML + * - Remove this method + */ + public static function encryptPostBody(Event $event, EventHint $hint): Event + { + $encryptedPayload = Bootstrap::$staticObjectManager->get(\Netlogix\Sentry\EventProcessor\EncryptedPayload::class); + return $encryptedPayload->rewriteEvent($event, $hint); + } + } diff --git a/Configuration/Policy.yaml b/Configuration/Policy.yaml new file mode 100644 index 0000000..713f85f --- /dev/null +++ b/Configuration/Policy.yaml @@ -0,0 +1,20 @@ +privilegeTargets: + + 'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege': + + 'Netlogix.Sentry:Backend.EncryptedPayload': + matcher: 'method(Netlogix\Sentry\Controller\EncryptedPayloadController->.*())' + +roles: + + 'Neos.Flow:Anonymous': + privileges: + - + privilegeTarget: 'Netlogix.Sentry:Backend.EncryptedPayload' + permission: DENY + + 'Neos.Neos:Administrator': + privileges: + - + privilegeTarget: 'Netlogix.Sentry:Backend.EncryptedPayload' + permission: GRANT diff --git a/Configuration/Routes.yaml b/Configuration/Routes.yaml new file mode 100644 index 0000000..ecd4b6a --- /dev/null +++ b/Configuration/Routes.yaml @@ -0,0 +1,9 @@ +- name: 'HashToken Login' + uriPattern: 'neos/sentry/show-parameters' + defaults: + '@package': 'Netlogix.Sentry' + '@controller': 'EncryptedPayload' + '@action': 'decrypt' + '@format': 'html' + appendExceedingArguments: TRUE + httpMethods: ['GET'] diff --git a/Configuration/Settings.Encryption.Backend.yaml b/Configuration/Settings.Encryption.Backend.yaml new file mode 100644 index 0000000..02865fb --- /dev/null +++ b/Configuration/Settings.Encryption.Backend.yaml @@ -0,0 +1,25 @@ +Neos: + Flow: + + mvc: + routes: + 'Netlogix.Sentry': + + # This package should be evaluated before Neos.Neos, but it doesn't depend on Neos.Neos. + position: start 100 + + # Use the following configuration snippet when this package is used in conjunction with the neos/neos CMS. + # Use something similar when another authentication provider will be used. + # Unfortunately, this cannot be done preemptively. + # + # security: + # authentication: + # providers: + # 'Neos.Neos:Backend': + # requestPatterns: + # 'Netlogix.Sentry:ShowEncryptedPayload': + # pattern: ControllerObjectName + # patternOptions: + # controllerObjectNamePattern: 'Netlogix\Sentry\Controller\.*' + + diff --git a/Configuration/Settings.Encryption.yaml b/Configuration/Settings.Encryption.yaml new file mode 100644 index 0000000..a1c9d51 --- /dev/null +++ b/Configuration/Settings.Encryption.yaml @@ -0,0 +1,13 @@ +Netlogix: + Sentry: + privacy: + + # Enable to replace the request POSt body with an encrypted version + encryptPostBody: false + + # Enter a fingerprint of an RSA key stored in the RsaWallet + rsaKeyFingerprint: '' + + # Enter the URI of the encryption module + # No route generation is done during exception handling + encryptionModuleUri: '/neos/sentry/show-parameters' diff --git a/README.md b/README.md index 21fbf3e..a43cd8c 100644 --- a/README.md +++ b/README.md @@ -196,3 +196,73 @@ Neos: ``` Please note that this also disables logging of this exception to `Data/Logs/Exceptions`. + +## Encrypt POST payload + +By default, the array of POST payload data is transported to the sentry server "as is". + +When encryption is enabled and a valid rsa key fingerprint is set, the POST payload is stripped and replaced by an +RSA encrypted string. + +```yaml +Netlogix: + Sentry: + + privacy: + encryptPostBody: true + rsaKeyFingerprint: '6ff568ae0f9b44b69627e275accf163a' +``` + +POST data without encryption usually looks like this in sentry: + +```json +{ + "--some-form": { + "__currentPage": 1, + "__state": "TmV0bG9naXguU2VudHJ5IHN0YXRlIGRhdGE=", + "__trustedProperties": "[Filtered]", + "firstName": "John", + "lastName": "Doe", + "birthday": "2021-01-01", + "email": "john.doe@netlogix.de", + "message": "Lorem ipsum dolor sit amet" + } +} +``` + +With encryption enabled it looks like this: + +```json +{ + "__ENCRYPTED__DATA__": { + "encryptedData": "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2ljaSBlbGl0LCBzZWQgZWl1c21vZCB0ZW1wb3IgaW5jaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9u", + "envelopeKey": "ZGVzZXJ1bnQgbW9sbGl0IGFuaW0gaWQgZXN0IGxhYm9ydW0=", + "initializationVector": "QmxpbmR0ZXh0" + } +} +``` + +There will be an additional sentry field "Encrypted POST Data" which contains a backlink to encrypt and show the +original data. + +In order for this to work, there must be an authentication provider in place that handels the Neos.Sentry controller. + +If this package is used in conjunction with the neos/neos CMS, the neos backend authentication provider can be tasked +with this job. See the code snippet below. + +If this package is used without neos/neos, a custom privilege for policy `Netlogix.Sentry:Backend.EncryptedPayload` +has to be configured. + +```yaml +Neos: + Flow: + security: + authentication: + providers: + 'Neos.Neos:Backend': + requestPatterns: + 'Netlogix.Sentry:ShowEncryptedPayload': + pattern: ControllerObjectName + patternOptions: + controllerObjectNamePattern: 'Netlogix\Sentry\Controller\.*' +``` diff --git a/Tests/Unit/Encryption/EncryptionService/EncryptionServiceTestCase.php b/Tests/Unit/Encryption/EncryptionService/EncryptionServiceTestCase.php new file mode 100644 index 0000000..ff025c9 --- /dev/null +++ b/Tests/Unit/Encryption/EncryptionService/EncryptionServiceTestCase.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\Tests\Unit\Encryption\EncryptionService; + +use Neos\Flow\Security\Cryptography\RsaWalletServicePhp; +use Neos\Flow\Tests\UnitTestCase; + +abstract class EncryptionServiceTestCase extends UnitTestCase +{ + protected const RSA_KEY_FINGERPRINT = '37f0cb0d76d54a9d2d0187a0ec2846e7'; + + protected const RSA_WALLET_FILE = __DIR__ . '/RsaWalletData.bin'; + + protected static function getRsaWalletServicePhp(): RsaWalletServicePhp + { + $wallet = new RsaWalletServicePhp(); + $wallet->injectSettings([ + 'security' => [ + 'cryptography' => [ + 'RSAWalletServicePHP' => [ + 'keystorePath' => self::RSA_WALLET_FILE, + 'paddingAlgorithm' => 0, + ], + ], + ], + ]); + $wallet->initializeObject(); + + return $wallet; + } +} diff --git a/Tests/Unit/Encryption/EncryptionService/OpenTest.php b/Tests/Unit/Encryption/EncryptionService/OpenTest.php new file mode 100644 index 0000000..81375d0 --- /dev/null +++ b/Tests/Unit/Encryption/EncryptionService/OpenTest.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\Tests\Unit\Encryption\EncryptionService; + +use Netlogix\Sentry\Encryption\EncryptionService; +use Netlogix\Sentry\Encryption\Sealed; + +class OpenTest extends EncryptionServiceTestCase +{ + /** + * @test + */ + public function EncryptionService_can_open_sealed_data(): void + { + $encryptionService = new EncryptionService(); + $encryptionService->injectSettings([ + 'privacy' => [ + 'rsaKeyFingerprint' => self::RSA_KEY_FINGERPRINT, + ], + ]); + + $sealed = Sealed::fromArray([ + 'encryptedData' => '8qM4pkKLNxgRy61vffZKwg==', + 'initializationVector' => 'StKlRPeWNOAk5LZ9jtFR3Q==', + 'envelopeKey' => 'Zk4jrCZcHDw8SJv3WgSyfJnkAteG1G8+A4DP3vIQJTbqoPqi6dKJLutqsJLwMfh4vmsp9K/uIud8SFh3M+HwvbCDs5KOnj4ozZTdtmCVKTKm6+6nx3v5zMdte+x0oahUeJqn4QcwDU7vCIDKtuSR1xQXXDkJGznYi/aMN/O20PHRCvuR93ZWD9IYyNd6ETJwqMglP81vWQHngX4uVK0KYXsUB7T5fgj+dISw/2+zSA4WVSjexK+BpZJQjqEwp+Zv1DL5l88LT3bG5l+k/bpDx4sBcDzc8/iwNvKmbngFluZWnxu+CuUjPR4PHyv5oWWBwQ6yy2i/0JAo9FgdGvVVKw==', + ]); + + $encryptionService->injectRsaWalletService(self::getRsaWalletServicePhp()); + $open = $encryptionService->open($sealed); + + self::assertEquals('foo', $open); + } +} diff --git a/Tests/Unit/Encryption/EncryptionService/RsaWalletData.bin b/Tests/Unit/Encryption/EncryptionService/RsaWalletData.bin new file mode 100644 index 0000000..5b94bc1 Binary files /dev/null and b/Tests/Unit/Encryption/EncryptionService/RsaWalletData.bin differ diff --git a/Tests/Unit/Encryption/EncryptionService/SealTest.php b/Tests/Unit/Encryption/EncryptionService/SealTest.php new file mode 100644 index 0000000..51c09c1 --- /dev/null +++ b/Tests/Unit/Encryption/EncryptionService/SealTest.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\Tests\Unit\Encryption\EncryptionService; + +use Netlogix\Sentry\Encryption\EncryptionService; +use Netlogix\Sentry\Encryption\Sealed; + +class SealTest extends EncryptionServiceTestCase +{ + /** + * @test + */ + public function EncryptionService_can_seal_payload(): void + { + $encryptionService = new EncryptionService(); + $encryptionService->injectSettings([ + 'privacy' => [ + 'rsaKeyFingerprint' => self::RSA_KEY_FINGERPRINT, + ], + ]); + + $encryptionService->injectRsaWalletService(self::getRsaWalletServicePhp()); + $sealed = $encryptionService->seal('foo'); + + self::assertInstanceOf(Sealed::class, $sealed); + + $data = $sealed->toArray(); + + self::assertArrayHasKey('encryptedData', $data); + self::assertArrayHasKey('initializationVector', $data); + self::assertArrayHasKey('envelopeKey', $data); + + self::assertGreaterThan(0, \strlen($data['encryptedData'])); + self::assertGreaterThan(0, \strlen($data['initializationVector'])); + self::assertGreaterThan(0, \strlen($data['envelopeKey'])); + } +} diff --git a/Tests/Unit/Scope/RewriteEvent/RewriteEventTest.php b/Tests/Unit/Scope/RewriteEvent/RewriteEventTest.php new file mode 100644 index 0000000..52813cf --- /dev/null +++ b/Tests/Unit/Scope/RewriteEvent/RewriteEventTest.php @@ -0,0 +1,147 @@ +<?php + +declare(strict_types=1); + +namespace Netlogix\Sentry\Tests\Unit\Scope\RewriteEvent; + +use Neos\Flow\Core\Bootstrap; +use Neos\Flow\ObjectManagement\ObjectManagerInterface; +use Neos\Flow\Tests\UnitTestCase; +use Netlogix\Sentry\Encryption\EncryptionService; +use Netlogix\Sentry\Encryption\Sealed; +use Netlogix\Sentry\EventProcessor\EncryptedPayload; +use Netlogix\Sentry\Integration\NetlogixIntegration; +use PHP_CodeSniffer\Tokenizers\JS; +use Sentry\Event; +use Sentry\EventHint; +use Sentry\EventId; + +class RewriteEventTest extends UnitTestCase +{ + /** + * @test + */ + public function No_configuration_does_not_replace_the_request(): void + { + $objectManager = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMock(); + + $encryptedPayload = new EncryptedPayload(); + $encryptedPayload->injectSettings([]); + + $objectManager + ->expects(self::once()) + ->method('get') + ->with(EncryptedPayload::class) + ->willReturn($encryptedPayload); + + Bootstrap::$staticObjectManager = $objectManager; + + $event = Event::createEvent(EventId::generate()); + $request = [ + 'data' => [ + 'foo' => 'bar', + ], + ]; + $event->setRequest($request); + + $newEvent = NetlogixIntegration::encryptPostBody($event, new EventHint()); + $newRequest = $newEvent->getRequest(); + + self::assertSame($request, $newRequest); + } + + /** + * @test + */ + public function Enabled_encryption_but_no_request_data_does_not_replace_the_request(): void + { + $objectManager = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMock(); + + $encryptedPayload = new EncryptedPayload(); + $encryptedPayload->injectSettings([ + 'privacy' => [ + 'encryptPostBody' => true, + ], + ]); + + $objectManager + ->expects(self::once()) + ->method('get') + ->with(EncryptedPayload::class) + ->willReturn($encryptedPayload); + + Bootstrap::$staticObjectManager = $objectManager; + + $request = [ + 'whatever' => [ + 'foo' => 'bar', + ], + ]; + $event = Event::createEvent(EventId::generate()); + $event->setRequest($request); + + $newEvent = NetlogixIntegration::encryptPostBody($event, new EventHint()); + $newRequest = $newEvent->getRequest(); + + self::assertEquals($request, $newRequest); + } + + /** + * @test + */ + public function Enabled_encryption_calls_the_encryption_service_and_rewrites_the_request_body(): void + { + $objectManager = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMock(); + + $request = [ + 'data' => [ + 'foo' => 'bar', + ], + ]; + + $encryption = $this->createMock(EncryptionService::class); + $encryption + ->expects(self::once()) + ->method('seal') + ->with(\json_encode($request['data'], \JSON_PRETTY_PRINT)) + ->willReturn(new Sealed('$encryptedData', '$initializationVector', '$envelopeKeys')); + + $encryptedPayload = new EncryptedPayload(); + $encryptedPayload->injectSettings([ + 'privacy' => [ + 'encryptPostBody' => true, + ], + ]); + $encryptedPayload->injectEncryptionService($encryption); + + $objectManager + ->expects(self::once()) + ->method('get') + ->with(EncryptedPayload::class) + ->willReturn($encryptedPayload); + + Bootstrap::$staticObjectManager = $objectManager; + + $event = Event::createEvent(EventId::generate()); + $event->setRequest($request); + + $newEvent = NetlogixIntegration::encryptPostBody($event, new EventHint()); + $request = $newEvent->getRequest(); + + self::assertEquals( + [ + 'data' => [ + '__ENCRYPTED__DATA__' => [ + 'encryptedData' => \base64_encode('$encryptedData'), + 'initializationVector' => \base64_encode('$initializationVector'), + 'envelopeKey' => \base64_encode('$envelopeKeys'), + ], + ], + ], + $request + ); + } +} diff --git a/composer.json b/composer.json index 03533be..2d07a1a 100644 --- a/composer.json +++ b/composer.json @@ -4,9 +4,11 @@ "type": "neos-package", "license": "MIT", "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "neos/flow": "^7.3.6 || ^8.0.4", - "sentry/sdk": "^3.1" + "sentry/sdk": "^3.1", + "ext-openssl": "*", + "ext-json": "*" }, "autoload": { "psr-4": {