Skip to content

Commit 147d1b7

Browse files
committed
[Mime] Added SMimeSigner and Encryptor
1 parent aa4385d commit 147d1b7

23 files changed

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

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy