SMimeSigner.php 14 KB

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