Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Mime] Add PGP feature #50222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions 1 composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
"monolog/monolog": "^1.25.1|^2",
"nyholm/psr7": "^1.0",
"pda/pheanstalk": "^4.0",
"pear/crypt_gpg": "^1.6",
"php-http/discovery": "^1.15",
"php-http/httplug": "^1.0|^2.0",
"php-http/message-factory": "^1.0",
Expand Down
194 changes: 194 additions & 0 deletions 194 src/Symfony/Component/Mime/Crypto/PGPEncrypter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mime\Crypto;

use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\MailboxListHeader;
use Symfony\Component\Mime\Helper\PGPSigningPreparer;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\Multipart\MixedPart;
use Symfony\Component\Mime\Part\Multipart\PGPEncryptedPart;
use Symfony\Component\Mime\Part\Multipart\PGPSignedPart;
use Symfony\Component\Mime\Part\PGPEncryptedInitializationPart;
use Symfony\Component\Mime\Part\PGPEncryptedMessagePart;
use Symfony\Component\Mime\Part\PGPKeyPart;
use Symfony\Component\Mime\Part\PGPSignaturePart;

/*
* @author PuLLi <the@pulli.dev>
*/
class PGPEncrypter
{
use PGPSigningPreparer;

private \Crypt_GPG $gpg;

private Headers $headers;

private ?string $signingKey = null;

public string $signature = '';

public string $signed = '';

/**
* @throws \Crypt_GPG_FileException
* @throws \PEAR_Exception
*/
public function __construct(array $options = [])
{
$this->gpg = new \Crypt_GPG(
array_merge(
$options,
[
'cipher-algo' => 'AES256',
'digest-algo' => 'SHA512',
]
)
);
}

public function signingKey(string $keyIdentifier): void
{
$this->signingKey = $keyIdentifier;
}

/**
* @throws \Crypt_GPG_Exception
* @throws \Crypt_GPG_KeyNotFoundException
*/
public function encrypt(Message $message, bool $attachKey = false): Message
{
return $this->encryptWithOrWithoutSigning($message, false, null, $attachKey);
}

/**
* @throws \Crypt_GPG_Exception
* @throws \Crypt_GPG_KeyNotFoundException
* @throws \Crypt_GPG_BadPassphraseException
*/
public function encryptAndSign(Message $message, string $passphrase = null, bool $attachKey = false): Message
{
return $this->encryptWithOrWithoutSigning($message, true, $passphrase, $attachKey);
}

/**
* @throws \Crypt_GPG_Exception
* @throws \Crypt_GPG_KeyNotFoundException
* @throws \Crypt_GPG_BadPassphraseException
*/
private function encryptWithOrWithoutSigning(Message $message, bool $sign = false, string $passphrase = null, bool $attachKey = false): Message
{
$this->headers = $message->getHeaders();
$body = $message->getBody();

foreach ($this->getRecipients() as $recipient) {
$this->gpg->addEncryptKey($recipient);
}

if ($attachKey) {
$body = $this->attachPublicKey($message);
}

if ($sign) {
$this->gpg->addSignKey($this->determineSigningKey(), $passphrase);
$body = $this->gpg->encryptAndSign($body->toString());
} else {
$body = $this->gpg->encrypt($body->toString());
}

$part = new PGPEncryptedPart(
new PGPEncryptedInitializationPart(),
new PGPEncryptedMessagePart($body)
);

return new Message($this->headers, $part);
}

/**
* @throws \Crypt_GPG_Exception
* @throws \Crypt_GPG_KeyNotFoundException
* @throws \Crypt_GPG_BadPassphraseException
*/
public function sign(Message $message, string $passphrase = null, bool $attachKey = false): Message
{
$this->headers = $message->getHeaders();
$body = $message->getBody()->toString();
$messagePart = $message->getBody();

if ($attachKey) {
$mixed = $this->attachPublicKey($message);
$body = $mixed->toString();
$messagePart = $mixed;
}
// TODO: find a way to normalize Message body and pass it along to PGPSignedPart
$body = $this->prepareMessageForSigning($messagePart, $body);
$this->signed = $body;

$this->gpg->addSignKey($this->determineSigningKey(), $passphrase);
$signature = $this->gpg->sign($body, \Crypt_GPG::SIGN_MODE_DETACHED);
$this->signature = $signature;
$part = new PGPSignedPart(
$messagePart,
new PGPSignaturePart($signature)
);

return new Message($this->headers, $part);
}

/**
* @throws \Crypt_GPG_Exception
* @throws \Crypt_GPG_KeyNotFoundException
*/
private function attachPublicKey(Message $message): MixedPart
{
$publicKey = $this->gpg->exportPublicKey($this->determineSigningKey());
$key = new PGPKeyPart($publicKey);

// TODO: find more elegant way than to create another MixedPart, if Message is already a MixedPart
return new MixedPart($message->getBody(), $key);
}

private function getRecipients(): array
{
$recipients = [
$this->getAddresses('to'),
$this->getAddresses('cc'),
$this->getAddresses('bcc'),
];

return array_merge(...$recipients);
}

private function getFrom(): string
{
return $this->getAddresses('from')[0];
}

private function getAddresses(string $type): array
{
$addresses = [];
$addressType = $this->headers->get($type);
if ($addressType instanceof MailboxListHeader) {
foreach ($addressType->getAddresses() as $address) {
$addresses[] = $address->getAddress();
}
}

return $addresses;
}

private function determineSigningKey(): string
{
return $this->signingKey ?? $this->getFrom();
}
}
65 changes: 65 additions & 0 deletions 65 src/Symfony/Component/Mime/Helper/PGPSigningPreparer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mime\Helper;

