Skip to content

Commit b6614d9

Browse files
committed
[Mime] Added SMimeSigner and Encryptor
1 parent e9aaaaf commit b6614d9

27 files changed

+992
-2
lines changed

src/Symfony/Component/Mime/MessageConverter.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ final class MessageConverter
3030
*/
3131
public static function toEmail(RawMessage $message): Email
3232
{
33+
if ($message instanceof WrappedMessage) {
34+
$message = $message->getOriginalMessage();
35+
}
36+
3337
if ($message instanceof Email) {
3438
return $message;
3539
}

src/Symfony/Component/Mime/RawMessage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
class RawMessage implements \Serializable
2020
{
21-
private $message;
21+
protected $message;
2222

2323
/**
2424
* @param iterable|string $message
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 <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\Filesystem\Filesystem;
15+
use Symfony\Component\Mime\Exception\RuntimeException;
16+
17+
/**
18+
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
19+
*
20+
* @experimental in 4.3
21+
*/
22+
abstract class SMime
23+
{
24+
protected $filesystem;
25+
26+
protected function normalizeFilePath(string $path): string
27+
{
28+
if (!file_exists($path)) {
29+
throw new RuntimeException(sprintf('File does not exist: %s', $path));
30+
}
31+
32+
return 'file://'.str_replace('\\', '/', realpath($path));
33+
}
34+
35+
protected function generateTmpFilename(): string
36+
{
37+
if (null === $this->filesystem) {
38+
if (!class_exists(Filesystem::class)) {
39+
throw new \Exception('In order to use the S/MIME functionality, the symfony/filesystem component must be installed');
40+
}
41+
42+
$this->filesystem = new Filesystem();
43+
}
44+
45+
return $this->filesystem->tempnam(sys_get_temp_dir(), 'smime');
46+
}
47+
48+
protected function iteratorToFile(iterable $iterator, string $bufferFile): void
49+
{
50+
$f = fopen($bufferFile, 'wb');
51+
52+
foreach ($iterator as $chunk) {
53+
fwrite($f, $chunk);
54+
}
55+
56+
fclose($f);
57+
}
58+
59+
protected function fileToIterator(string $bufferFile): iterable
60+
{
61+
$stream = fopen($bufferFile, 'rb');
62+
63+
while (!feof($stream)) {
64+
yield fread($stream, 16372);
65+
}
66+
67+
fclose($stream);
68+
}
69+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\WrappedMessage;
17+
18+
/**
19+
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
20+
*
21+
* @experimental in 4.3
22+
*/
23+
final class SMimeEncrypter extends SMime
24+
{
25+
private $certs;
26+
private $cipher;
27+
28+
/**
29+
* @param string|string[] $certificate Either a lone X.509 certificate, or an array of X.509 certificates
30+
*/
31+
public function __construct($certificate, int $cipher = OPENSSL_CIPHER_AES_256_CBC)
32+
{
33+
if (\is_array($certificate)) {
34+
$this->certs = array_map([$this, 'normalizeFilePath'], $certificate);
35+
} else {
36+
$this->certs = $this->normalizeFilePath($certificate);
37+
}
38+
39+
$this->cipher = $cipher;
40+
}
41+
42+
public function encryptMessage(Message $message): WrappedMessage
43+
{
44+
$bufferFile = $this->generateTmpFilename();
45+
$outputFile = $this->generateTmpFilename();
46+
47+
$this->iteratorToFile($message->toIterable(), $bufferFile);
48+
49+
if (!@openssl_pkcs7_encrypt($bufferFile, $outputFile, $this->certs, [], 0, $this->cipher)) {
50+
throw new RuntimeException(sprintf('Failed to encrypt S/Mime message. Error: "%s".', openssl_error_string()));
51+
}
52+
53+
return new WrappedMessage($message, $this->fileToIterator($outputFile));
54+
}
55+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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\WrappedMessage;
17+
18+
/**
19+
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
20+
*
21+
* @experimental in 4.3
22+
*/
23+
final class SMimeSigner extends SMime
24+
{
25+
private $signCertificate;
26+
private $signPrivateKey;
27+
private $signOptions;
28+
private $extraCerts;
29+
30+
/**
31+
* @var string|null
32+
*/
33+
private $privateKeyPassphrase;
34+
35+
/**
36+
* @see https://secure.php.net/manual/en/openssl.pkcs7.flags.php
37+
*
38+
* @param string $certificate
39+
* @param string $privateKey A file containing the private key (in PEM format)
40+
* @param string|null $privateKeyPassphrase A passphrase of the private key (if any)
41+
* @param string $extraCerts A file containing intermediate certificates (in PEM format) needed by the signing certificate
42+
* @param int $signOptions Bitwise operator options for openssl_pkcs7_sign()
43+
*/
44+
public function __construct(string $certificate, string $privateKey, ?string $privateKeyPassphrase = null, ?string $extraCerts = null, int $signOptions = PKCS7_DETACHED)
45+
{
46+
$this->signCertificate = $this->normalizeFilePath($certificate);
47+
48+
if (null !== $privateKeyPassphrase) {
49+
$this->signPrivateKey = [$this->normalizeFilePath($privateKey), $privateKeyPassphrase];
50+
} else {
51+
$this->signPrivateKey = $this->normalizeFilePath($privateKey);
52+
}
53+
54+
$this->signOptions = $signOptions;
55+
$this->extraCerts = $extraCerts ? realpath($extraCerts) : null;
56+
$this->privateKeyPassphrase = $privateKeyPassphrase;
57+
}
58+
59+
public function signMessage(Message $message): WrappedMessage
60+
{
61+
$bufferFile = $this->generateTmpFilename();
62+
$outputFile = $this->generateTmpFilename();
63+
64+
$this->iteratorToFile($message->toIterable(), $bufferFile);
65+
66+
if (!@openssl_pkcs7_sign($bufferFile, $outputFile, $this->signCertificate, $this->signPrivateKey, [], $this->signOptions, $this->extraCerts)) {
67+
throw new RuntimeException(sprintf('Failed to sign S/Mime message. Error: "%s".', openssl_error_string()));
68+
}
69+
70+
return new WrappedMessage($message, $this->fileToIterator($outputFile));
71+
}
72+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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\Tests\Security;
13+
14+
use Symfony\Component\Mime\Email;
15+
use Symfony\Component\Mime\Security\SMimeEncrypter;
16+
use Symfony\Component\Mime\WrappedMessage;
17+
18+
class SMimeEncryptorTest extends SMimeTestCase
19+
{
20+
public function testEncryptMessage()
21+
{
22+
$message = (new Email())
23+
->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris')))
24+
->to('fabien@symfony.com')
25+
->subject('Testing')
26+
->from('noreply@example.com')
27+
->text('El Barto was not here');
28+
29+
$message->getHeaders()->addIdHeader('Message-ID', 'some@id');
30+
31+
$encrypter = new SMimeEncrypter($this->samplesDir.'encrypt.crt');
32+
$signedMessage = $encrypter->encryptMessage($message);
33+
34+
$this->assertMessageIsEncryptedProperly($signedMessage);
35+
}
36+
37+
public function testEncryptMessageWithMultipleCerts()
38+
{
39+
$message = (new Email())
40+
->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris')))
41+
->to('fabien@symfony.com')
42+
->subject('Testing')
43+
->from('noreply@example.com')
44+
->text('El Barto was not here');
45+
46+
$message2 = (new Email())
47+
->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris')))
48+
->to('luna@symfony.com')
49+
->subject('Testing')
50+
->from('noreply@example.com')
51+
->text('El Barto was not here');
52+
53+
$message->getHeaders()->addIdHeader('Message-ID', 'some@id');
54+
$message2->getHeaders()->addIdHeader('Message-ID', 'some@id2');
55+
56+
$encrypter = new SMimeEncrypter(['fabien@symfony.com' => $this->samplesDir.'encrypt.crt', 'luna@symfony.com' => $this->samplesDir.'encrypt2.crt']);
57+
58+
$this->assertMessageIsEncryptedProperly($encrypter->encryptMessage($message));
59+
$this->assertMessageIsEncryptedProperly($encrypter->encryptMessage($message2));
60+
}
61+
62+
private function assertMessageIsEncryptedProperly(WrappedMessage $message): void
63+
{
64+
$messageFile = $this->normalizeFilePath($this->generateTmpFilename());
65+
file_put_contents($messageFile, $message->toString());
66+
67+
$outputFile = $this->normalizeFilePath($this->generateTmpFilename());
68+
69+
$this->assertTrue(
70+
openssl_pkcs7_decrypt(
71+
$messageFile,
72+
$outputFile,
73+
'file://'.$this->samplesDir.'encrypt.crt',
74+
'file://'.$this->samplesDir.'encrypt.key'
75+
),
76+
sprintf('Decryption of the message failed. Internal error "%s".', openssl_error_string())
77+
);
78+
$this->assertEquals($message->getOriginalMessage()->toString(), file_get_contents($outputFile));
79+
}
80+
}

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