From 8ba20b022e2ceacfb8acdb30af947e09b4710ba7 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Fri, 21 Feb 2025 18:45:49 +0100 Subject: [PATCH] Add experimental MimePgp component Introduce the new MimePgp component for encrypting MIME messages using OpenPGP. The component is marked as experimental and includes initial setup files, exceptions, tests, and a changelog. --- composer.json | 1 + .../DependencyInjection/Configuration.php | 48 ++++++ .../FrameworkExtension.php | 30 ++++ .../Resources/config/mailer.php | 25 +++ .../Resources/config/schema/symfony-1.0.xsd | 20 +++ .../DependencyInjection/ConfigurationTest.php | 14 ++ .../Bundle/FrameworkBundle/composer.json | 1 + src/Symfony/Component/Mailer/CHANGELOG.md | 1 + .../PgpMimeEncryptedMessageListener.php | 66 ++++++++ .../PgpMimeSignedMessageListener.php | 47 ++++++ .../PgpPublicKeyRepositoryInterface.php | 25 +++ .../PgpMimeEncryptedMessageListenerTest.php | 106 +++++++++++++ .../PgpMimeSignedMessageListenerTest.php | 50 ++++++ .../Mailer/Tests/Fixtures/pgp_public_key.asc | 51 ++++++ .../Mailer/Tests/Fixtures/pgp_secret_key.asc | 107 +++++++++++++ src/Symfony/Component/Mailer/composer.json | 1 + src/Symfony/Component/MimePgp/.gitattributes | 3 + .../MimePgp/.github/PULL_REQUEST_TEMPLATE.md | 8 + .../.github/workflows/close-pull-request.yml | 20 +++ src/Symfony/Component/MimePgp/.gitignore | 3 + src/Symfony/Component/MimePgp/CHANGELOG.md | 7 + .../MimePgp/Exception/ExceptionInterface.php | 19 +++ .../Exception/KeyNotFoundException.php | 19 +++ .../MimePgp/Exception/RuntimeException.php | 19 +++ src/Symfony/Component/MimePgp/LICENSE | 19 +++ .../Mime/Part/Multipart/PgpEncryptedPart.php | 36 +++++ .../Mime/Part/Multipart/PgpSignedPart.php | 37 +++++ .../Part/PgpEncryptedInitializationPart.php | 48 ++++++ .../Mime/Part/PgpEncryptedMessagePart.php | 53 +++++++ .../MimePgp/Mime/Part/PgpKeyPart.php | 55 +++++++ .../MimePgp/Mime/Part/PgpSignaturePart.php | 56 +++++++ .../Component/MimePgp/PgpEncrypter.php | 60 +++++++ .../Component/MimePgp/PgpMimeTrait.php | 34 ++++ src/Symfony/Component/MimePgp/PgpProcess.php | 143 +++++++++++++++++ src/Symfony/Component/MimePgp/PgpSigner.php | 72 +++++++++ src/Symfony/Component/MimePgp/README.md | 14 ++ .../Part/Multipart/PgpEncryptedPartTest.php | 25 +++ .../Part/Multipart/PgpSignedPartTest.php | 27 ++++ .../PgpEncryptedInitializationPartTest.php | 26 ++++ .../Part/PgpEncryptedMessagePartTest.php | 26 ++++ .../MimePgp/Tests/Part/PgpKeyPartTest.php | 36 +++++ .../Tests/Part/PgpSignaturePartTest.php | 29 ++++ .../MimePgp/Tests/PgpEncrypterTest.php | 127 +++++++++++++++ .../Component/MimePgp/Tests/PgpSignerTest.php | 146 ++++++++++++++++++ .../MimePgp/Tests/PgpTestingProcess.php | 142 +++++++++++++++++ .../MimePgp/Tests/_data/other_public_key.asc | 17 ++ .../Tests/_data/pgp_test_public_key.asc | 51 ++++++ .../Tests/_data/pgp_test_secret_key.asc | 107 +++++++++++++ src/Symfony/Component/MimePgp/composer.json | 33 ++++ .../Component/MimePgp/phpunit.xml.dist | 31 ++++ 50 files changed, 2141 insertions(+) create mode 100644 src/Symfony/Component/Mailer/EventListener/PgpMimeEncryptedMessageListener.php create mode 100644 src/Symfony/Component/Mailer/EventListener/PgpMimeSignedMessageListener.php create mode 100644 src/Symfony/Component/Mailer/EventListener/PgpPublicKeyRepositoryInterface.php create mode 100644 src/Symfony/Component/Mailer/Tests/EventListener/PgpMimeEncryptedMessageListenerTest.php create mode 100644 src/Symfony/Component/Mailer/Tests/EventListener/PgpMimeSignedMessageListenerTest.php create mode 100644 src/Symfony/Component/Mailer/Tests/Fixtures/pgp_public_key.asc create mode 100644 src/Symfony/Component/Mailer/Tests/Fixtures/pgp_secret_key.asc create mode 100644 src/Symfony/Component/MimePgp/.gitattributes create mode 100644 src/Symfony/Component/MimePgp/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 src/Symfony/Component/MimePgp/.github/workflows/close-pull-request.yml create mode 100644 src/Symfony/Component/MimePgp/.gitignore create mode 100644 src/Symfony/Component/MimePgp/CHANGELOG.md create mode 100644 src/Symfony/Component/MimePgp/Exception/ExceptionInterface.php create mode 100644 src/Symfony/Component/MimePgp/Exception/KeyNotFoundException.php create mode 100644 src/Symfony/Component/MimePgp/Exception/RuntimeException.php create mode 100644 src/Symfony/Component/MimePgp/LICENSE create mode 100644 src/Symfony/Component/MimePgp/Mime/Part/Multipart/PgpEncryptedPart.php create mode 100644 src/Symfony/Component/MimePgp/Mime/Part/Multipart/PgpSignedPart.php create mode 100644 src/Symfony/Component/MimePgp/Mime/Part/PgpEncryptedInitializationPart.php create mode 100644 src/Symfony/Component/MimePgp/Mime/Part/PgpEncryptedMessagePart.php create mode 100644 src/Symfony/Component/MimePgp/Mime/Part/PgpKeyPart.php create mode 100644 src/Symfony/Component/MimePgp/Mime/Part/PgpSignaturePart.php create mode 100644 src/Symfony/Component/MimePgp/PgpEncrypter.php create mode 100644 src/Symfony/Component/MimePgp/PgpMimeTrait.php create mode 100644 src/Symfony/Component/MimePgp/PgpProcess.php create mode 100644 src/Symfony/Component/MimePgp/PgpSigner.php create mode 100644 src/Symfony/Component/MimePgp/README.md create mode 100644 src/Symfony/Component/MimePgp/Tests/Part/Multipart/PgpEncryptedPartTest.php create mode 100644 src/Symfony/Component/MimePgp/Tests/Part/Multipart/PgpSignedPartTest.php create mode 100644 src/Symfony/Component/MimePgp/Tests/Part/PgpEncryptedInitializationPartTest.php create mode 100644 src/Symfony/Component/MimePgp/Tests/Part/PgpEncryptedMessagePartTest.php create mode 100644 src/Symfony/Component/MimePgp/Tests/Part/PgpKeyPartTest.php create mode 100644 src/Symfony/Component/MimePgp/Tests/Part/PgpSignaturePartTest.php create mode 100644 src/Symfony/Component/MimePgp/Tests/PgpEncrypterTest.php create mode 100644 src/Symfony/Component/MimePgp/Tests/PgpSignerTest.php create mode 100644 src/Symfony/Component/MimePgp/Tests/PgpTestingProcess.php create mode 100644 src/Symfony/Component/MimePgp/Tests/_data/other_public_key.asc create mode 100644 src/Symfony/Component/MimePgp/Tests/_data/pgp_test_public_key.asc create mode 100644 src/Symfony/Component/MimePgp/Tests/_data/pgp_test_secret_key.asc create mode 100644 src/Symfony/Component/MimePgp/composer.json create mode 100644 src/Symfony/Component/MimePgp/phpunit.xml.dist diff --git a/composer.json b/composer.json index 263117fc8ddc4..78435fe375276 100644 --- a/composer.json +++ b/composer.json @@ -89,6 +89,7 @@ "symfony/mailer": "self.version", "symfony/messenger": "self.version", "symfony/mime": "self.version", + "symfony/mime-pgp": "self.version", "symfony/monolog-bridge": "self.version", "symfony/notifier": "self.version", "symfony/options-resolver": "self.version", diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index ca20f15a49619..c05f2cd5a4db5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -2343,6 +2343,54 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->end() ->end() ->end() + ->arrayNode('pgp_signer') + ->addDefaultsIfNotSet() + ->canBeEnabled() + ->info('PGP/MIME signer configuration') + ->children() + ->scalarNode('secret_key') + ->info('Path to the secret key (ASCII armored format without the `file://` prefix)') + ->defaultValue('') + ->cannotBeEmpty() + ->end() + ->scalarNode('public_key') + ->info('Path to the public key (ASCII armored format without the `file://` prefix)') + ->defaultNull('') + ->end() + ->scalarNode('passphrase') + ->info('The secret key passphrase') + ->defaultNull() + ->end() + ->scalarNode('binary') + ->info('Path to the GnuPG binary') + ->defaultValue('gpg') + ->end() + ->scalarNode('digest_algorithm') + ->info('The digest algorithm') + ->defaultValue('SHA512') + ->end() + ->end() + ->end() + ->arrayNode('pgp_encrypter') + ->addDefaultsIfNotSet() + ->canBeEnabled() + ->info('S/MIME encrypter configuration') + ->children() + ->scalarNode('repository') + ->info('Path to the S/MIME certificate repository. Shall implement the `Symfony\Component\Mailer\EventListener\PgpPublicKeyRepositoryInterface`.') + ->defaultValue('') + ->cannotBeEmpty() + ->end() + ->scalarNode('binary') + ->info('Path to the GnuPG binary') + ->defaultValue('gpg') + ->end() + ->integerNode('cipher_algorithm') + ->info('The cipher algorithm used to encrypt the message') + ->defaultValue('AES256') + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 4c36bf4dc8f24..2e1d7e9e23f1e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -113,6 +113,8 @@ use Symfony\Component\Mailer\Command\MailerTestCommand; use Symfony\Component\Mailer\EventListener\DkimSignedMessageListener; use Symfony\Component\Mailer\EventListener\MessengerTransportListener; +use Symfony\Component\Mailer\EventListener\PgpMimeEncryptedMessageListener; +use Symfony\Component\Mailer\EventListener\PgpMimeSignedMessageListener; use Symfony\Component\Mailer\EventListener\SmimeEncryptedMessageListener; use Symfony\Component\Mailer\EventListener\SmimeSignedMessageListener; use Symfony\Component\Mailer\Mailer; @@ -2901,6 +2903,34 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co $container->removeDefinition('mailer.smime_encrypter.listener'); } + if ($config['pgp_signer']['enabled']) { + if (!class_exists(PgpMimeSignedMessageListener::class)) { + throw new LogicException('PGP/MIME signed messages support cannot be enabled as this version of the Mailer component does not support it.'); + } + $smimeSigner = $container->getDefinition('mailer.pgp_signer'); + $smimeSigner->setArgument(0, $config['pgp_signer']['secret_key']); + $smimeSigner->setArgument(1, $config['pgp_signer']['public_key']); + $smimeSigner->setArgument(2, $config['pgp_signer']['passphrase']); + $smimeSigner->setArgument(3, [ + 'binary' => $config['pgp_signer']['binary'], + 'digest_algorithm' => $config['pgp_signer']['digest_algorithm'], + ]); + } else { + $container->removeDefinition('mailer.pgp_signer'); + $container->removeDefinition('mailer.pgp_signer.listener'); + } + + if ($config['pgp_encrypter']['enabled']) { + if (!class_exists(PgpMimeEncryptedMessageListener::class)) { + throw new LogicException('PGP/MIME encrypted messages support cannot be enabled as this version of the Mailer component does not support it.'); + } + $container->setAlias('mailer.pgp_encrypter.repository', $config['pgp_encrypter']['repository']); + $container->setParameter('mailer.pgp_encrypter.binary', $config['pgp_encrypter']['binary']); + $container->setParameter('mailer.pgp_encrypter.cipher_algorithm', $config['pgp_encrypter']['cipher_algorithm']); + } else { + $container->removeDefinition('mailer.pgp_encrypter.listener'); + } + if ($webhookEnabled) { $loader->load('mailer_webhook.php'); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php index 25b3fefdbfb00..b837dc9bf16ff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php @@ -17,6 +17,8 @@ use Symfony\Component\Mailer\EventListener\MessageListener; use Symfony\Component\Mailer\EventListener\MessageLoggerListener; use Symfony\Component\Mailer\EventListener\MessengerTransportListener; +use Symfony\Component\Mailer\EventListener\PgpMimeEncryptedMessageListener; +use Symfony\Component\Mailer\EventListener\PgpMimeSignedMessageListener; use Symfony\Component\Mailer\EventListener\SmimeEncryptedMessageListener; use Symfony\Component\Mailer\EventListener\SmimeSignedMessageListener; use Symfony\Component\Mailer\Mailer; @@ -28,6 +30,7 @@ use Symfony\Component\Mime\Crypto\DkimSigner; use Symfony\Component\Mime\Crypto\SMimeEncrypter; use Symfony\Component\Mime\Crypto\SMimeSigner; +use Symfony\Component\MimePgp\PgpSigner; return static function (ContainerConfigurator $container) { $container->services() @@ -126,6 +129,28 @@ ]) ->tag('kernel.event_subscriber') + ->set('mailer.pgp_signer', PgpSigner::class) + ->args([ + abstract_arg('secret_key'), + abstract_arg('public_key'), + abstract_arg('passphrase'), + abstract_arg('options'), + ]) + + ->set('mailer.pgp_signer.listener', PgpMimeSignedMessageListener::class) + ->args([ + service('mailer.pgp_signer'), + ]) + ->tag('kernel.event_subscriber') + + ->set('mailer.pgp_encrypter.listener', PgpMimeEncryptedMessageListener::class) + ->args([ + service('mailer.pgp_encrypter.repository'), + param('mailer.pgp_encrypter.binary'), + param('mailer.pgp_encrypter.cipher_algorithm'), + ]) + ->tag('kernel.event_subscriber') + ->set('console.command.mailer_test', MailerTestCommand::class) ->args([ service('mailer.transports'), diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index b47de331a4775..57fa99a621ce7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -794,6 +794,8 @@ + + @@ -840,6 +842,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 607049274c7da..6135061240c7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -944,6 +944,20 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'certificate' => '', 'cipher' => null, ], + 'pgp_signer' => [ + 'enabled' => false, + 'secret_key' => '', + 'public_key' => null, + 'passphrase' => null, + 'binary' => 'gpg', + 'digest_algorithm' => 'SHA512', + ], + 'pgp_encrypter' => [ + 'enabled' => false, + 'repository' => '', + 'binary' => 'gpg', + 'cipher_algorithm' => 'AES256', + ], ], 'notifier' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(Notifier::class), diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 03707eea39b5f..1547b9af850e2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -53,6 +53,7 @@ "symfony/mailer": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", "symfony/mime": "^6.4|^7.0", + "symfony/mime-pgp": "^7.3", "symfony/notifier": "^6.4|^7.0", "symfony/process": "^6.4|^7.0", "symfony/rate-limiter": "^6.4|^7.0", diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 3816cc474948b..6addee44016fb 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add DSN param `source_ip` to allow binding to a (specific) IPv4 or IPv6 address. * Add DSN param `require_tls` to enforce use of TLS/STARTTLS * Add `DkimSignedMessageListener`, `SmimeEncryptedMessageListener`, and `SmimeSignedMessageListener` + * Add `PgpMimeSignedMessageListener` 7.2 --- diff --git a/src/Symfony/Component/Mailer/EventListener/PgpMimeEncryptedMessageListener.php b/src/Symfony/Component/Mailer/EventListener/PgpMimeEncryptedMessageListener.php new file mode 100644 index 0000000000000..748780bb1e0aa --- /dev/null +++ b/src/Symfony/Component/Mailer/EventListener/PgpMimeEncryptedMessageListener.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mime\Message; +use Symfony\Component\MimePgp\PgpEncrypter; + +/** + * Encrypts messages using S/MIME. + * + * @author Florent Morselli + */ +final class PgpMimeEncryptedMessageListener implements EventSubscriberInterface +{ + public function __construct( + private readonly PgpPublicKeyRepositoryInterface $pgpRepository, + private readonly string $binary = 'gpg', + private readonly string $cipherAlgorithm = 'AES256', + ) { + } + + public function onMessage(MessageEvent $event): void + { + $message = $event->getMessage(); + if (!$message instanceof Message) { + return; + } + if (!$message->getHeaders()->has('X-Pgp-Encrypt')) { + return; + } + $message->getHeaders()->remove('X-Pgp-Encrypt'); + $publicKeys = []; + foreach ($event->getEnvelope()->getRecipients() as $recipient) { + $certificatePath = $this->pgpRepository->findPublicKeyPathFor($recipient->getAddress()); + if (null === $certificatePath) { + return; + } + $publicKeys[$recipient->getAddress()] = $certificatePath; + } + if (0 === \count($publicKeys)) { + return; + } + + $encrypter = new PgpEncrypter($publicKeys, ['binary' => $this->binary, 'cipher_algorithm' => $this->cipherAlgorithm]); + + $event->setMessage($encrypter->encrypt($message)); + } + + public static function getSubscribedEvents(): array + { + return [ + MessageEvent::class => ['onMessage', -128], + ]; + } +} diff --git a/src/Symfony/Component/Mailer/EventListener/PgpMimeSignedMessageListener.php b/src/Symfony/Component/Mailer/EventListener/PgpMimeSignedMessageListener.php new file mode 100644 index 0000000000000..f694ab3cb5013 --- /dev/null +++ b/src/Symfony/Component/Mailer/EventListener/PgpMimeSignedMessageListener.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mime\Message; +use Symfony\Component\MimePgp\PgpSigner; + +/** + * Signs messages using PGP/MIME. + * + * @author Florent Morselli + */ +class PgpMimeSignedMessageListener implements EventSubscriberInterface +{ + public function __construct( + private readonly PgpSigner $signer, + ) { + } + + public function onMessage(MessageEvent $event): void + { + $message = $event->getMessage(); + if (!$message instanceof Message) { + return; + } + + $event->setMessage($this->signer->sign($message)); + } + + public static function getSubscribedEvents(): array + { + return [ + MessageEvent::class => ['onMessage', -128], + ]; + } +} diff --git a/src/Symfony/Component/Mailer/EventListener/PgpPublicKeyRepositoryInterface.php b/src/Symfony/Component/Mailer/EventListener/PgpPublicKeyRepositoryInterface.php new file mode 100644 index 0000000000000..5d4d80b33012b --- /dev/null +++ b/src/Symfony/Component/Mailer/EventListener/PgpPublicKeyRepositoryInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +/** + * Encrypts messages using S/MIME. + * + * @author Florent Morselli + */ +interface PgpPublicKeyRepositoryInterface +{ + /** + * @return ?string The path to the PGP public key. null if not found + */ + public function findPublicKeyPathFor(string $email): ?string; +} diff --git a/src/Symfony/Component/Mailer/Tests/EventListener/PgpMimeEncryptedMessageListenerTest.php b/src/Symfony/Component/Mailer/Tests/EventListener/PgpMimeEncryptedMessageListenerTest.php new file mode 100644 index 0000000000000..e18e5904c74ac --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/EventListener/PgpMimeEncryptedMessageListenerTest.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\EventListener\PgpMimeEncryptedMessageListener; +use Symfony\Component\Mailer\EventListener\PgpPublicKeyRepositoryInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Header\MailboxListHeader; +use Symfony\Component\Mime\Header\UnstructuredHeader; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\Part\TextPart; +use Symfony\Component\MimePgp\Mime\Part\Multipart\PgpEncryptedPart; + +class PgpMimeEncryptedMessageListenerTest extends TestCase +{ + /** + * @requires extension openssl + */ + public function testPgpMessageEncryptionProcess() + { + $repository = $this->createMock(PgpPublicKeyRepositoryInterface::class); + $repository->method('findPublicKeyPathFor')->willReturn(\dirname(__DIR__).'/Fixtures/pgp_public_key.asc'); + $listener = new PgpMimeEncryptedMessageListener($repository); + $message = new Message( + new Headers( + new MailboxListHeader('From', [new Address('sender@example.com')]), + new UnstructuredHeader('X-Pgp-Encrypt', 'true'), + ), + new TextPart('hello') + ); + $envelope = new Envelope(new Address('sender@example.com'), [new Address('pgp@pulli.dev')]); + $event = new MessageEvent($message, $envelope, 'default'); + + $listener->onMessage($event); + $this->assertNotSame($message, $event->getMessage()); + $this->assertInstanceOf(TextPart::class, $message->getBody()); + $this->assertInstanceOf(PgpEncryptedPart::class, $event->getMessage()->getBody()); + $this->assertFalse($event->getMessage()->getHeaders()->has('X-Pgp-Encrypt')); + } + + /** + * @requires extension openssl + */ + public function testMessageNotEncryptedWhenOneRecipientCertificateIsMissing() + { + $repository = $this->createMock(PgpPublicKeyRepositoryInterface::class); + $repository->method('findPublicKeyPathFor')->willReturnOnConsecutiveCalls(\dirname(__DIR__).'/Fixtures/pgp_public_key.asc', null); + $listener = new PgpMimeEncryptedMessageListener($repository); + $message = new Message( + new Headers( + new MailboxListHeader('From', [new Address('sender@example.com')]), + new UnstructuredHeader('X-Pgp-Encrypt', 'true'), + ), + new TextPart('hello') + ); + $envelope = new Envelope(new Address('sender@example.com'), [ + new Address('pgp@pulli.dev'), + new Address('r2@example.com'), + ]); + $event = new MessageEvent($message, $envelope, 'default'); + + $listener->onMessage($event); + $this->assertSame($message, $event->getMessage()); + $this->assertInstanceOf(TextPart::class, $message->getBody()); + $this->assertInstanceOf(TextPart::class, $event->getMessage()->getBody()); + } + + /** + * @requires extension openssl + */ + public function testMessageNotExplicitlyAskedForNonEncryption() + { + $repository = $this->createMock(PgpPublicKeyRepositoryInterface::class); + $repository->method('findPublicKeyPathFor')->willReturn(\dirname(__DIR__).'/Fixtures/pgp_public_key.asc'); + $listener = new PgpMimeEncryptedMessageListener($repository); + $message = new Message( + new Headers( + new MailboxListHeader('From', [new Address('sender@example.com')]), + ), + new TextPart('hello') + ); + $envelope = new Envelope(new Address('sender@example.com'), [ + new Address('pgp@pulli.dev'), + new Address('r2@example.com'), + ]); + $event = new MessageEvent($message, $envelope, 'default'); + + $listener->onMessage($event); + $this->assertSame($message, $event->getMessage()); + $this->assertInstanceOf(TextPart::class, $message->getBody()); + $this->assertInstanceOf(TextPart::class, $event->getMessage()->getBody()); + } +} diff --git a/src/Symfony/Component/Mailer/Tests/EventListener/PgpMimeSignedMessageListenerTest.php b/src/Symfony/Component/Mailer/Tests/EventListener/PgpMimeSignedMessageListenerTest.php new file mode 100644 index 0000000000000..f7db3ff38acd7 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/EventListener/PgpMimeSignedMessageListenerTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\EventListener\PgpMimeSignedMessageListener; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Header\MailboxListHeader; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\Part\TextPart; +use Symfony\Component\MimePgp\Mime\Part\Multipart\PgpSignedPart; +use Symfony\Component\MimePgp\PgpSigner; + +class PgpMimeSignedMessageListenerTest extends TestCase +{ + public function testPgpMimeMessageSigningProcess() + { + $signer = new PgpSigner( + \dirname(__DIR__).'/Fixtures/pgp_secret_key.asc', + \dirname(__DIR__).'/Fixtures/pgp_public_key.asc', + 'test1234' + ); + $listener = new PgpMimeSignedMessageListener($signer); + $message = new Message( + new Headers( + new MailboxListHeader('From', [new Address('sender@example.com')]) + ), + new TextPart('hello') + ); + $envelope = new Envelope(new Address('sender@example.com'), [new Address('r1@example.com')]); + $event = new MessageEvent($message, $envelope, 'default'); + + $listener->onMessage($event); + $this->assertNotSame($message, $event->getMessage()); + $this->assertInstanceOf(TextPart::class, $message->getBody()); + $this->assertInstanceOf(PgpSignedPart::class, $event->getMessage()->getBody()); + } +} diff --git a/src/Symfony/Component/Mailer/Tests/Fixtures/pgp_public_key.asc b/src/Symfony/Component/Mailer/Tests/Fixtures/pgp_public_key.asc new file mode 100644 index 0000000000000..24e5a4e389ff8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Fixtures/pgp_public_key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=w8wH +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/Symfony/Component/Mailer/Tests/Fixtures/pgp_secret_key.asc b/src/Symfony/Component/Mailer/Tests/Fixtures/pgp_secret_key.asc new file mode 100644 index 0000000000000..a5fc574574fa8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Fixtures/pgp_secret_key.asc @@ -0,0 +1,107 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +cvLFBoHiJ0sMhrEfncqtBJnOCw== +=cAza +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/Symfony/Component/Mailer/composer.json b/src/Symfony/Component/Mailer/composer.json index 4336e725133fc..5e59627cced0b 100644 --- a/src/Symfony/Component/Mailer/composer.json +++ b/src/Symfony/Component/Mailer/composer.json @@ -28,6 +28,7 @@ "symfony/console": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", + "symfony/mime-pgp": "^7.3", "symfony/twig-bridge": "^6.4|^7.0" }, "conflict": { diff --git a/src/Symfony/Component/MimePgp/.gitattributes b/src/Symfony/Component/MimePgp/.gitattributes new file mode 100644 index 0000000000000..14c3c35940427 --- /dev/null +++ b/src/Symfony/Component/MimePgp/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.git* export-ignore diff --git a/src/Symfony/Component/MimePgp/.github/PULL_REQUEST_TEMPLATE.md b/src/Symfony/Component/MimePgp/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000..4689c4dad430e --- /dev/null +++ b/src/Symfony/Component/MimePgp/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/src/Symfony/Component/MimePgp/.github/workflows/close-pull-request.yml b/src/Symfony/Component/MimePgp/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000000000..e55b47817e69a --- /dev/null +++ b/src/Symfony/Component/MimePgp/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! diff --git a/src/Symfony/Component/MimePgp/.gitignore b/src/Symfony/Component/MimePgp/.gitignore new file mode 100644 index 0000000000000..5414c2c655e72 --- /dev/null +++ b/src/Symfony/Component/MimePgp/.gitignore @@ -0,0 +1,3 @@ +composer.lock +phpunit.xml +vendor/ diff --git a/src/Symfony/Component/MimePgp/CHANGELOG.md b/src/Symfony/Component/MimePgp/CHANGELOG.md new file mode 100644 index 0000000000000..5294c5b5f3637 --- /dev/null +++ b/src/Symfony/Component/MimePgp/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.3 +--- + + * Introduce the component as experimental diff --git a/src/Symfony/Component/MimePgp/Exception/ExceptionInterface.php b/src/Symfony/Component/MimePgp/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..ef4d695a1886a --- /dev/null +++ b/src/Symfony/Component/MimePgp/Exception/ExceptionInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Exception; + +/** + * @author Fabien Potencier + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/src/Symfony/Component/MimePgp/Exception/KeyNotFoundException.php b/src/Symfony/Component/MimePgp/Exception/KeyNotFoundException.php new file mode 100644 index 0000000000000..5e86a230f6324 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Exception/KeyNotFoundException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Exception; + +/** + * @author PuLLi + */ +class KeyNotFoundException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/MimePgp/Exception/RuntimeException.php b/src/Symfony/Component/MimePgp/Exception/RuntimeException.php new file mode 100644 index 0000000000000..57ab54702325a --- /dev/null +++ b/src/Symfony/Component/MimePgp/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Exception; + +/** + * @author Fabien Potencier + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/MimePgp/LICENSE b/src/Symfony/Component/MimePgp/LICENSE new file mode 100644 index 0000000000000..bc38d714ef697 --- /dev/null +++ b/src/Symfony/Component/MimePgp/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2025-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/MimePgp/Mime/Part/Multipart/PgpEncryptedPart.php b/src/Symfony/Component/MimePgp/Mime/Part/Multipart/PgpEncryptedPart.php new file mode 100644 index 0000000000000..80cf9343750f3 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Mime/Part/Multipart/PgpEncryptedPart.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; +use Symfony\Component\Mime\Part\AbstractPart; + +/* + * @author PuLLi + * + * @internal + */ +final class PgpEncryptedPart extends AbstractMultipartPart +{ + public function __construct(AbstractPart ...$parts) + { + parent::__construct(...$parts); + $this->getHeaders()->addParameterizedHeader('Content-Type', 'multipart/encrypted', [ + 'protocol' => 'application/pgp-encrypted', + ]); + } + + public function getMediaSubtype(): string + { + return 'encrypted'; + } +} diff --git a/src/Symfony/Component/MimePgp/Mime/Part/Multipart/PgpSignedPart.php b/src/Symfony/Component/MimePgp/Mime/Part/Multipart/PgpSignedPart.php new file mode 100644 index 0000000000000..192ccfc53c1b7 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Mime/Part/Multipart/PgpSignedPart.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; +use Symfony\Component\Mime\Part\AbstractPart; + +/* + * @author PuLLi + * + * @internal + */ +final class PgpSignedPart extends AbstractMultipartPart +{ + public function __construct(string $digestAlgorithm, AbstractPart ...$parts) + { + parent::__construct(...$parts); + $this->getHeaders()->addParameterizedHeader('Content-Type', 'multipart/signed', [ + 'micalg' => 'pgp-'.strtolower($digestAlgorithm), + 'protocol' => 'application/pgp-signature', + ]); + } + + public function getMediaSubtype(): string + { + return 'signed'; + } +} diff --git a/src/Symfony/Component/MimePgp/Mime/Part/PgpEncryptedInitializationPart.php b/src/Symfony/Component/MimePgp/Mime/Part/PgpEncryptedInitializationPart.php new file mode 100644 index 0000000000000..6bf8efa0ee1e7 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Mime/Part/PgpEncryptedInitializationPart.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Mime\Part; + +use Symfony\Component\Mime\Part\AbstractPart; + +/* + * @author PuLLi + * + * @internal + */ +final class PgpEncryptedInitializationPart extends AbstractPart +{ + public function __construct() + { + parent::__construct(); + $this->getHeaders()->addTextHeader('Content-Disposition', 'attachment'); + } + + public function bodyToString(): string + { + return "Version: 1\r\n"; + } + + public function bodyToIterable(): iterable + { + yield $this->bodyToString(); + } + + public function getMediaType(): string + { + return 'application'; + } + + public function getMediaSubtype(): string + { + return 'pgp-encrypted'; + } +} diff --git a/src/Symfony/Component/MimePgp/Mime/Part/PgpEncryptedMessagePart.php b/src/Symfony/Component/MimePgp/Mime/Part/PgpEncryptedMessagePart.php new file mode 100644 index 0000000000000..6d1e8f14ec250 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Mime/Part/PgpEncryptedMessagePart.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Mime\Part; + +use Symfony\Component\Mime\Part\AbstractPart; + +/* + * @author PuLLi + * + * @internal + */ +final class PgpEncryptedMessagePart extends AbstractPart +{ + private string $body; + + public function __construct(string $body) + { + parent::__construct(); + $this->body = $body; + $this->getHeaders()->addParameterizedHeader('Content-Disposition', 'inline', [ + 'filename' => 'msg.asc', + ]); + } + + public function bodyToString(): string + { + return $this->body; + } + + public function bodyToIterable(): iterable + { + yield $this->body; + } + + public function getMediaType(): string + { + return 'application'; + } + + public function getMediaSubtype(): string + { + return 'octet-stream'; + } +} diff --git a/src/Symfony/Component/MimePgp/Mime/Part/PgpKeyPart.php b/src/Symfony/Component/MimePgp/Mime/Part/PgpKeyPart.php new file mode 100644 index 0000000000000..cd580c003097e --- /dev/null +++ b/src/Symfony/Component/MimePgp/Mime/Part/PgpKeyPart.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Mime\Part; + +use Symfony\Component\Mime\Part\AbstractPart; + +/* + * @author PuLLi + * + * @internal + */ +final class PgpKeyPart extends AbstractPart +{ + private string $key; + + public function __construct(string $key, string $keyName = 'public-key.asc') + { + parent::__construct(); + $this->key = $key; + $headers = $this->getHeaders(); + $headers->addParameterizedHeader('Content-Disposition', 'attachment', [ + 'filename' => $keyName, + ]); + $headers->addTextHeader('MIME-Version', '1.0'); + } + + public function bodyToString(): string + { + return $this->key; + } + + public function bodyToIterable(): iterable + { + yield $this->key; + } + + public function getMediaType(): string + { + return 'application'; + } + + public function getMediaSubtype(): string + { + return 'pgp-keys'; + } +} diff --git a/src/Symfony/Component/MimePgp/Mime/Part/PgpSignaturePart.php b/src/Symfony/Component/MimePgp/Mime/Part/PgpSignaturePart.php new file mode 100644 index 0000000000000..35fa6479ddbb5 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Mime/Part/PgpSignaturePart.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Mime\Part; + +use Symfony\Component\Mime\Part\AbstractPart; + +/* + * @author PuLLi + * + * @internal + */ +final class PgpSignaturePart extends AbstractPart +{ + public function __construct(private readonly string $signature) + { + parent::__construct(); + $headers = $this->getHeaders(); + $headers->addParameterizedHeader('Content-Type', 'application/pgp-signature', [ + 'name' => 'OpenPGP_signature.asc', + ]); + $headers->addParameterizedHeader('Content-Disposition', 'attachment', [ + 'filename' => 'OpenPGP_signature', + ]); + $headers->addTextHeader('Content-Description', 'OpenPGP digital signature'); + $headers->addTextHeader('MIME-Version', '1.0'); + } + + public function bodyToString(): string + { + return $this->signature; + } + + public function bodyToIterable(): iterable + { + yield $this->signature; + } + + public function getMediaType(): string + { + return 'application'; + } + + public function getMediaSubtype(): string + { + return 'pgp-signature'; + } +} diff --git a/src/Symfony/Component/MimePgp/PgpEncrypter.php b/src/Symfony/Component/MimePgp/PgpEncrypter.php new file mode 100644 index 0000000000000..9a11526a77833 --- /dev/null +++ b/src/Symfony/Component/MimePgp/PgpEncrypter.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp; + +use Symfony\Component\Mime\Message; +use Symfony\Component\MimePgp\Exception\KeyNotFoundException; +use Symfony\Component\MimePgp\Mime\Part\Multipart\PgpEncryptedPart; +use Symfony\Component\MimePgp\Mime\Part\PgpEncryptedInitializationPart; +use Symfony\Component\MimePgp\Mime\Part\PgpEncryptedMessagePart; + +/* + * @author PuLLi + */ +final class PgpEncrypter +{ + use PgpMimeTrait; + + private PgpProcess $pgpProcess; + + /** + * @param array $recipientKeys + * @param array{binary?: string, cipher_algorithm?: string, digest_algorithm?: string, temp_prefix?: string} $options + */ + public function __construct(private readonly array $recipientKeys, array $options = []) + { + $this->pgpProcess = new PgpProcess( + $options['binary'] ?? 'gpg', + $options['temp_prefix'] ?? 'GPGMIME', + $options['cipher_algorithm'] ?? 'AES256', + ); + } + + public function encrypt(Message $message): Message + { + if (0 === \count($this->recipientKeys)) { + throw new KeyNotFoundException('No recipient keys found.'); + } + + $body = $message->getBody(); + $data = $this->iteratorToString($body->toIterable()); + + $output = $this->pgpProcess->encrypt($data, $this->recipientKeys); + + $part = new PgpEncryptedPart( + new PgpEncryptedInitializationPart(), + new PgpEncryptedMessagePart($output) + ); + + return new Message($message->getHeaders(), $part); + } +} diff --git a/src/Symfony/Component/MimePgp/PgpMimeTrait.php b/src/Symfony/Component/MimePgp/PgpMimeTrait.php new file mode 100644 index 0000000000000..5adde8d357f64 --- /dev/null +++ b/src/Symfony/Component/MimePgp/PgpMimeTrait.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp; + +use Symfony\Component\Mime\Exception\RuntimeException; + +/* + * @author PuLLi + */ +trait PgpMimeTrait +{ + protected function normalizeFilePath(string $path): string + { + if (!file_exists($path)) { + throw new RuntimeException(\sprintf('File does not exist: "%s".', $path)); + } + + return str_replace('\\', '/', realpath($path)); + } + + protected function iteratorToString(iterable $iterator): string + { + return implode('', iterator_to_array($iterator, false)); + } +} diff --git a/src/Symfony/Component/MimePgp/PgpProcess.php b/src/Symfony/Component/MimePgp/PgpProcess.php new file mode 100644 index 0000000000000..8a1ba47a3447d --- /dev/null +++ b/src/Symfony/Component/MimePgp/PgpProcess.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp; + +use Symfony\Component\MimePgp\Exception\RuntimeException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; + +/* + * @author PuLLi + * + * @internal + */ +final readonly class PgpProcess +{ + public function __construct( + private string $binaryPath = 'gpg', + private string $tempPrefix = 'GPGMIME', + private string $cipherAlgorithm = 'AES256', + private string $digestAlgorithm = 'SHA512', + ) + { + } + + public function sign(string $data, string $pgpKey,#[\SensitiveParameter] ?string $passphrase = null): string + { + $temporaryKeyring = tempnam(sys_get_temp_dir(), $this->tempPrefix); + $this->storeKeys([$pgpKey => $passphrase], $temporaryKeyring); + + $command = [ + '--no-default-keyring', + '--primary-keyring', + $temporaryKeyring, + '--auto-key-locate', + 'local', + '--armor', + '--digest-algo', + $this->digestAlgorithm, + '--detach-sign', + '--textmode', + ]; + if ($passphrase) { + $command[] = '--pinentry-mode'; + $command[] = 'loopback'; + $command[] = '--passphrase'; + $command[] = $passphrase; + } + + try { + $output = $this->execute($command, $data); + } finally { + unlink($temporaryKeyring); + } + + return $output; + } + + /** + * @param array $pgpKeys The public keys to encrypt the data with. The array keys are the user email addresses and the values are the file paths to the public keys. + */ + public function encrypt(string $data, array $pgpKeys): string + { + $temporaryKeyring = tempnam(sys_get_temp_dir(), $this->tempPrefix); + $this->storeKeys(array_fill_keys($pgpKeys, null), $temporaryKeyring); + + $command = [ + '--no-default-keyring', + '--primary-keyring', + $temporaryKeyring, + '--auto-key-locate', + 'local', + '--encrypt', + '--cipher-algo', + $this->cipherAlgorithm, + '--always-trust', + '--armor', + ]; + foreach (array_keys($pgpKeys) as $recipient) { + $command[] = '--recipient'; + $command[] = $recipient; + } + + try { + $output = $this->execute($command, $data); + } finally { + unlink($temporaryKeyring); + } + + return $output; + } + + /** + * @param string[] $command + */ + private function execute(array $command, ?string $input = null): string + { + array_unshift($command, $this->binaryPath); + $process = new Process($command); + if ($input) { + $process->setInput($input); + } + + try { + $process->mustRun(); + + return $process->getOutput(); + } catch (ProcessFailedException $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * @param array $filePaths + */ + private function storeKeys(#[\SensitiveParameter] array $filePaths, string $keyringPath): void + { + foreach ($filePaths as $filePath => $passphrase) { + $command = [ + '--no-default-keyring', + '--primary-keyring', + $keyringPath, + ]; + if ($passphrase !== null) { + $command[] = '--pinentry-mode'; + $command[] = 'loopback'; + $command[] = '--passphrase'; + $command[] = $passphrase; + } + $command[] = '--import'; + $command[] = $filePath; + $this->execute($command); + } + } +} diff --git a/src/Symfony/Component/MimePgp/PgpSigner.php b/src/Symfony/Component/MimePgp/PgpSigner.php new file mode 100644 index 0000000000000..5407cc86992c7 --- /dev/null +++ b/src/Symfony/Component/MimePgp/PgpSigner.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp; + +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\Part\AbstractPart; +use Symfony\Component\Mime\Part\Multipart\MixedPart; +use Symfony\Component\MimePgp\Mime\Part\Multipart\PgpSignedPart; +use Symfony\Component\MimePgp\Mime\Part\PgpKeyPart; +use Symfony\Component\MimePgp\Mime\Part\PgpSignaturePart; + +/* + * @author PuLLi + */ +final class PgpSigner +{ + use PgpMimeTrait; + + private PgpProcess $pgpProcess; + + private string $digestAlgorithm; + + /** + * @param array{binary?: string, cipher_algorithm?: string, digest_algorithm?: string, temp_prefix?: string} $options + */ + public function __construct(private readonly string $signingKey, private readonly ?string $publicKey = null, #[\SensitiveParameter] private readonly ?string $secretKeyPassphrase = null, array $options = []) + { + $this->digestAlgorithm = $options['digest_algorithm'] ?? 'SHA512'; + $this->pgpProcess = new PgpProcess( + $options['binary'] ?? 'gpg', + $options['temp_prefix'] ?? 'GPGMIME', + $options['cipher_algorithm'] ?? 'AES256', + $this->digestAlgorithm, + ); + } + + public function sign(Message $message): Message + { + $body = $message->getBody(); + $body = $this->attachPublicKey($body); + $data = $this->iteratorToString($body->toIterable()); + $output = $this->pgpProcess->sign($data, $this->signingKey, $this->secretKeyPassphrase); + + + $part = new PgpSignedPart( + $this->digestAlgorithm, + $body, + new PgpSignaturePart($output) + ); + + return new Message($message->getHeaders(), $part); + } + + private function attachPublicKey(AbstractPart $part): AbstractPart + { + if ($this->publicKey === null) { + return $part; + } + + $key = new PgpKeyPart(file_get_contents($this->publicKey)); + return new MixedPart($part, $key); + } +} diff --git a/src/Symfony/Component/MimePgp/README.md b/src/Symfony/Component/MimePgp/README.md new file mode 100644 index 0000000000000..087da0cb4cfcb --- /dev/null +++ b/src/Symfony/Component/MimePgp/README.md @@ -0,0 +1,14 @@ +MIME Component +============== + +The MIME PGP component allows sending PGP/MIME encrypted and signed emails. +This component is currently experimental. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/pgp.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/MimePgp/Tests/Part/Multipart/PgpEncryptedPartTest.php b/src/Symfony/Component/MimePgp/Tests/Part/Multipart/PgpEncryptedPartTest.php new file mode 100644 index 0000000000000..8b8611a0b4ec8 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/Part/Multipart/PgpEncryptedPartTest.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests\Part\Multipart; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\MimePgp\Mime\Part\Multipart\PgpEncryptedPart; + +final class PgpEncryptedPartTest extends TestCase +{ + public function testPGPEncryptedPart() + { + $part = (new PgpEncryptedPart())->toString(); + $this->assertStringContainsString('Content-Type: multipart/encrypted', $part, 'Content-Type not found.'); + $this->assertStringContainsString('protocol="application/pgp-encrypted"', $part, 'Protocol not found'); + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/Part/Multipart/PgpSignedPartTest.php b/src/Symfony/Component/MimePgp/Tests/Part/Multipart/PgpSignedPartTest.php new file mode 100644 index 0000000000000..f4cd29fefff21 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/Part/Multipart/PgpSignedPartTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests\Part\Multipart; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\Part\TextPart; +use Symfony\Component\MimePgp\Mime\Part\Multipart\PgpSignedPart; + +final class PgpSignedPartTest extends TestCase +{ + public function testPGPSignedPart() + { + $part = (new PgpSignedPart('SHA512', new TextPart('Test')))->toString(); + $this->assertStringContainsString('Content-Type: multipart/signed', $part, 'Content-Type not found'); + $this->assertStringContainsString('micalg=pgp-sha512', $part, 'micalg not found'); + $this->assertStringContainsString('protocol="application/pgp-signature"', $part, 'protocol not found'); + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/Part/PgpEncryptedInitializationPartTest.php b/src/Symfony/Component/MimePgp/Tests/Part/PgpEncryptedInitializationPartTest.php new file mode 100644 index 0000000000000..0058a62c751fc --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/Part/PgpEncryptedInitializationPartTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests\Part; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\MimePgp\Mime\Part\PgpEncryptedInitializationPart; + +class PgpEncryptedInitializationPartTest extends TestCase +{ + public function testPGPEncryptedInitializationPart() + { + $part = (new PgpEncryptedInitializationPart())->toString(); + $this->assertStringContainsString('Content-Type: application/pgp-encrypted', $part, 'Content-Type not found'); + $this->assertStringContainsString('Content-Disposition: attachment', $part, 'Content-Disposition not found'); + $this->assertStringContainsString("\r\n\r\nVersion: 1\r\n", $part, 'Version not found'); + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/Part/PgpEncryptedMessagePartTest.php b/src/Symfony/Component/MimePgp/Tests/Part/PgpEncryptedMessagePartTest.php new file mode 100644 index 0000000000000..9e6d833ea9646 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/Part/PgpEncryptedMessagePartTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests\Part; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\MimePgp\Mime\Part\PgpEncryptedMessagePart; + +class PgpEncryptedMessagePartTest extends TestCase +{ + public function testPGPEncryptedMessagePart() + { + $part = (new PgpEncryptedMessagePart(''))->toString(); + $this->assertStringContainsString('Content-Type: application/octet-stream', $part, 'Content-Type not found'); + $this->assertStringContainsString('Content-Disposition: inline', $part, 'Content-Disposition not found'); + $this->assertStringContainsString('filename=msg.asc', $part, 'filename not found'); + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/Part/PgpKeyPartTest.php b/src/Symfony/Component/MimePgp/Tests/Part/PgpKeyPartTest.php new file mode 100644 index 0000000000000..d480f1abcf04e --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/Part/PgpKeyPartTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests\Part; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\MimePgp\Mime\Part\PgpKeyPart; + +class PgpKeyPartTest extends TestCase +{ + public function testPGPKeyPartWithStandardKeyName() + { + $part = (new PgpKeyPart(''))->toString(); + $this->assertStringContainsString('Content-Type: application/pgp-key', $part, 'Content-Type not found'); + $this->assertStringContainsString('Content-Disposition: attachment', $part, 'Content-Disposition not found'); + $this->assertStringContainsString('filename=public-key.asc', $part, 'filename not found'); + $this->assertStringContainsString('MIME-Version: 1.0', $part, 'MIME-Version not found'); + } + + public function testPGPKeyPartWithCustomKeyName() + { + $part = (new PgpKeyPart('', 'custom.asc'))->toString(); + $this->assertStringContainsString('Content-Type: application/pgp-key', $part, 'Content-Type not found'); + $this->assertStringContainsString('Content-Disposition: attachment', $part, 'Content-Disposition not found'); + $this->assertStringContainsString('filename=custom.asc', $part, 'filename not found'); + $this->assertStringContainsString('MIME-Version: 1.0', $part, 'MIME-Version not found'); + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/Part/PgpSignaturePartTest.php b/src/Symfony/Component/MimePgp/Tests/Part/PgpSignaturePartTest.php new file mode 100644 index 0000000000000..8ed75aeca8d2a --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/Part/PgpSignaturePartTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests\Part; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\MimePgp\Mime\Part\PgpSignaturePart; + +class PgpSignaturePartTest extends TestCase +{ + public function testPGPSignaturePart() + { + $part = (new PgpSignaturePart(''))->toString(); + $this->assertStringContainsString('Content-Type: application/pgp-signature', $part, 'Content-Type not found'); + $this->assertStringContainsString('name=OpenPGP_signature.asc', $part, 'name not found'); + $this->assertStringContainsString('Content-Disposition: attachment', $part, 'Content-Disposition not found'); + $this->assertStringContainsString('filename=OpenPGP_signature', $part, 'filename not found'); + $this->assertStringContainsString('Content-Description: OpenPGP digital signature', $part, 'Content-Description not found'); + $this->assertStringContainsString('MIME-Version: 1.0', $part, 'MIME-Version not found'); + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/PgpEncrypterTest.php b/src/Symfony/Component/MimePgp/Tests/PgpEncrypterTest.php new file mode 100644 index 0000000000000..7d59c88a2fef1 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/PgpEncrypterTest.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Message; +use Symfony\Component\MimePgp\Mime\Part\Multipart\PgpEncryptedPart; +use Symfony\Component\MimePgp\Mime\Part\PgpEncryptedInitializationPart; +use Symfony\Component\MimePgp\Mime\Part\PgpEncryptedMessagePart; +use Symfony\Component\MimePgp\PgpEncrypter; +use Symfony\Component\MimePgp\PgpProcess; + +class PgpEncrypterTest extends TestCase +{ + private const KEY_EMAIL_ADDRESS = 'pgp@pulli.dev'; + + private const KEY_PASSWORD = 'test1234'; + + public function testPgpProcessCanEncryptCorrectly() + { + //Given + $process = new PgpProcess(); + $tester = new PgpTestingProcess(); + + // When + $output = $process->encrypt('Hello there!', [self::KEY_EMAIL_ADDRESS => __DIR__ .'/_data/pgp_test_public_key.asc']); + + //Then + $decrypted = $tester->decrypt($output, __DIR__ .'/_data/pgp_test_public_key.asc', self::KEY_PASSWORD); + $this->assertSame('Hello there!', $decrypted); + } + + public function testEncrypting() + { + //Given + $encrypter = new PgpEncrypter([ + self::KEY_EMAIL_ADDRESS => __DIR__ .'/_data/pgp_test_public_key.asc' + ]); + + $email = (new Email()) + ->from(new Address(static::KEY_EMAIL_ADDRESS, 'PuLLi')) + ->to(new Address(static::KEY_EMAIL_ADDRESS, 'PuLLi')) + ->text("Hello there!\n\nHow are you?") + ->subject('PGP Mail'); + + //When + $encrypted = $encrypter->encrypt($email); + + //Then + $this->checkEncryptedMessage($encrypted); + + $encryptedString = $encrypted->toString(); + + $this->assertStringContainsString('-----BEGIN PGP MESSAGE-----', $encryptedString, 'PGP message begin is missing.'); + $this->assertStringContainsString('-----END PGP MESSAGE-----', $encryptedString, 'PGP message end is missing.'); + + [$initiliazationPart, $encryptedMessagePart] = $encrypted->getBody()->getParts(); + static::assertInstanceOf(PgpEncryptedInitializationPart::class, $initiliazationPart); + static::assertInstanceOf(PgpEncryptedMessagePart::class, $encryptedMessagePart); + + $tester = new PgpTestingProcess(); + $result = $tester->decrypt($encryptedMessagePart->toString(), __DIR__ .'/_data/pgp_test_secret_key.asc', self::KEY_PASSWORD); + $this->assertStringContainsString('Hello there!', $result, 'Unable to decrypt message.'); + } + + public function testEncryptingAndSigning() + { + $encrypter = new PgpEncrypter([ + self::KEY_EMAIL_ADDRESS => __DIR__ .'/_data/pgp_test_public_key.asc' + ]); + + $email = (new Email()) + ->from(new Address(static::KEY_EMAIL_ADDRESS, 'PuLLi')) + ->to(new Address(static::KEY_EMAIL_ADDRESS, 'PuLLi')) + ->text("Hello there!\n\nHow are you?") + ->subject('PGP Mail'); + + //When + $encrypted = $encrypter->encrypt($email); + + //Then + $this->checkEncryptedMessage($encrypted); + + $encryptedMessageString = $encrypted->toString(); + + $this->assertStringContainsString('-----BEGIN PGP MESSAGE-----', $encryptedMessageString, 'PGP message begin is missing.'); + $this->assertStringContainsString('-----END PGP MESSAGE-----', $encryptedMessageString, 'PGP message end is missing.'); + $this->assertStringNotContainsString('-----BEGIN PGP SIGNATURE-----', $encryptedMessageString, 'PGP Signature begin is present.'); + $this->assertStringNotContainsString('-----END PGP SIGNATURE-----', $encryptedMessageString, 'PGP Signature end is present.'); + + [$initiliazationPart, $encryptedMessagePart] = $encrypted->getBody()->getParts(); + static::assertInstanceOf(PgpEncryptedInitializationPart::class, $initiliazationPart); + static::assertInstanceOf(PgpEncryptedMessagePart::class, $encryptedMessagePart); + + $tester = new PgpTestingProcess(); + $result = $tester->decrypt($encryptedMessagePart->toString(), __DIR__ .'/_data/pgp_test_secret_key.asc', self::KEY_PASSWORD); + $this->assertStringContainsString('Hello there!', $result, 'Signature is not valid.'); + } + + private function checkEncryptedMessage(Message $message): void + { + $body = $message->getBody(); + + $this->assertInstanceOf(PgpEncryptedPart::class, $body, 'Message body is not encrypted.'); + + [$initializationPart, $messagePart] = $body->getParts(); + + $this->assertInstanceOf(PgpEncryptedInitializationPart::class, $initializationPart, 'Is not a PGP Initialization part.'); + $this->assertInstanceOf(PgpEncryptedMessagePart::class, $messagePart, 'Is not a PGP Message part.'); + } + + private function normalize(string $part): string + { + return str_replace("\n", "\r\n", str_replace(["\r\n", "\r"], "\n", $part)); + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/PgpSignerTest.php b/src/Symfony/Component/MimePgp/Tests/PgpSignerTest.php new file mode 100644 index 0000000000000..084072a790286 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/PgpSignerTest.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\Part\Multipart\MixedPart; +use Symfony\Component\Mime\Part\TextPart; +use Symfony\Component\MimePgp\Mime\Part\Multipart\PgpSignedPart; +use Symfony\Component\MimePgp\Mime\Part\PgpKeyPart; +use Symfony\Component\MimePgp\Mime\Part\PgpSignaturePart; +use Symfony\Component\MimePgp\PgpProcess; +use Symfony\Component\MimePgp\PgpSigner; + +class PgpSignerTest extends TestCase +{ + private const KEY_EMAIL_ADDRESS = 'pgp@pulli.dev'; + + private const KEY_PASSWORD = 'test1234'; + + public function testPgpProcessCanSignCorrectly() + { + //Given + $process = new PgpProcess(); + $tester = new PgpTestingProcess(); + + // When + $output = $process->sign('Hello there!', __DIR__ .'/_data/pgp_test_secret_key.asc', self::KEY_PASSWORD); + + //Then + $verified = $tester->verify('Hello there!', $output, __DIR__ .'/_data/pgp_test_public_key.asc'); + $this->assertTrue($verified); + $verified = $tester->verify('Hello there!', $output, __DIR__ .'/_data/other_public_key.asc'); + $this->assertFalse($verified); + } + + public function testSigningWithPublicKey() + { + $signer = new PgpSigner( + __DIR__ .'/_data/pgp_test_secret_key.asc', + __DIR__ .'/_data/pgp_test_public_key.asc', + self::KEY_PASSWORD + ); + + $email = (new Email()) + ->from(new Address(static::KEY_EMAIL_ADDRESS, 'PuLLi')) + ->to(new Address(static::KEY_EMAIL_ADDRESS, 'PuLLi')) + ->text("Hello there!\n\nHow are you?") + ->subject('PGP Mail'); + + //When + $signedMessage = $signer->sign($email); + + //Then + $this->assertInstanceOf(Message::class, $signedMessage); + + $body = $signedMessage->getBody(); + + $this->assertInstanceOf(PgpSignedPart::class, $body, 'Message is not signed.'); + + [$signedPart, $signaturePart] = $body->getParts(); + + $this->assertInstanceOf(PgpSignaturePart::class, $signaturePart, 'Not a PgpSignaturePart.'); + + $this->assertInstanceOf(MixedPart::class, $signedPart, 'SignedPart is not a MixedPart.'); + // Manually clean the signed part again + $signedPartString = $this->normalize($signedPart->toString()); + + [$signedPart, $publicKeyPart] = $signedPart->getParts(); + + $this->assertInstanceOf(TextPart::class, $signedPart, 'Message is not text part.'); + $this->assertInstanceOf(PgpKeyPart::class, $publicKeyPart, 'Message is not public key part.'); + + $signature = $signaturePart->bodyToString(); + + $this->assertStringContainsString('-----BEGIN PGP SIGNATURE-----', $signature, 'PGP Signature begin is missing.'); + $this->assertStringContainsString('-----END PGP SIGNATURE-----', $signature, 'PGP Signature end end is missing.'); + + $originalBody = $this->normalize($email->getBody()->toString()); + $this->assertStringContainsString($originalBody."\r\n", $body->toString(), 'Signed message does not contain the actual message.'); + + $tester = new PgpTestingProcess(); + $result = $tester->verify($signedPartString, $signature, __DIR__ .'/_data/pgp_test_public_key.asc'); + $this->assertTrue($result, 'Signature is not valid.'); + } + + public function testSigningWithoutPublicKey() + { + $signer = new PgpSigner( + __DIR__ .'/_data/pgp_test_secret_key.asc', + null, + self::KEY_PASSWORD + ); + + $email = (new Email()) + ->from(new Address(static::KEY_EMAIL_ADDRESS, 'PuLLi')) + ->to(new Address(static::KEY_EMAIL_ADDRESS, 'PuLLi')) + ->text("Hello there!\n\nHow are you?") + ->subject('PGP Mail'); + + //When + $signedMessage = $signer->sign($email); + + //Then + $this->assertInstanceOf(Message::class, $signedMessage); + + $body = $signedMessage->getBody(); + + $this->assertInstanceOf(PgpSignedPart::class, $body, 'Message is not signed.'); + + [$signedPart, $signaturePart] = $body->getParts(); + + $this->assertInstanceOf(PgpSignaturePart::class, $signaturePart, 'Not a PgpSignaturePart.'); + + $this->assertInstanceOf(TextPart::class, $signedPart, 'SignedPart is not a TextPart.'); + $signedPartString = $signedPart->toString(); + + $signature = $signaturePart->bodyToString(); + + $this->assertStringContainsString('-----BEGIN PGP SIGNATURE-----', $signature, 'PGP Signature begin is missing.'); + $this->assertStringContainsString('-----END PGP SIGNATURE-----', $signature, 'PGP Signature end end is missing.'); + + $originalBody = $this->normalize($email->getBody()->toString()); + $this->assertStringContainsString($originalBody."\r\n", $body->toString(), 'Signed message does not contain the actual message.'); + + $tester = new PgpTestingProcess(); + $result = $tester->verify($signedPartString, $signature, __DIR__ .'/_data/pgp_test_public_key.asc'); + $this->assertTrue($result, 'Signature is not valid.'); + } + + private function normalize(string $part): string + { + return str_replace("\n", "\r\n", str_replace(["\r\n", "\r"], "\n", $part)); + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/PgpTestingProcess.php b/src/Symfony/Component/MimePgp/Tests/PgpTestingProcess.php new file mode 100644 index 0000000000000..3eda471b5a251 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/PgpTestingProcess.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\MimePgp\Tests; + +/* + * @author PuLLi + */ +use Symfony\Component\MimePgp\Exception\RuntimeException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; + +final readonly class PgpTestingProcess +{ + public function __construct( + private string $binaryPath = 'gpg', + private string $tempPrefix = 'GPGMIME', + ) + { + } + + public function verify(string $data, string $signature, string $pgpKey): bool + { + $temporarySignature = tempnam(sys_get_temp_dir(), $this->tempPrefix); + file_put_contents($temporarySignature, $signature); + $temporaryData = tempnam(sys_get_temp_dir(), $this->tempPrefix); + file_put_contents($temporaryData, $data); + $temporaryKeyring = tempnam(sys_get_temp_dir(), $this->tempPrefix); + $this->storeKeys([$pgpKey => null], $temporaryKeyring); + + $command = [ + '--no-default-keyring', + '--primary-keyring', + $temporaryKeyring, + '--auto-key-locate', + 'local', + '--verify', + $temporarySignature, + $temporaryData, + ]; + + try { + $this->execute($command, $data); + return true; + } catch (\Throwable) { + return false; + } finally { + unlink($temporarySignature); + unlink($temporaryData); + unlink($temporaryKeyring); + } + } + + /** + * @param array $pgpKeys + */ + public function decrypt(string $data, string $pgpKey, #[\SensitiveParameter] ?string $passphrase = null): string + { + $temporaryData = tempnam(sys_get_temp_dir(), $this->tempPrefix); + file_put_contents($temporaryData, $data); + $temporaryKeyring = tempnam(sys_get_temp_dir(), $this->tempPrefix); + $this->storeKeys([$pgpKey => $passphrase], $temporaryKeyring); + + $command = [ + '--no-default-keyring', + '--primary-keyring', + $temporaryKeyring, + '--auto-key-locate', + 'local', + ]; + if ($passphrase) { + $command[] = '--pinentry-mode'; + $command[] = 'loopback'; + $command[] = '--passphrase'; + $command[] = $passphrase; + } + $command[] = '--decrypt'; + $command[] = $temporaryData; + + try { + $output = $this->execute($command, $data); + } finally { + unlink($temporaryData); + unlink($temporaryKeyring); + } + + return $output; + } + + /** + * @param string[] $command + */ + private function execute(array $command, ?string $input = null): string + { + array_unshift($command, $this->binaryPath); + $process = new Process($command); + if ($input) { + $process->setInput($input); + } + try { + $process->mustRun(); + if (!$process->isSuccessful()) { + throw new RuntimeException($process->getErrorOutput()); + } + } catch (ProcessFailedException $e) { + throw new RuntimeException($e->getMessage()); + } + + return $process->getOutput(); + } + + /** + * @param array $filePaths + */ + private function storeKeys(#[\SensitiveParameter] array $filePaths, string $keyringPath): void + { + foreach ($filePaths as $filePath => $passphrase) { + $command = [ + '--no-default-keyring', + '--primary-keyring', + $keyringPath, + ]; + if ($passphrase !== null) { + $command[] = '--pinentry-mode'; + $command[] = 'loopback'; + $command[] = '--passphrase'; + $command[] = $passphrase; + } + $command[] = '--import'; + $command[] = $filePath; + $this->execute($command); + } + } +} diff --git a/src/Symfony/Component/MimePgp/Tests/_data/other_public_key.asc b/src/Symfony/Component/MimePgp/Tests/_data/other_public_key.asc new file mode 100644 index 0000000000000..124c0260ae0f2 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/_data/other_public_key.asc @@ -0,0 +1,17 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZ7cEwBYJKwYBBAHaRw8BAQdAyfwrQ/uAao/tEEDKjHSqG+quIXR0SVpotreq +9sYb1Oe0IVRpbWUgQm90IDx0aW1lQG0uc3BvbWt5LWxhYnMuY29tPoiZBBMWCgBB +FiEEDcNI2VUxId/CwfOq7rict7pnerQFAme3BMACGwMFCQWjoPAFCwkIBwICIgIG +FQoJCAsCBBYCAwECHgcCF4AACgkQ7rict7pnerRGEAD9GzKzUlf3eDajcyculN3Y +04FUKOs8ueIbIFrGh9ISbk8A/1G4Ue2sXIDkEcwSh6B48somN8iqAxLhiIlLoXFC +0iMNtCBUaW1lIEJvdCA8dGltZUBtLnNwb21reS1sYWJzLmV1PoiZBBMWCgBBFiEE +DcNI2VUxId/CwfOq7rict7pnerQFAme3BbcCGwMFCQWjoPAFCwkIBwICIgIGFQoJ +CAsCBBYCAwECHgcCF4AACgkQ7rict7pnerQekgEA/IEPKBgczSaybl/xv/rcV83w +Jjdq2QPJgM6UlbwIU3MBALxjf8K7gaglz9uivqt4lKDrs8q0shJKcMg0AGMb4/EJ +uDgEZ7cEwBIKKwYBBAGXVQEFAQEHQC/5IhUVylIJ9ilszF7twQEu9NmhUPi4ifSJ +48Zjv7oGAwEIB4h+BBgWCgAmFiEEDcNI2VUxId/CwfOq7rict7pnerQFAme3BMAC +GwwFCQWjoPAACgkQ7rict7pnerS3fgEAheU3xwqBrcJpb+N6qW+pwntMZB+9xknK +yelJJq7gnOgBAJLtdIUjw+7RJlYf5xPIeYr+5+xAinn6RvdXrEIm4/oC +=xH7C +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/Symfony/Component/MimePgp/Tests/_data/pgp_test_public_key.asc b/src/Symfony/Component/MimePgp/Tests/_data/pgp_test_public_key.asc new file mode 100644 index 0000000000000..24e5a4e389ff8 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/_data/pgp_test_public_key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=w8wH +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/Symfony/Component/MimePgp/Tests/_data/pgp_test_secret_key.asc b/src/Symfony/Component/MimePgp/Tests/_data/pgp_test_secret_key.asc new file mode 100644 index 0000000000000..a5fc574574fa8 --- /dev/null +++ b/src/Symfony/Component/MimePgp/Tests/_data/pgp_test_secret_key.asc @@ -0,0 +1,107 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +cvLFBoHiJ0sMhrEfncqtBJnOCw== +=cAza +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/Symfony/Component/MimePgp/composer.json b/src/Symfony/Component/MimePgp/composer.json new file mode 100644 index 0000000000000..832ac68a8acf1 --- /dev/null +++ b/src/Symfony/Component/MimePgp/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/mime-pgp", + "type": "library", + "description": "Allows encryption of MIME messages via OpenPGP", + "keywords": ["openpgp", "pgp", "mime"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "symfony/mime": "^7.2", + "symfony/process": "^7.2" + }, + "require-dev": { + "egulias/email-validator": "^4.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\MimePgp\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/MimePgp/phpunit.xml.dist b/src/Symfony/Component/MimePgp/phpunit.xml.dist new file mode 100644 index 0000000000000..1f1502a86c78b --- /dev/null +++ b/src/Symfony/Component/MimePgp/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + +