diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index f328b6729e..75b35defb8 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -26,6 +26,7 @@ CHANGELOG * removed support for phpredis 4 `compression` * [BC BREAK] `RedisTagAwareAdapter` is not compatible with `RedisCluster` from `Predis` anymore, use `phpredis` instead * Marked the `CacheDataCollector` class as `@final`. + * added `SodiumMarshaller` to encrypt/decrypt values using libsodium 4.3.0 ----- diff --git a/src/Symfony/Component/Cache/Marshaller/SodiumMarshaller.php b/src/Symfony/Component/Cache/Marshaller/SodiumMarshaller.php new file mode 100644 index 0000000000..dbf486a721 --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/SodiumMarshaller.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * Encrypt/decrypt values using Libsodium. + * + * @author Ahmed TAILOULOUTE + */ +class SodiumMarshaller implements MarshallerInterface +{ + private $marshaller; + private $decryptionKeys; + + /** + * @param string[] $decryptionKeys The key at index "0" is required and is used to decrypt and encrypt values; + * more rotating keys can be provided to decrypt values; + * each key must be generated using sodium_crypto_box_keypair() + */ + public function __construct(array $decryptionKeys, MarshallerInterface $marshaller = null) + { + if (!self::isSupported()) { + throw new CacheException('The "sodium" PHP extension is not loaded.'); + } + + if (!isset($decryptionKeys[0])) { + throw new InvalidArgumentException('At least one decryption key must be provided at index "0".'); + } + + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + $this->decryptionKeys = $decryptionKeys; + } + + public static function isSupported(): bool + { + return \function_exists('sodium_crypto_box_seal'); + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $encryptionKey = sodium_crypto_box_publickey($this->decryptionKeys[0]); + + $encryptedValues = []; + foreach ($this->marshaller->marshall($values, $failed) as $k => $v) { + $encryptedValues[$k] = sodium_crypto_box_seal($v, $encryptionKey); + } + + return $encryptedValues; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + foreach ($this->decryptionKeys as $k) { + if (false !== $decryptedValue = @sodium_crypto_box_seal_open($value, $k)) { + $value = $decryptedValue; + break; + } + } + + return $this->marshaller->unmarshall($value); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/SodiumMarshallerTest.php b/src/Symfony/Component/Cache/Tests/Marshaller/SodiumMarshallerTest.php new file mode 100644 index 0000000000..bd80fb10dc --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/SodiumMarshallerTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Marshaller; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\SodiumMarshaller; + +/** + * @requires extension sodium + */ +class SodiumMarshallerTest extends TestCase +{ + private $decryptionKey; + + protected function setUp(): void + { + $this->decryptionKey = sodium_crypto_box_keypair(); + } + + public function testMarshall() + { + $defaultMarshaller = new DefaultMarshaller(); + $sodiumMarshaller = new SodiumMarshaller([$this->decryptionKey], $defaultMarshaller); + + $values = ['a' => '123']; + $failed = []; + $defaultResult = $defaultMarshaller->marshall($values, $failed); + + $sodiumResult = $sodiumMarshaller->marshall($values, $failed); + $sodiumResult['a'] = sodium_crypto_box_seal_open($sodiumResult['a'], $this->decryptionKey); + + $this->assertSame($defaultResult, $sodiumResult); + } + + public function testUnmarshall() + { + $defaultMarshaller = new DefaultMarshaller(); + $sodiumMarshaller = new SodiumMarshaller([$this->decryptionKey], $defaultMarshaller); + + $values = ['a' => '123']; + $failed = []; + + $sodiumResult = $sodiumMarshaller->marshall($values, $failed); + $defaultResult = $defaultMarshaller->marshall($values, $failed); + + $this->assertSame($values['a'], $sodiumMarshaller->unmarshall($sodiumResult['a'])); + $this->assertSame($values['a'], $sodiumMarshaller->unmarshall($defaultResult['a'])); + + $sodiumMarshaller = new SodiumMarshaller([sodium_crypto_box_keypair(), $this->decryptionKey], $defaultMarshaller); + + $this->assertSame($values['a'], $sodiumMarshaller->unmarshall($sodiumResult['a'])); + $this->assertSame($values['a'], $sodiumMarshaller->unmarshall($defaultResult['a'])); + } +}