PageRenderTime 26ms CodeModel.GetById 9ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 0ms

/nova/modules/swiftmailer/classes/Swift/Signers/SMimeSigner.php

https://github.com/anodyne/nova
PHP | 437 lines | 237 code | 66 blank | 134 comment | 29 complexity | 093284dea7ab659f78ce0caacf18fff1 MD5 | raw file
  1<?php
  2
  3/*
  4 * This file is part of SwiftMailer.
  5 * (c) 2004-2009 Chris Corbyn
  6 *
  7 * For the full copyright and license information, please view the LICENSE
  8 * file that was distributed with this source code.
  9 */
 10
 11/**
 12 * MIME Message Signer used to apply S/MIME Signature/Encryption to a message.
 13 *
 14 *
 15 * @author Romain-Geissler
 16 * @author Sebastiaan Stok <s.stok@rollerscapes.net>
 17 */
 18class Swift_Signers_SMimeSigner implements Swift_Signers_BodySigner
 19{
 20    protected $signCertificate;
 21    protected $signPrivateKey;
 22    protected $encryptCert;
 23    protected $signThenEncrypt = true;
 24    protected $signLevel;
 25    protected $encryptLevel;
 26    protected $signOptions;
 27    protected $encryptOptions;
 28    protected $encryptCipher;
 29    protected $extraCerts = null;
 30
 31    /**
 32     * @var Swift_StreamFilters_StringReplacementFilterFactory
 33     */
 34    protected $replacementFactory;
 35
 36    /**
 37     * @var Swift_Mime_HeaderFactory
 38     */
 39    protected $headerFactory;
 40
 41    /**
 42     * Constructor.
 43     *
 44     * @param string $certificate
 45     * @param string $privateKey
 46     * @param string $encryptCertificate
 47     */
 48    public function __construct($signCertificate = null, $signPrivateKey = null, $encryptCertificate = null)
 49    {
 50        if (null !== $signPrivateKey) {
 51            $this->setSignCertificate($signCertificate, $signPrivateKey);
 52        }
 53
 54        if (null !== $encryptCertificate) {
 55            $this->setEncryptCertificate($encryptCertificate);
 56        }
 57
 58        $this->replacementFactory = Swift_DependencyContainer::getInstance()
 59            ->lookup('transport.replacementfactory');
 60
 61        $this->signOptions = PKCS7_DETACHED;
 62
 63        // Supported since php5.4
 64        if (defined('OPENSSL_CIPHER_AES_128_CBC')) {
 65            $this->encryptCipher = OPENSSL_CIPHER_AES_128_CBC;
 66        } else {
 67            $this->encryptCipher = OPENSSL_CIPHER_RC2_128;
 68        }
 69    }
 70
 71    /**
 72     * Returns an new Swift_Signers_SMimeSigner instance.
 73     *
 74     * @param string $certificate
 75     * @param string $privateKey
 76     *
 77     * @return Swift_Signers_SMimeSigner
 78     */
 79    public static function newInstance($certificate = null, $privateKey = null)
 80    {
 81        return new self($certificate, $privateKey);
 82    }
 83
 84    /**
 85     * Set the certificate location to use for signing.
 86     *
 87     * @link http://www.php.net/manual/en/openssl.pkcs7.flags.php
 88     *
 89     * @param string       $certificate
 90     * @param string|array $privateKey  If the key needs an passphrase use array('file-location', 'passphrase') instead
 91     * @param int          $signOptions Bitwise operator options for openssl_pkcs7_sign()
 92     * @param string       $extraCerts  A file containing intermediate certificates needed by the signing certificate
 93     *
 94     * @return Swift_Signers_SMimeSigner
 95     */
 96    public function setSignCertificate($certificate, $privateKey = null, $signOptions = PKCS7_DETACHED, $extraCerts = null)
 97    {
 98        $this->signCertificate = 'file://'.str_replace('\\', '/', realpath($certificate));
 99
100        if (null !== $privateKey) {
101            if (is_array($privateKey)) {
102                $this->signPrivateKey = $privateKey;
103                $this->signPrivateKey[0] = 'file://'.str_replace('\\', '/', realpath($privateKey[0]));
104            } else {
105                $this->signPrivateKey = 'file://'.str_replace('\\', '/', realpath($privateKey));
106            }
107        }
108
109        $this->signOptions = $signOptions;
110        if (null !== $extraCerts) {
111            $this->extraCerts = str_replace('\\', '/', realpath($extraCerts));
112        }
113
114        return $this;
115    }
116
117    /**
118     * Set the certificate location to use for encryption.
119     *
120     * @link http://www.php.net/manual/en/openssl.pkcs7.flags.php
121     * @link http://nl3.php.net/manual/en/openssl.ciphers.php
122     *
123     * @param string|array $recipientCerts Either an single X.509 certificate, or an assoc array of X.509 certificates.
124     * @param int          $cipher
125     *
126     * @return Swift_Signers_SMimeSigner
127     */
128    public function setEncryptCertificate($recipientCerts, $cipher = null)
129    {
130        if (is_array($recipientCerts)) {
131            $this->encryptCert = array();
132
133            foreach ($recipientCerts as $cert) {
134                $this->encryptCert[] = 'file://'.str_replace('\\', '/', realpath($cert));
135            }
136        } else {
137            $this->encryptCert = 'file://'.str_replace('\\', '/', realpath($recipientCerts));
138        }
139
140        if (null !== $cipher) {
141            $this->encryptCipher = $cipher;
142        }
143
144        return $this;
145    }
146
147    /**
148     * @return string
149     */
150    public function getSignCertificate()
151    {
152        return $this->signCertificate;
153    }
154
155    /**
156     * @return string
157     */
158    public function getSignPrivateKey()
159    {
160        return $this->signPrivateKey;
161    }
162
163    /**
164     * Set perform signing before encryption.
165     *
166     * The default is to first sign the message and then encrypt.
167     * But some older mail clients, namely Microsoft Outlook 2000 will work when the message first encrypted.
168     * As this goes against the official specs, its recommended to only use 'encryption -> signing' when specifically targeting these 'broken' clients.
169     *
170     * @param string $signThenEncrypt
171     *
172     * @return Swift_Signers_SMimeSigner
173     */
174    public function setSignThenEncrypt($signThenEncrypt = true)
175    {
176        $this->signThenEncrypt = $signThenEncrypt;
177
178        return $this;
179    }
180
181    /**
182     * @return bool
183     */
184    public function isSignThenEncrypt()
185    {
186        return $this->signThenEncrypt;
187    }
188
189    /**
190     * Resets internal states.
191     *
192     * @return Swift_Signers_SMimeSigner
193     */
194    public function reset()
195    {
196        return $this;
197    }
198
199    /**
200     * Change the Swift_Message to apply the signing.
201     *
202     * @param Swift_Message $message
203     *
204     * @return Swift_Signers_SMimeSigner
205     */
206    public function signMessage(Swift_Message $message)
207    {
208        if (null === $this->signCertificate && null === $this->encryptCert) {
209            return $this;
210        }
211
212        // Store the message using ByteStream to a file{1}
213        // Remove all Children
214        // Sign file{1}, parse the new MIME headers and set them on the primary MimeEntity
215        // Set the singed-body as the new body (without boundary)
216
217        $messageStream = new Swift_ByteStream_TemporaryFileByteStream();
218        $this->toSMimeByteStream($messageStream, $message);
219        $message->setEncoder(Swift_DependencyContainer::getInstance()->lookup('mime.rawcontentencoder'));
220
221        $message->setChildren(array());
222        $this->streamToMime($messageStream, $message);
223    }
224
225    /**
226     * Return the list of header a signer might tamper.
227     *
228     * @return array
229     */
230    public function getAlteredHeaders()
231    {
232        return array('Content-Type', 'Content-Transfer-Encoding', 'Content-Disposition');
233    }
234
235    /**
236     * @param Swift_InputByteStream $inputStream
237     * @param Swift_Message         $mimeEntity
238     */
239    protected function toSMimeByteStream(Swift_InputByteStream $inputStream, Swift_Message $message)
240    {
241        $mimeEntity = $this->createMessage($message);
242        $messageStream = new Swift_ByteStream_TemporaryFileByteStream();
243
244        $mimeEntity->toByteStream($messageStream);
245        $messageStream->commit();
246
247        if (null !== $this->signCertificate && null !== $this->encryptCert) {
248            $temporaryStream = new Swift_ByteStream_TemporaryFileByteStream();
249
250            if ($this->signThenEncrypt) {
251                $this->messageStreamToSignedByteStream($messageStream, $temporaryStream);
252                $this->messageStreamToEncryptedByteStream($temporaryStream, $inputStream);
253            } else {
254                $this->messageStreamToEncryptedByteStream($messageStream, $temporaryStream);
255                $this->messageStreamToSignedByteStream($temporaryStream, $inputStream);
256            }
257        } elseif ($this->signCertificate !== null) {
258            $this->messageStreamToSignedByteStream($messageStream, $inputStream);
259        } else {
260            $this->messageStreamToEncryptedByteStream($messageStream, $inputStream);
261        }
262    }
263
264    /**
265     * @param Swift_Message $message
266     *
267     * @return Swift_Message
268     */
269    protected function createMessage(Swift_Message $message)
270    {
271        $mimeEntity = new Swift_Message('', $message->getBody(), $message->getContentType(), $message->getCharset());
272        $mimeEntity->setChildren($message->getChildren());
273
274        $messageHeaders = $mimeEntity->getHeaders();
275        $messageHeaders->remove('Message-ID');
276        $messageHeaders->remove('Date');
277        $messageHeaders->remove('Subject');
278        $messageHeaders->remove('MIME-Version');
279        $messageHeaders->remove('To');
280        $messageHeaders->remove('From');
281
282        return $mimeEntity;
283    }
284
285    /**
286     * @param Swift_FileStream      $outputStream
287     * @param Swift_InputByteStream $inputStream
288     *
289     * @throws Swift_IoException
290     */
291    protected function messageStreamToSignedByteStream(Swift_FileStream $outputStream, Swift_InputByteStream $inputStream)
292    {
293        $signedMessageStream = new Swift_ByteStream_TemporaryFileByteStream();
294
295        $args = array($outputStream->getPath(), $signedMessageStream->getPath(), $this->signCertificate, $this->signPrivateKey, array(), $this->signOptions);
296        if (null !== $this->extraCerts) {
297            $args[] = $this->extraCerts;
298        }
299
300        if (!call_user_func_array('openssl_pkcs7_sign', $args)) {
301            throw new Swift_IoException(sprintf('Failed to sign S/Mime message. Error: "%s".', openssl_error_string()));
302        }
303
304        $this->copyFromOpenSSLOutput($signedMessageStream, $inputStream);
305    }
306
307    /**
308     * @param Swift_FileStream      $outputStream
309     * @param Swift_InputByteStream $is
310     *
311     * @throws Swift_IoException
312     */
313    protected function messageStreamToEncryptedByteStream(Swift_FileStream $outputStream, Swift_InputByteStream $is)
314    {
315        $encryptedMessageStream = new Swift_ByteStream_TemporaryFileByteStream();
316
317        if (!openssl_pkcs7_encrypt($outputStream->getPath(), $encryptedMessageStream->getPath(), $this->encryptCert, array(), 0, $this->encryptCipher)) {
318            throw new Swift_IoException(sprintf('Failed to encrypt S/Mime message. Error: "%s".', openssl_error_string()));
319        }
320
321        $this->copyFromOpenSSLOutput($encryptedMessageStream, $is);
322    }
323
324    /**
325     * @param Swift_OutputByteStream $fromStream
326     * @param Swift_InputByteStream  $toStream
327     */
328    protected function copyFromOpenSSLOutput(Swift_OutputByteStream $fromStream, Swift_InputByteStream $toStream)
329    {
330        $bufferLength = 4096;
331        $filteredStream = new Swift_ByteStream_TemporaryFileByteStream();
332        $filteredStream->addFilter($this->replacementFactory->createFilter("\r\n", "\n"), 'CRLF to LF');
333        $filteredStream->addFilter($this->replacementFactory->createFilter("\n", "\r\n"), 'LF to CRLF');
334
335        while (false !== ($buffer = $fromStream->read($bufferLength))) {
336            $filteredStream->write($buffer);
337        }
338
339        $filteredStream->flushBuffers();
340
341        while (false !== ($buffer = $filteredStream->read($bufferLength))) {
342            $toStream->write($buffer);
343        }
344
345        $toStream->commit();
346    }
347
348    /**
349     * Merges an OutputByteStream to Swift_Message.
350     *
351     * @param Swift_OutputByteStream $fromStream
352     * @param Swift_Message          $message
353     */
354    protected function streamToMime(Swift_OutputByteStream $fromStream, Swift_Message $message)
355    {
356        $bufferLength = 78;
357        $headerData = '';
358
359        $fromStream->setReadPointer(0);
360
361        while (($buffer = $fromStream->read($bufferLength)) !== false) {
362            $headerData .= $buffer;
363
364            if (false !== strpos($buffer, "\r\n\r\n")) {
365                break;
366            }
367        }
368
369        $headersPosEnd = strpos($headerData, "\r\n\r\n");
370        $headerData = trim($headerData);
371        $headerData = substr($headerData, 0, $headersPosEnd);
372        $headerLines = explode("\r\n", $headerData);
373        unset($headerData);
374
375        $headers = array();
376        $currentHeaderName = '';
377
378        foreach ($headerLines as $headerLine) {
379            // Line separated
380            if (ctype_space($headerLines[0]) || false === strpos($headerLine, ':')) {
381                $headers[$currentHeaderName] .= ' '.trim($headerLine);
382                continue;
383            }
384
385            $header = explode(':', $headerLine, 2);
386            $currentHeaderName = strtolower($header[0]);
387            $headers[$currentHeaderName] = trim($header[1]);
388        }
389
390        $messageStream = new Swift_ByteStream_TemporaryFileByteStream();
391        $messageStream->addFilter($this->replacementFactory->createFilter("\r\n", "\n"), 'CRLF to LF');
392        $messageStream->addFilter($this->replacementFactory->createFilter("\n", "\r\n"), 'LF to CRLF');
393
394        $messageHeaders = $message->getHeaders();
395
396        // No need to check for 'application/pkcs7-mime', as this is always base64
397        if ('multipart/signed;' === substr($headers['content-type'], 0, 17)) {
398            if (!preg_match('/boundary=("[^"]+"|(?:[^\s]+|$))/is', $headers['content-type'], $contentTypeData)) {
399                throw new Swift_SwiftException('Failed to find Boundary parameter');
400            }
401
402            $boundary = trim($contentTypeData['1'], '"');
403            $boundaryLen = strlen($boundary);
404
405            // Skip the header and CRLF CRLF
406            $fromStream->setReadPointer($headersPosEnd + 4);
407
408            while (false !== ($buffer = $fromStream->read($bufferLength))) {
409                $messageStream->write($buffer);
410            }
411
412            $messageStream->commit();
413
414            $messageHeaders->remove('Content-Transfer-Encoding');
415            $message->setContentType($headers['content-type']);
416            $message->setBoundary($boundary);
417            $message->setBody($messageStream);
418        } else {
419            $fromStream->setReadPointer($headersPosEnd + 4);
420
421            if (null === $this->headerFactory) {
422                $this->headerFactory = Swift_DependencyContainer::getInstance()->lookup('mime.headerfactory');
423            }
424
425            $message->setContentType($headers['content-type']);
426            $messageHeaders->set($this->headerFactory->createTextHeader('Content-Transfer-Encoding', $headers['content-transfer-encoding']));
427            $messageHeaders->set($this->headerFactory->createTextHeader('Content-Disposition', $headers['content-disposition']));
428
429            while (false !== ($buffer = $fromStream->read($bufferLength))) {
430                $messageStream->write($buffer);
431            }
432
433            $messageStream->commit();
434            $message->setBody($messageStream);
435        }
436    }
437}