use Symfony\Component\Mime\Part\AbstractMultipartPart;
use Symfony\Component\Mime\Part\AbstractPart;

/*
* @author PuLLi <the@pulli.dev>
*
*/
trait PGPSigningPreparer
{
protected function normalizeLineEnding(string $text): string
{
return str_replace("\n", "\r\n", str_replace(["\r\n", "\r"], "\n", $text));
}

protected function prepareMessageForSigning(AbstractPart $part, string $msg): string
{
// Only text part
if ('text' === $part->getMediaType()) {
$msg = $this->getMessage($part, $msg);
} elseif ($part instanceof AbstractMultipartPart) {
// Find the text part inside the multipart
$msg = $this->findTextPart($part->getParts(), $msg);
}

return $msg;
}

/**
* @param AbstractPart[] $parts
*/
protected function findTextPart(array $parts, string $msg): string
{
foreach ($parts as $part) {
$msg = $this->prepareMessageForSigning($part, $msg);
}

return $msg;
}

protected function getMessage(AbstractPart $part, string $msg): string
{
$textPart = $part->toString();
$normalizedText = $this->normalizeLineEnding($textPart);

// If text part has no extra line endings, add them
if (str_contains("\r\n--", $textPart)) {
return str_replace("$textPart\r\n--", "$normalizedText\r\n\r\n--", $msg);
}

return str_replace($textPart, "$normalizedText\r\n", $msg);
}
}
34 changes: 34 additions & 0 deletions 34 src/Symfony/Component/Mime/Part/Multipart/PGPEncryptedPart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mime\Part\Multipart;

use Symfony\Component\Mime\Part\AbstractMultipartPart;
use Symfony\Component\Mime\Part\AbstractPart;

/*
* @author PuLLi <the@pulli.dev>
*/
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';
}
}
61 changes: 61 additions & 0 deletions 61 src/Symfony/Component/Mime/Part/Multipart/PGPSignedPart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mime\Part\Multipart;

use Symfony\Component\Mime\Helper\PGPSigningPreparer;
use Symfony\Component\Mime\Part\AbstractMultipartPart;
use Symfony\Component\Mime\Part\AbstractPart;

/*
* @author PuLLi <the@pulli.dev>
*/
class PGPSignedPart extends AbstractMultipartPart
{
use PGPSigningPreparer;

public function __construct(AbstractPart ...$parts)
{
parent::__construct(...$parts);
$this->getHeaders()->addParameterizedHeader('Content-Type', 'multipart/signed', [
'micalg' => 'pgp-sha512',
'protocol' => 'application/pgp-signature',
]);
}

public function getMediaSubtype(): string
{
return 'signed';
}

public function toString(): string
{
// We only have a text/multipart and the signature
$parts = $this->getParts();

return $this->prepareMessageForSigning($parts[0], parent::toString());
}

public function toIterable(): iterable
{
yield $this->toString();
}

public function bodyToString(): string
{
return "This is an OpenPGP/MIME signed message (RFC 3156 and 4880).\r\n\r\n".parent::bodyToString();
}

public function bodyToIterable(): iterable
{
yield $this->bodyToString();
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.