Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 835f6b0

Browse files
committed
feature #30981 [Mime] S/MIME Support (sstok)
This PR was merged into the 4.4 branch. Discussion ---------- [Mime] S/MIME Support | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes <!-- don't forget to update src/**/CHANGELOG.md files --> | BC breaks? | no | Deprecations? | no | Tests pass? | no | Fixed tickets | #30875 | License | MIT | Doc PR | TODO ~~This is a heavy work in progress and far from working, I tried to finish this before the end of FOSSA but due to the large complexity of working with raw Mime data it will properly take a week or so (at least I hope so..) to completely finish this. I'm sharing it here for the statistics.~~ This adds the S/MIME Signer and Encryptor, unlike the Swiftmailer implementation both these functionalities have been separated. When a transporter doesn't support the Raw MIME entity information it will fallback to the original message (without signing/encryption). In any case using a secure connection is always the best guarantee against modification or information disclosure. Commits ------- 6e70d12 [Mime] Added SMimeSigner and Encryptor
2 parents ca566a5 + 6e70d12 commit 835f6b0

23 files changed

+1074
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\Crypto;
13+
14+
use Symfony\Component\Mime\Exception\RuntimeException;
15+
use Symfony\Component\Mime\Part\SMimePart;
16+
17+
/**
18+
* @author Sebastiaan Stok <[email protected]>
19+
*
20+
* @internal
21+
*/
22+
abstract class SMime
23+
{
24+
protected function normalizeFilePath(string $path): string
25+
{
26+
if (!file_exists($path)) {
27+
throw new RuntimeException(sprintf('File does not exist: %s.', $path));
28+
}
29+
30+
return 'file://'.str_replace('\\', '/', realpath($path));
31+
}
32+
33+
protected function iteratorToFile(iterable $iterator, $stream): void
34+
{
35+
foreach ($iterator as $chunk) {
36+
fwrite($stream, $chunk);
37+
}
38+
}
39+
40+
protected function convertMessageToSMimePart($stream, string $type, string $subtype): SMimePart
41+
{
42+
rewind($stream);
43+
44+
$headers = '';
45+
46+
while (!feof($stream)) {
47+
$buffer = fread($stream, 78);
48+
$headers .= $buffer;
49+
50+
// Detect ending of header list
51+
if (preg_match('/(\r\n\r\n|\n\n)/', $headers, $match)) {
52+
$headersPosEnd = strpos($headers, $headerBodySeparator = $match[0]);
53+
54+
break;
55+
}
56+
}
57+
58+
$headers = $this->getMessageHeaders(trim(substr($headers, 0, $headersPosEnd)));
59+
60+
fseek($stream, $headersPosEnd + \strlen($headerBodySeparator));
61+
62+
return new SMimePart($this->getStreamIterator($stream), $type, $subtype, $this->getParametersFromHeader($headers['content-type']));
63+
}
64+
65+
protected function getStreamIterator($stream): iterable
66+
{
67+
while (!feof($stream)) {
68+
yield fread($stream, 16372);
69+
}
70+
}
71+
72+
private function getMessageHeaders(string $headerData): array
73+
{
74+
$headers = [];
75+
$headerLines = explode("\r\n", str_replace("\n", "\r\n", str_replace("\r\n", "\n", $headerData)));
76+
$currentHeaderName = '';
77+
78+
// Transform header lines into an associative array
79+
foreach ($headerLines as $headerLine) {
80+
// Empty lines between headers indicate a new mime-entity
81+
if ('' === $headerLine) {
82+
break;
83+
}
84+
85+
// Handle headers that span multiple lines
86+
if (false === strpos($headerLine, ':')) {
87+
$headers[$currentHeaderName] .= ' '.trim($headerLine);
88+
continue;
89+
}
90+
91+
$header = explode(':', $headerLine, 2);
92+
$currentHeaderName = strtolower($header[0]);
93+
$headers[$currentHeaderName] = trim($header[1]);
94+
}
95+
96+
return $headers;
97+
}
98+
99+
private function getParametersFromHeader(string $header): array
100+
{
101+
$params = [];
102+
103+
preg_match_all('/(?P<name>[a-z-0-9]+)=(?P<value>"[^"]+"|(?:[^\s;]+|$))(?:\s+;)?/i', $header, $matches);
104+
105+
foreach ($matches['value'] as $pos => $paramValue) {
106+
$params[$matches['name'][$pos]] = trim($paramValue, '"');
107+
}
108+
109+
return $params;
110+
}
111+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\Crypto;
13+
14+
use Symfony\Component\Mime\Exception\RuntimeException;
15+
use Symfony\Component\Mime\Message;
16+
17+
/**
18+
* @author Sebastiaan Stok <[email protected]>
19+
*/
20+
final class SMimeEncrypter extends SMime
21+
{
22+
private $certs;
23+
private $cipher;
24+
25+
/**
26+
* @param string|string[] $certificate Either a lone X.509 certificate, or an array of X.509 certificates
27+
*/
28+
public function __construct($certificate, int $cipher = OPENSSL_CIPHER_AES_256_CBC)
29+
{
30+
if (\is_array($certificate)) {
31+
$this->certs = array_map([$this, 'normalizeFilePath'], $certificate);
32+
} else {
33+
$this->certs = $this->normalizeFilePath($certificate);
34+
}
35+
36+
$this->cipher = $cipher;
37+
}
38+
39+
public function encrypt(Message $message): Message
40+
{
41+
$bufferFile = tmpfile();
42+
$outputFile = tmpfile();
43+
44+
$this->iteratorToFile($message->toIterable(), $bufferFile);
45+
46+
if (!@openssl_pkcs7_encrypt(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->certs, [], 0, $this->cipher)) {
47+
throw new RuntimeException(sprintf('Failed to encrypt S/Mime message. Error: "%s".', openssl_error_string()));
48+
}
49+
50+
$mimePart = $this->convertMessageToSMimePart($outputFile, 'application', 'pkcs7-mime');
51+
$mimePart->getHeaders()
52+
->addTextHeader('Content-Transfer-Encoding', 'base64')
53+
->addParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'smime.p7m'])
54+
;
55+
56+
return new Message($message->getHeaders(), $mimePart);
57+
}
58+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\Crypto;
13+
14+
use Symfony\Component\Mime\Exception\RuntimeException;
15+
use Symfony\Component\Mime\Message;
16+
17+
/**
18+
* @author Sebastiaan Stok <[email protected]>
19+
*/
20+
final class SMimeSigner extends SMime
21+
{
22+
private $signCertificate;
23+
private $signPrivateKey;
24+
private $signOptions;
25+
private $extraCerts;
26+
27+
/**
28+
* @var string|null
29+
*/
30+
private $privateKeyPassphrase;
31+
32+
/**
33+
* @see https://secure.php.net/manual/en/openssl.pkcs7.flags.php
34+
*
35+
* @param string $certificate
36+
* @param string $privateKey A file containing the private key (in PEM format)
37+
* @param string|null $privateKeyPassphrase A passphrase of the private key (if any)
38+
* @param string $extraCerts A file containing intermediate certificates (in PEM format) needed by the signing certificate
39+
* @param int $signOptions Bitwise operator options for openssl_pkcs7_sign()
40+
*/
41+
public function __construct(string $certificate, string $privateKey, ?string $privateKeyPassphrase = null, ?string $extraCerts = null, int $signOptions = PKCS7_DETACHED)
42+
{
43+
$this->signCertificate = $this->normalizeFilePath($certificate);
44+
45+
if (null !== $privateKeyPassphrase) {
46+
$this->signPrivateKey = [$this->normalizeFilePath($privateKey), $privateKeyPassphrase];
47+
} else {
48+
$this->signPrivateKey = $this->normalizeFilePath($privateKey);
49+
}
50+
51+
$this->signOptions = $signOptions;
52+
$this->extraCerts = $extraCerts ? realpath($extraCerts) : null;
53+
$this->privateKeyPassphrase = $privateKeyPassphrase;
54+
}
55+
56+
public function sign(Message $message): Message
57+
{
58+
$bufferFile = tmpfile();
59+
$outputFile = tmpfile();
60+
61+
$this->iteratorToFile($message->getBody()->toIterable(), $bufferFile);
62+
63+
if (!@openssl_pkcs7_sign(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->signCertificate, $this->signPrivateKey, [], $this->signOptions, $this->extraCerts)) {
64+
throw new RuntimeException(sprintf('Failed to sign S/Mime message. Error: "%s".', openssl_error_string()));
65+
}
66+
67+
return new Message($message->getHeaders(), $this->convertMessageToSMimePart($outputFile, 'multipart', 'signed'));
68+
}
69+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\Part;
13+
14+
use Symfony\Component\Mime\Header\Headers;
15+
16+
/**
17+
* @author Sebastiaan Stok <[email protected]>
18+
*
19+
* @experimental in 4.4
20+
*/
21+
class SMimePart extends AbstractPart
22+
{
23+
private $body;
24+
private $type;
25+
private $subtype;
26+
private $parameters;
27+
28+
/**
29+
* @param iterable|string $body
30+
*/
31+
public function __construct($body, string $type, string $subtype, array $parameters)
32+
{
33+
parent::__construct();
34+
35+
if (!\is_string($body) && !is_iterable($body)) {
36+
throw new \TypeError(sprintf('The body of "%s" must be a string or a iterable (got "%s").', self::class, \is_object($body) ? \get_class($body) : \gettype($body)));
37+
}
38+
39+
$this->body = $body;
40+
$this->type = $type;
41+
$this->subtype = $subtype;
42+
$this->parameters = $parameters;
43+
}
44+
45+
public function getMediaType(): string
46+
{
47+
return $this->type;
48+
}
49+
50+
public function getMediaSubtype(): string
51+
{
52+
return $this->subtype;
53+
}
54+
55+
public function bodyToString(): string
56+
{
57+
if (\is_string($this->body)) {
58+
return $this->body;
59+
}
60+
61+
$body = '';
62+
foreach ($this->body as $chunk) {
63+
$body .= $chunk;
64+
}
65+
$this->body = $body;
66+
67+
return $body;
68+
}
69+
70+
public function bodyToIterable(): iterable
71+
{
72+
if (\is_string($this->body)) {
73+
yield $this->body;
74+
75+
return;
76+
}
77+
78+
$body = '';
79+
foreach ($this->body as $chunk) {
80+
$body .= $chunk;
81+
yield $chunk;
82+
}
83+
$this->body = $body;
84+
}
85+
86+
public function getPreparedHeaders(): Headers
87+
{
88+
$headers = clone parent::getHeaders();
89+
90+
$headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype());
91+
92+
foreach ($this->parameters as $name => $value) {
93+
$headers->setHeaderParameter('Content-Type', $name, $value);
94+
}
95+
96+
return $headers;
97+
}
98+
99+
public function __sleep(): array
100+
{
101+
// convert iterables to strings for serialization
102+
if (is_iterable($this->body)) {
103+
$this->body = $this->bodyToString();
104+
}
105+
106+
$this->_headers = $this->getHeaders();
107+
108+
return ['_headers', 'body', 'type', 'subtype', 'parameters'];
109+
}
110+
111+
public function __wakeup(): void
112+
{
113+
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
114+
$r->setAccessible(true);
115+
$r->setValue($this, $this->_headers);
116+
unset($this->_headers);
117+
}
118+
}

0 commit comments

Comments
 (0)