first commit
This commit is contained in:
97
vendor/symfony/mime/Crypto/DkimOptions.php
vendored
Normal file
97
vendor/symfony/mime/Crypto/DkimOptions.php
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* A helper providing autocompletion for available DkimSigner options.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class DkimOptions
|
||||
{
|
||||
private array $options = [];
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function algorithm(string $algo): static
|
||||
{
|
||||
$this->options['algorithm'] = $algo;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function signatureExpirationDelay(int $show): static
|
||||
{
|
||||
$this->options['signature_expiration_delay'] = $show;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function bodyMaxLength(int $max): static
|
||||
{
|
||||
$this->options['body_max_length'] = $max;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function bodyShowLength(bool $show): static
|
||||
{
|
||||
$this->options['body_show_length'] = $show;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function headerCanon(string $canon): static
|
||||
{
|
||||
$this->options['header_canon'] = $canon;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function bodyCanon(string $canon): static
|
||||
{
|
||||
$this->options['body_canon'] = $canon;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function headersToIgnore(array $headers): static
|
||||
{
|
||||
$this->options['headers_to_ignore'] = $headers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
217
vendor/symfony/mime/Crypto/DkimSigner.php
vendored
Normal file
217
vendor/symfony/mime/Crypto/DkimSigner.php
vendored
Normal file
@ -0,0 +1,217 @@
|
||||
<?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\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Mime\Exception\RuntimeException;
|
||||
use Symfony\Component\Mime\Header\UnstructuredHeader;
|
||||
use Symfony\Component\Mime\Message;
|
||||
use Symfony\Component\Mime\Part\AbstractPart;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* RFC 6376 and 8301
|
||||
*/
|
||||
final class DkimSigner
|
||||
{
|
||||
public const CANON_SIMPLE = 'simple';
|
||||
public const CANON_RELAXED = 'relaxed';
|
||||
|
||||
public const ALGO_SHA256 = 'rsa-sha256';
|
||||
public const ALGO_ED25519 = 'ed25519-sha256'; // RFC 8463
|
||||
|
||||
private \OpenSSLAsymmetricKey $key;
|
||||
private string $domainName;
|
||||
private string $selector;
|
||||
private array $defaultOptions;
|
||||
|
||||
/**
|
||||
* @param string $pk The private key as a string or the path to the file containing the private key, should be prefixed with file:// (in PEM format)
|
||||
* @param string $passphrase A passphrase of the private key (if any)
|
||||
*/
|
||||
public function __construct(string $pk, string $domainName, string $selector, array $defaultOptions = [], string $passphrase = '')
|
||||
{
|
||||
if (!\extension_loaded('openssl')) {
|
||||
throw new \LogicException('PHP extension "openssl" is required to use DKIM.');
|
||||
}
|
||||
$this->key = openssl_pkey_get_private($pk, $passphrase) ?: throw new InvalidArgumentException('Unable to load DKIM private key: '.openssl_error_string());
|
||||
$this->domainName = $domainName;
|
||||
$this->selector = $selector;
|
||||
$this->defaultOptions = $defaultOptions + [
|
||||
'algorithm' => self::ALGO_SHA256,
|
||||
'signature_expiration_delay' => 0,
|
||||
'body_max_length' => \PHP_INT_MAX,
|
||||
'body_show_length' => false,
|
||||
'header_canon' => self::CANON_RELAXED,
|
||||
'body_canon' => self::CANON_RELAXED,
|
||||
'headers_to_ignore' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function sign(Message $message, array $options = []): Message
|
||||
{
|
||||
$options += $this->defaultOptions;
|
||||
if (!\in_array($options['algorithm'], [self::ALGO_SHA256, self::ALGO_ED25519], true)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid DKIM signing algorithm "%s".', $options['algorithm']));
|
||||
}
|
||||
$headersToIgnore['return-path'] = true;
|
||||
$headersToIgnore['x-transport'] = true;
|
||||
foreach ($options['headers_to_ignore'] as $name) {
|
||||
$headersToIgnore[strtolower($name)] = true;
|
||||
}
|
||||
unset($headersToIgnore['from']);
|
||||
$signedHeaderNames = [];
|
||||
$headerCanonData = '';
|
||||
$headers = $message->getPreparedHeaders();
|
||||
foreach ($headers->getNames() as $name) {
|
||||
foreach ($headers->all($name) as $header) {
|
||||
if (isset($headersToIgnore[strtolower($header->getName())])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('' !== $header->getBodyAsString()) {
|
||||
$headerCanonData .= $this->canonicalizeHeader($header->toString(), $options['header_canon']);
|
||||
$signedHeaderNames[] = $header->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[$bodyHash, $bodyLength] = $this->hashBody($message->getBody(), $options['body_canon'], $options['body_max_length']);
|
||||
|
||||
$params = [
|
||||
'v' => '1',
|
||||
'q' => 'dns/txt',
|
||||
'a' => $options['algorithm'],
|
||||
'bh' => base64_encode($bodyHash),
|
||||
'd' => $this->domainName,
|
||||
'h' => implode(': ', $signedHeaderNames),
|
||||
'i' => '@'.$this->domainName,
|
||||
's' => $this->selector,
|
||||
't' => time(),
|
||||
'c' => $options['header_canon'].'/'.$options['body_canon'],
|
||||
];
|
||||
|
||||
if ($options['body_show_length']) {
|
||||
$params['l'] = $bodyLength;
|
||||
}
|
||||
if ($options['signature_expiration_delay']) {
|
||||
$params['x'] = $params['t'] + $options['signature_expiration_delay'];
|
||||
}
|
||||
$value = '';
|
||||
foreach ($params as $k => $v) {
|
||||
$value .= $k.'='.$v.'; ';
|
||||
}
|
||||
$value = trim($value);
|
||||
$header = new UnstructuredHeader('DKIM-Signature', $value);
|
||||
$headerCanonData .= rtrim($this->canonicalizeHeader($header->toString()."\r\n b=", $options['header_canon']));
|
||||
if (self::ALGO_SHA256 === $options['algorithm']) {
|
||||
if (!openssl_sign($headerCanonData, $signature, $this->key, \OPENSSL_ALGO_SHA256)) {
|
||||
throw new RuntimeException('Unable to sign DKIM hash: '.openssl_error_string());
|
||||
}
|
||||
} else {
|
||||
throw new \RuntimeException(sprintf('The "%s" DKIM signing algorithm is not supported yet.', self::ALGO_ED25519));
|
||||
}
|
||||
$header->setValue($value.' b='.trim(chunk_split(base64_encode($signature), 73, ' ')));
|
||||
$headers->add($header);
|
||||
|
||||
return new Message($headers, $message->getBody());
|
||||
}
|
||||
|
||||
private function canonicalizeHeader(string $header, string $headerCanon): string
|
||||
{
|
||||
if (self::CANON_RELAXED !== $headerCanon) {
|
||||
return $header."\r\n";
|
||||
}
|
||||
|
||||
$exploded = explode(':', $header, 2);
|
||||
$name = strtolower(trim($exploded[0]));
|
||||
$value = str_replace("\r\n", '', $exploded[1]);
|
||||
$value = trim(preg_replace("/[ \t][ \t]+/", ' ', $value));
|
||||
|
||||
return $name.':'.$value."\r\n";
|
||||
}
|
||||
|
||||
private function hashBody(AbstractPart $body, string $bodyCanon, int $maxLength): array
|
||||
{
|
||||
$hash = hash_init('sha256');
|
||||
$relaxed = self::CANON_RELAXED === $bodyCanon;
|
||||
$currentLine = '';
|
||||
$emptyCounter = 0;
|
||||
$isSpaceSequence = false;
|
||||
$length = 0;
|
||||
foreach ($body->bodyToIterable() as $chunk) {
|
||||
$canon = '';
|
||||
for ($i = 0, $len = \strlen($chunk); $i < $len; ++$i) {
|
||||
switch ($chunk[$i]) {
|
||||
case "\r":
|
||||
break;
|
||||
case "\n":
|
||||
// previous char is always \r
|
||||
if ($relaxed) {
|
||||
$isSpaceSequence = false;
|
||||
}
|
||||
if ('' === $currentLine) {
|
||||
++$emptyCounter;
|
||||
} else {
|
||||
$currentLine = '';
|
||||
$canon .= "\r\n";
|
||||
}
|
||||
break;
|
||||
case ' ':
|
||||
case "\t":
|
||||
if ($relaxed) {
|
||||
$isSpaceSequence = true;
|
||||
break;
|
||||
}
|
||||
// no break
|
||||
default:
|
||||
if ($emptyCounter > 0) {
|
||||
$canon .= str_repeat("\r\n", $emptyCounter);
|
||||
$emptyCounter = 0;
|
||||
}
|
||||
if ($isSpaceSequence) {
|
||||
$currentLine .= ' ';
|
||||
$canon .= ' ';
|
||||
$isSpaceSequence = false;
|
||||
}
|
||||
$currentLine .= $chunk[$i];
|
||||
$canon .= $chunk[$i];
|
||||
}
|
||||
}
|
||||
|
||||
if ($length + \strlen($canon) >= $maxLength) {
|
||||
$canon = substr($canon, 0, $maxLength - $length);
|
||||
$length += \strlen($canon);
|
||||
hash_update($hash, $canon);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$length += \strlen($canon);
|
||||
hash_update($hash, $canon);
|
||||
}
|
||||
|
||||
// Add trailing Line return if last line is non empty
|
||||
if ('' !== $currentLine) {
|
||||
hash_update($hash, "\r\n");
|
||||
$length += \strlen("\r\n");
|
||||
}
|
||||
|
||||
if (!$relaxed && 0 === $length) {
|
||||
hash_update($hash, "\r\n");
|
||||
$length = 2;
|
||||
}
|
||||
|
||||
return [hash_final($hash, true), $length];
|
||||
}
|
||||
}
|
111
vendor/symfony/mime/Crypto/SMime.php
vendored
Normal file
111
vendor/symfony/mime/Crypto/SMime.php
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
<?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\Exception\RuntimeException;
|
||||
use Symfony\Component\Mime\Part\SMimePart;
|
||||
|
||||
/**
|
||||
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class SMime
|
||||
{
|
||||
protected function normalizeFilePath(string $path): string
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
throw new RuntimeException(sprintf('File does not exist: "%s".', $path));
|
||||
}
|
||||
|
||||
return 'file://'.str_replace('\\', '/', realpath($path));
|
||||
}
|
||||
|
||||
protected function iteratorToFile(iterable $iterator, $stream): void
|
||||
{
|
||||
foreach ($iterator as $chunk) {
|
||||
fwrite($stream, $chunk);
|
||||
}
|
||||
}
|
||||
|
||||
protected function convertMessageToSMimePart($stream, string $type, string $subtype): SMimePart
|
||||
{
|
||||
rewind($stream);
|
||||
|
||||
$headers = '';
|
||||
|
||||
while (!feof($stream)) {
|
||||
$buffer = fread($stream, 78);
|
||||
$headers .= $buffer;
|
||||
|
||||
// Detect ending of header list
|
||||
if (preg_match('/(\r\n\r\n|\n\n)/', $headers, $match)) {
|
||||
$headersPosEnd = strpos($headers, $headerBodySeparator = $match[0]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$headers = $this->getMessageHeaders(trim(substr($headers, 0, $headersPosEnd)));
|
||||
|
||||
fseek($stream, $headersPosEnd + \strlen($headerBodySeparator));
|
||||
|
||||
return new SMimePart($this->getStreamIterator($stream), $type, $subtype, $this->getParametersFromHeader($headers['content-type']));
|
||||
}
|
||||
|
||||
protected function getStreamIterator($stream): iterable
|
||||
{
|
||||
while (!feof($stream)) {
|
||||
yield str_replace("\n", "\r\n", str_replace("\r\n", "\n", fread($stream, 16372)));
|
||||
}
|
||||
}
|
||||
|
||||
private function getMessageHeaders(string $headerData): array
|
||||
{
|
||||
$headers = [];
|
||||
$headerLines = explode("\r\n", str_replace("\n", "\r\n", str_replace("\r\n", "\n", $headerData)));
|
||||
$currentHeaderName = '';
|
||||
|
||||
// Transform header lines into an associative array
|
||||
foreach ($headerLines as $headerLine) {
|
||||
// Empty lines between headers indicate a new mime-entity
|
||||
if ('' === $headerLine) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle headers that span multiple lines
|
||||
if (!str_contains($headerLine, ':')) {
|
||||
$headers[$currentHeaderName] .= ' '.trim($headerLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
$header = explode(':', $headerLine, 2);
|
||||
$currentHeaderName = strtolower($header[0]);
|
||||
$headers[$currentHeaderName] = trim($header[1]);
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function getParametersFromHeader(string $header): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
preg_match_all('/(?P<name>[a-z-0-9]+)=(?P<value>"[^"]+"|(?:[^\s;]+|$))(?:\s+;)?/i', $header, $matches);
|
||||
|
||||
foreach ($matches['value'] as $pos => $paramValue) {
|
||||
$params[$matches['name'][$pos]] = trim($paramValue, '"');
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
63
vendor/symfony/mime/Crypto/SMimeEncrypter.php
vendored
Normal file
63
vendor/symfony/mime/Crypto/SMimeEncrypter.php
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
<?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\Exception\RuntimeException;
|
||||
use Symfony\Component\Mime\Message;
|
||||
|
||||
/**
|
||||
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
|
||||
*/
|
||||
final class SMimeEncrypter extends SMime
|
||||
{
|
||||
private string|array $certs;
|
||||
private int $cipher;
|
||||
|
||||
/**
|
||||
* @param string|string[] $certificate The path (or array of paths) of the file(s) containing the X.509 certificate(s)
|
||||
* @param int|null $cipher A set of algorithms used to encrypt the message. Must be one of these PHP constants: https://www.php.net/manual/en/openssl.ciphers.php
|
||||
*/
|
||||
public function __construct(string|array $certificate, ?int $cipher = null)
|
||||
{
|
||||
if (!\extension_loaded('openssl')) {
|
||||
throw new \LogicException('PHP extension "openssl" is required to use SMime.');
|
||||
}
|
||||
|
||||
if (\is_array($certificate)) {
|
||||
$this->certs = array_map($this->normalizeFilePath(...), $certificate);
|
||||
} else {
|
||||
$this->certs = $this->normalizeFilePath($certificate);
|
||||
}
|
||||
|
||||
$this->cipher = $cipher ?? \OPENSSL_CIPHER_AES_256_CBC;
|
||||
}
|
||||
|
||||
public function encrypt(Message $message): Message
|
||||
{
|
||||
$bufferFile = tmpfile();
|
||||
$outputFile = tmpfile();
|
||||
|
||||
$this->iteratorToFile($message->toIterable(), $bufferFile);
|
||||
|
||||
if (!@openssl_pkcs7_encrypt(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->certs, [], 0, $this->cipher)) {
|
||||
throw new RuntimeException(sprintf('Failed to encrypt S/Mime message. Error: "%s".', openssl_error_string()));
|
||||
}
|
||||
|
||||
$mimePart = $this->convertMessageToSMimePart($outputFile, 'application', 'pkcs7-mime');
|
||||
$mimePart->getHeaders()
|
||||
->addTextHeader('Content-Transfer-Encoding', 'base64')
|
||||
->addParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'smime.p7m'])
|
||||
;
|
||||
|
||||
return new Message($message->getHeaders(), $mimePart);
|
||||
}
|
||||
}
|
65
vendor/symfony/mime/Crypto/SMimeSigner.php
vendored
Normal file
65
vendor/symfony/mime/Crypto/SMimeSigner.php
vendored
Normal file
@ -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\Crypto;
|
||||
|
||||
use Symfony\Component\Mime\Exception\RuntimeException;
|
||||
use Symfony\Component\Mime\Message;
|
||||
|
||||
/**
|
||||
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
|
||||
*/
|
||||
final class SMimeSigner extends SMime
|
||||
{
|
||||
private string $signCertificate;
|
||||
private string|array $signPrivateKey;
|
||||
private int $signOptions;
|
||||
private ?string $extraCerts;
|
||||
|
||||
/**
|
||||
* @param string $certificate The path of the file containing the signing certificate (in PEM format)
|
||||
* @param string $privateKey The path of the file containing the private key (in PEM format)
|
||||
* @param string|null $privateKeyPassphrase A passphrase of the private key (if any)
|
||||
* @param string|null $extraCerts The path of the file containing intermediate certificates (in PEM format) needed by the signing certificate
|
||||
* @param int|null $signOptions Bitwise operator options for openssl_pkcs7_sign() (@see https://secure.php.net/manual/en/openssl.pkcs7.flags.php)
|
||||
*/
|
||||
public function __construct(string $certificate, string $privateKey, ?string $privateKeyPassphrase = null, ?string $extraCerts = null, ?int $signOptions = null)
|
||||
{
|
||||
if (!\extension_loaded('openssl')) {
|
||||
throw new \LogicException('PHP extension "openssl" is required to use SMime.');
|
||||
}
|
||||
|
||||
$this->signCertificate = $this->normalizeFilePath($certificate);
|
||||
|
||||
if (null !== $privateKeyPassphrase) {
|
||||
$this->signPrivateKey = [$this->normalizeFilePath($privateKey), $privateKeyPassphrase];
|
||||
} else {
|
||||
$this->signPrivateKey = $this->normalizeFilePath($privateKey);
|
||||
}
|
||||
|
||||
$this->signOptions = $signOptions ?? \PKCS7_DETACHED;
|
||||
$this->extraCerts = $extraCerts ? realpath($extraCerts) : null;
|
||||
}
|
||||
|
||||
public function sign(Message $message): Message
|
||||
{
|
||||
$bufferFile = tmpfile();
|
||||
$outputFile = tmpfile();
|
||||
|
||||
$this->iteratorToFile($message->getBody()->toIterable(), $bufferFile);
|
||||
|
||||
if (!@openssl_pkcs7_sign(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->signCertificate, $this->signPrivateKey, [], $this->signOptions, $this->extraCerts)) {
|
||||
throw new RuntimeException(sprintf('Failed to sign S/Mime message. Error: "%s".', openssl_error_string()));
|
||||
}
|
||||
|
||||
return new Message($message->getHeaders(), $this->convertMessageToSMimePart($outputFile, 'multipart', 'signed'));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user