diff --git a/composer.json b/composer.json index 536a5df93e..d331138fb3 100644 --- a/composer.json +++ b/composer.json @@ -59,6 +59,7 @@ "lexik/jwt-authentication-bundle": "^3.1", "liip/imagine-bundle": "^2.13", "pagerfanta/pagerfanta": "^4.0", + "paragonie/halite": "^5.0", "payum/offline": "^1.7.5", "payum/payum-bundle": "^2.6", "php-http/discovery": "^1.20", diff --git a/src/Sylius/Component/Payment/Encryption/Encrypter.php b/src/Sylius/Component/Payment/Encryption/Encrypter.php new file mode 100644 index 0000000000..1f110f8ae2 --- /dev/null +++ b/src/Sylius/Component/Payment/Encryption/Encrypter.php @@ -0,0 +1,71 @@ +getKey()) . self::ENCRYPTION_SUFFIX; + } catch (\TypeError|\SodiumException|HaliteAlert $exception) { + throw EncryptionException::cannotEncrypt($exception); + } + } + + public function decrypt(string $data): string + { + if (!str_ends_with($data, self::ENCRYPTION_SUFFIX)) { + return $data; + } + + try { + $data = substr($data, 0, -self::ENCRYPTION_SUFFIX_LENGTH); + + return Crypto::decrypt($data, $this->getKey())->getString(); + } catch (\TypeError|\SodiumException|HaliteAlert $exception) { + throw EncryptionException::cannotDecrypt($exception); + } + } + + private function getKey(): EncryptionKey + { + if (null === $this->key) { + $this->key = KeyFactory::deriveEncryptionKey(new HiddenString($this->keyPhrase), $this->keySalt); + } + + return $this->key; + } +} diff --git a/src/Sylius/Component/Payment/Encryption/EncrypterInterface.php b/src/Sylius/Component/Payment/Encryption/EncrypterInterface.php new file mode 100644 index 0000000000..21ad6cfbab --- /dev/null +++ b/src/Sylius/Component/Payment/Encryption/EncrypterInterface.php @@ -0,0 +1,25 @@ +beConstructedWith('a_very_strong_password', 'very_strong_salt'); + } + + function it_is_an_encrypter(): void + { + $this->shouldImplement(EncrypterInterface::class); + } + + function it_throws_an_exception_if_it_cannot_encrypt(): void + { + $this->beConstructedWith('', ''); + $this->shouldThrow(EncryptionException::class)->during('encrypt', ['data']); + } + + function it_throws_an_exception_if_it_cannot_decrypt(): void + { + $this->beConstructedWith('', ''); + $this->shouldThrow(EncryptionException::class)->during('decrypt', ['data#ENCRYPTED']); + } + + function it_encrypts_data(): void + { + $this->encrypt('data')->shouldBeString(); + $this->encrypt('data')->shouldNotBe('data'); + $this->encrypt('data')->shouldEndWith('#ENCRYPTED'); + } + + function it_decrypts_data(): void + { + $data = 'data'; + $encryptedData = $this->getWrappedObject()->encrypt($data); + + $this->decrypt($encryptedData)->shouldNotEndWith('#ENCRYPTED'); + $this->decrypt($encryptedData)->shouldBe($data); + } + + function it_does_nothing_when_data_is_not_marked_as_encrypted(): void + { + $this->decrypt('data')->shouldBe('data'); + } +}