1<?php 2/** 3 * Copyright 2002-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file COPYING for license information (GPL). If you 6 * did not receive this file, see http://www.horde.org/licenses/gpl. 7 * 8 * @category Horde 9 * @copyright 2002-2017 Horde LLC 10 * @license http://www.horde.org/licenses/gpl GPL 11 * @package IMP 12 */ 13 14/** 15 * Renderer to allow viewing/decrypting of PGP formatted messages (RFC 3156). 16 * 17 * This class handles the following MIME types: 18 * - application/pgp-encrypted (in multipart/encrypted part) 19 * - application/pgp-keys 20 * - application/pgp-signature (in multipart/signed part) 21 * 22 * This driver may add the following parameters to the URL: 23 * - pgp_verify_msg: (boolean) Do verification of PGP signed data? 24 * - pgp_view_key: (boolean) View PGP key details? 25 * 26 * @author Michael Slusarz <slusarz@horde.org> 27 * @category Horde 28 * @copyright 2002-2017 Horde LLC 29 * @license http://www.horde.org/licenses/gpl GPL 30 * @package IMP 31 */ 32class IMP_Mime_Viewer_Pgp extends Horde_Mime_Viewer_Base 33{ 34 /* Metadata constants. */ 35 const PGP_SIGN_ENC = 'imp-pgp-signed-encrypted'; 36 37 /** 38 * This driver's display capabilities. 39 * 40 * @var array 41 */ 42 protected $_capability = array( 43 'full' => false, 44 'info' => false, 45 'inline' => true, 46 /* This driver *does* render raw data, but only for 47 * application/pgp-signature parts that have been processed by the 48 * text/plain driver and for displaying raw pgp keys. Altering this 49 * value is handled via the canRender() function. */ 50 'raw' => false 51 ); 52 53 /** 54 * Metadata for the current viewer/data. 55 * 56 * @var array 57 */ 58 protected $_metadata = array( 59 'compressed' => false, 60 'embedded' => true, 61 'forceinline' => true 62 ); 63 64 /** 65 * The address of the sender. 66 * 67 * @var Horde_Mail_Rfc822_Address 68 */ 69 protected $_sender = null; 70 71 /** 72 * Return the full rendered version of the Horde_Mime_Part object. 73 * 74 * @return array See parent::render(). 75 */ 76 protected function _render() 77 { 78 switch ($this->_mimepart->getType()) { 79 case 'application/pgp-keys': 80 $vars = $GLOBALS['injector']->getInstance('Horde_Variables'); 81 if ($vars->pgp_view_key) { 82 // Throws exception on error. 83 return array( 84 $this->_mimepart->getMimeId() => array( 85 'data' => '<html><body><tt>' . nl2br(str_replace(' ', ' ', $GLOBALS['injector']->getInstance('IMP_Crypt_Pgp')->pgpPrettyKey($this->_mimepart->getContents()))) . '</tt></body></html>', 86 'type' => 'text/html; charset=' . $this->getConfigParam('charset') 87 ) 88 ); 89 } 90 91 return array( 92 $this->_mimepart->getMimeId() => array( 93 'data' => $this->_mimepart->getContents(), 94 'type' => 'text/plain; charset=' . $this->_mimepart->getCharset() 95 ) 96 ); 97 } 98 } 99 100 /** 101 * Return the full rendered version of the Horde_Mime_Part object. 102 * 103 * @return array See parent::render(). 104 */ 105 protected function _renderRaw() 106 { 107 $ret = array( 108 'data' => '', 109 'type' => 'text/plain; charset=' . $this->getConfigParam('charset') 110 ); 111 112 switch ($this->_mimepart->getType()) { 113 case 'application/pgp-signature': 114 $parts = $GLOBALS['injector']->getInstance('IMP_Crypt_Pgp_Parse')->parse($this->_mimepart->getContents()); 115 foreach (array_keys($parts) as $key) { 116 if ($parts[$key]['type'] == Horde_Crypt_Pgp::ARMOR_SIGNATURE) { 117 $ret['data'] = implode("\r\n", $parts[$key]['data']); 118 break; 119 } 120 } 121 break; 122 } 123 124 return array( 125 $this->_mimepart->getMimeId() => $ret 126 ); 127 } 128 129 /** 130 * Return the rendered inline version of the Horde_Mime_Part object. 131 * 132 * @return array See parent::render(). 133 */ 134 protected function _renderInline() 135 { 136 $id = $this->_mimepart->getMimeId(); 137 138 switch ($this->_mimepart->getType()) { 139 case 'application/pgp-keys': 140 return $this->_outputPGPKey(); 141 142 case 'multipart/signed': 143 return $this->_outputPGPSigned(); 144 145 case 'multipart/encrypted': 146 $cache = $this->getConfigParam('imp_contents')->getViewCache(); 147 148 if (isset($cache->pgp[$id])) { 149 return array_merge(array( 150 $id => array( 151 'data' => null, 152 'status' => $cache->pgp[$id]['status'], 153 'type' => 'text/plain; charset=' . $this->getConfigParam('charset'), 154 'wrap' => $cache->pgp[$id]['wrap'] 155 ) 156 ), $cache->pgp[$id]['other']); 157 } 158 // Fall-through 159 160 case 'application/pgp-encrypted': 161 case 'application/pgp-signature': 162 default: 163 return array(); 164 } 165 } 166 167 /** 168 * If this MIME part can contain embedded MIME part(s), and those part(s) 169 * exist, return a representation of that data. 170 * 171 * @return mixed A Horde_Mime_Part object representing the embedded data. 172 * Returns null if no embedded MIME part(s) exist. 173 */ 174 protected function _getEmbeddedMimeParts() 175 { 176 if ($this->_mimepart->getType() != 'multipart/encrypted') { 177 return null; 178 } 179 180 $partlist = array_keys($this->_mimepart->contentTypeMap()); 181 $base_id = reset($partlist); 182 $version_id = next($partlist); 183 $data_id = Horde_Mime::mimeIdArithmetic($version_id, 'next'); 184 185 $status = new IMP_Mime_Status(); 186 $status->icon('mime/encryption.png', 'PGP'); 187 188 $cache = $this->getConfigParam('imp_contents')->getViewCache(); 189 $cache->pgp[$base_id] = array( 190 'status' => array($status), 191 'other' => array( 192 $version_id => null, 193 $data_id => null 194 ), 195 'wrap' => '' 196 ); 197 198 /* Is PGP active? */ 199 if (empty($GLOBALS['conf']['gnupg']['path']) || 200 !$GLOBALS['prefs']->getValue('use_pgp')) { 201 $status->addText(_("The data in this part has been encrypted via PGP, however, PGP support is disabled so the message cannot be decrypted.")); 202 return null; 203 } 204 205 /* PGP version information appears in the first MIME subpart. We 206 * don't currently need to do anything with this information. The 207 * encrypted data appears in the second MIME subpart. */ 208 $encrypted_part = $this->getConfigParam('imp_contents')->getMIMEPart($data_id); 209 $encrypted_data = $encrypted_part->getContents(); 210 211 $symmetric_pass = $personal_pass = null; 212 213 /* Check if this a symmetrically encrypted message. */ 214 try { 215 $imp_pgp = $GLOBALS['injector']->getInstance('IMP_Crypt_Pgp'); 216 $symmetric = $imp_pgp->encryptedSymmetrically($encrypted_data); 217 if ($symmetric) { 218 $symmetric_id = $this->_getSymmetricID(); 219 $symmetric_pass = $imp_pgp->getPassphrase('symmetric', $symmetric_id); 220 221 if (is_null($symmetric_pass)) { 222 $status->addText(_("The data in this part has been encrypted via PGP.")); 223 224 /* Ask for the correct passphrase if this is encrypted 225 * symmetrically. */ 226 $imple = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Imple')->create('IMP_Ajax_Imple_PassphraseDialog', array( 227 'params' => array( 228 'symmetricid' => $symmetric_id 229 ), 230 'type' => 'pgpSymmetric' 231 )); 232 $status->addText(Horde::link('#', '', '', '', '', '', '', array('id' => $imple->getDomId())) . _("You must enter the passphrase used to encrypt this message to view it.") . '</a>'); 233 return null; 234 } 235 } 236 } catch (Horde_Exception $e) { 237 Horde::log($e, 'INFO'); 238 return null; 239 } 240 241 /* Check if this is a literal compressed message. */ 242 try { 243 $info = $imp_pgp->pgpPacketInformation($encrypted_data); 244 } catch (Horde_Exception $e) { 245 Horde::log($e, 'INFO'); 246 return null; 247 } 248 249 $literal = !empty($info['literal']); 250 if ($literal) { 251 $status->addText(_("The data in this part has been compressed via PGP.")); 252 } else { 253 $status->addText(_("The data in this part has been encrypted via PGP.")); 254 255 if (!$symmetric) { 256 if ($imp_pgp->getPersonalPrivateKey()) { 257 $personal_pass = $imp_pgp->getPassphrase('personal'); 258 if (is_null($personal_pass)) { 259 /* Ask for the private key's passphrase if this is 260 * encrypted asymmetrically. */ 261 $imple = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Imple')->create('IMP_Ajax_Imple_PassphraseDialog', array( 262 'type' => 'pgpPersonal' 263 )); 264 $status->addText(Horde::link('#', '', '', '', '', '', '', array('id' => $imple->getDomId())) . _("You must enter the passphrase for your PGP private key to view this message.") . '</a>'); 265 return null; 266 } 267 } else { 268 /* Output if there is no personal private key to decrypt 269 * with. */ 270 $status->addText(_("However, no personal private key exists so the message cannot be decrypted.")); 271 return null; 272 } 273 } 274 } 275 276 try { 277 if (!is_null($symmetric_pass)) { 278 $decrypted_data = $imp_pgp->decryptMessage($encrypted_data, 'symmetric', array( 279 'passphrase' => $symmetric_pass, 280 'sender' => $this->_getSender()->bare_address 281 )); 282 } elseif (!is_null($personal_pass)) { 283 $decrypted_data = $imp_pgp->decryptMessage($encrypted_data, 'personal', array( 284 'passphrase' => $personal_pass, 285 'sender' => $this->_getSender()->bare_address 286 )); 287 } else { 288 $decrypted_data = $imp_pgp->decryptMessage($encrypted_data, 'literal'); 289 } 290 } catch (Horde_Exception $e) { 291 $status->addText(_("The data in this part does not appear to be a valid PGP encrypted message. Error: ") . $e->getMessage()); 292 if (!is_null($symmetric_pass)) { 293 $imp_pgp->unsetPassphrase('symmetric', $this->_getSymmetricID()); 294 return $this->_getEmbeddedMimeParts(); 295 } 296 return null; 297 } 298 299 $cache->pgp[$base_id]['wrap'] = 'mimePartWrapValid'; 300 301 /* Check for combined encryption/signature data. */ 302 if ($decrypted_data->result) { 303 $sig_text = is_bool($decrypted_data->result) 304 ? _("The data in this part has been digitally signed via PGP.") 305 : $this->_textFilter($decrypted_data->result, 'text2html', array('parselevel' => Horde_Text_Filter_Text2html::NOHTML)); 306 307 $status2 = new IMP_Mime_Status($sig_text); 308 $status2->action(IMP_Mime_Status::SUCCESS); 309 310 $cache->pgp[$base_id]['status'][] = $status2; 311 } 312 313 /* Force armor data as text/plain data. */ 314 if ($this->_mimepart->getMetadata(Horde_Crypt_Pgp_Parse::PGP_ARMOR)) { 315 $decrypted_data->message = "Content-Type: text/plain\n\n" . 316 $decrypted_data->message; 317 } 318 319 $new_part = Horde_Mime_Part::parseMessage($decrypted_data->message, array( 320 'forcemime' => true 321 )); 322 323 if ($new_part->getType() == 'multipart/signed') { 324 $data = new Horde_Stream_Temp(); 325 try { 326 $data->add(Horde_Mime_Part::getRawPartText($decrypted_data->message, 'header', '1')); 327 $data->add("\n\n"); 328 $data->add(Horde_Mime_Part::getRawPartText($decrypted_data->message, 'body', '1')); 329 } catch (Horde_Mime_Exception $e) {} 330 331 $new_part->setMetadata(self::PGP_SIGN_ENC, $data->stream); 332 $new_part->setContents($decrypted_data->message, array( 333 'encoding' => 'binary' 334 )); 335 } 336 337 return $new_part; 338 } 339 340 /** 341 * Generates output for 'application/pgp-keys' MIME_Parts. 342 * 343 * @return string The HTML output. 344 */ 345 protected function _outputPGPKey() 346 { 347 /* Is PGP active? */ 348 if (empty($GLOBALS['conf']['gnupg']['path']) || 349 !$GLOBALS['prefs']->getValue('use_pgp')) { 350 return array(); 351 } 352 353 /* Initialize status message. */ 354 $status = new IMP_Mime_Status(_("A PGP Public Key is attached to the message.")); 355 $status->icon('mime/encryption.png', 'PGP'); 356 357 $imp_contents = $this->getConfigParam('imp_contents'); 358 $mime_id = $this->_mimepart->getMimeId(); 359 360 if ($GLOBALS['prefs']->getValue('use_pgp') && 361 $GLOBALS['prefs']->getValue('add_source') && 362 $GLOBALS['registry']->hasMethod('contacts/addField')) { 363 // TODO: Check for key existence. 364 $imple = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Imple')->create('IMP_Ajax_Imple_ImportEncryptKey', array( 365 'mime_id' => $mime_id, 366 'muid' => strval($imp_contents->getIndicesOb()), 367 'type' => 'pgp' 368 )); 369 $status->addText(Horde::link('#', '', '', '', '', '', '', array('id' => $imple->getDomId())) . _("Save the key to your address book.") . '</a>'); 370 } 371 $status->addText($imp_contents->linkViewJS($this->_mimepart, 'view_attach', _("View key details."), array('params' => array('mode' => IMP_Contents::RENDER_FULL, 'pgp_view_key' => 1)))); 372 373 return array( 374 $mime_id => array( 375 'data' => '', 376 'status' => $status, 377 'type' => 'text/html; charset=' . $this->getConfigParam('charset') 378 ) 379 ); 380 } 381 382 /** 383 * Generates HTML output for 'multipart/signed' MIME parts. 384 * 385 * @return string The HTML output. 386 */ 387 protected function _outputPGPSigned() 388 { 389 global $conf, $injector, $prefs, $registry, $session; 390 391 $partlist = array_keys($this->_mimepart->contentTypeMap()); 392 $base_id = reset($partlist); 393 $signed_id = next($partlist); 394 $sig_id = Horde_Mime::mimeIdArithmetic($signed_id, 'next'); 395 396 if (!$prefs->getValue('use_pgp') || empty($conf['gnupg']['path'])) { 397 return array( 398 $sig_id => null 399 ); 400 } 401 402 $status = new IMP_Mime_Status(); 403 $status->addText(_("The data in this part has been digitally signed via PGP.")); 404 $status->icon('mime/encryption.png', 'PGP'); 405 406 $ret = array( 407 $base_id => array( 408 'data' => '', 409 'nosummary' => true, 410 'status' => array($status), 411 'type' => 'text/html; charset=' . $this->getConfigParam('charset'), 412 'wrap' => 'mimePartWrap' 413 ), 414 $sig_id => null 415 ); 416 417 if ($prefs->getValue('pgp_verify') || 418 $injector->getInstance('Horde_Variables')->pgp_verify_msg) { 419 $imp_contents = $this->getConfigParam('imp_contents'); 420 $sig_part = $imp_contents->getMIMEPart($sig_id); 421 422 $status2 = new IMP_Mime_Status(); 423 424 if (!$sig_part) { 425 $status2->action(IMP_Mime_Status::ERROR); 426 $sig_text = _("This digitally signed message is broken."); 427 $ret[$base_id]['wrap'] = 'mimePartWrapInvalid'; 428 } else { 429 /* Close session, since this may be a long-running 430 * operation. */ 431 $session->close(); 432 433 try { 434 $imp_pgp = $injector->getInstance('IMP_Crypt_Pgp'); 435 if ($sig_raw = $sig_part->getMetadata(Horde_Crypt_Pgp_Parse::SIG_RAW)) { 436 $sig_result = $imp_pgp->verifySignature($sig_raw, $this->_getSender()->bare_address, null, $sig_part->getMetadata(Horde_Crypt_Pgp_Parse::SIG_CHARSET)); 437 } else { 438 $stream = $imp_contents->isEmbedded($signed_id) 439 ? $this->_mimepart->getMetadata(self::PGP_SIGN_ENC) 440 : $imp_contents->getBodyPart($signed_id, array('mimeheaders' => true, 'stream' => true))->data; 441 442 rewind($stream); 443 stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol'); 444 stream_filter_append($stream, 'horde_eol', STREAM_FILTER_READ, array( 445 'eol' => Horde_Mime_Part::RFC_EOL 446 )); 447 448 $sig_result = $imp_pgp->verifySignature(stream_get_contents($stream), $this->_getSender()->bare_address, $sig_part->getContents()); 449 } 450 451 $status2->action(IMP_Mime_Status::SUCCESS); 452 $sig_text = $sig_result->message; 453 $ret[$base_id]['wrap'] = 'mimePartWrapValid'; 454 } catch (Horde_Exception $e) { 455 $status2->action(IMP_Mime_Status::ERROR); 456 $sig_text = $e->getMessage(); 457 $ret[$base_id]['wrap'] = 'mimePartWrapInvalid'; 458 } 459 } 460 461 $status2->addText($this->_textFilter($sig_text, 'text2html', array( 462 'parselevel' => Horde_Text_Filter_Text2html::NOHTML 463 ))); 464 $ret[$base_id]['status'][] = $status2; 465 } else { 466 switch ($registry->getView()) { 467 case Horde_Registry::VIEW_BASIC: 468 $status->addText(Horde::link(Horde::selfUrlParams()->add(array('pgp_verify_msg' => 1))) . _("Click HERE to verify the message.") . '</a>'); 469 break; 470 471 case Horde_Registry::VIEW_DYNAMIC: 472 $status->addText(Horde::link('#', '', 'pgpVerifyMsg') . _("Click HERE to verify the message.") . '</a>'); 473 break; 474 } 475 } 476 477 return $ret; 478 } 479 480 /** 481 * Generates the symmetric ID for this message. 482 * 483 * @return string Symmetric ID. 484 */ 485 protected function _getSymmetricID() 486 { 487 return $GLOBALS['injector']->getInstance('IMP_Crypt_Pgp')->getSymmetricID($this->getConfigParam('imp_contents')->getMailbox(), $this->getConfigParam('imp_contents')->getUid(), $this->_mimepart->getMimeId()); 488 } 489 490 /** 491 * Determine the address of the sender. 492 * 493 * @return Horde_Mail_Rfc822_Address The from address. 494 */ 495 protected function _getSender() 496 { 497 if (is_null($this->_sender)) { 498 $headers = $this->getConfigParam('imp_contents')->getHeader(); 499 $tmp = $headers->getOb('from'); 500 $this->_sender = $tmp[0]; 501 } 502 503 return $this->_sender; 504 } 505 506 /** 507 * Can this driver render the data? 508 * 509 * @param string $mode See parent::canRender(). 510 * 511 * @return boolean See parent::canRender(). 512 */ 513 public function canRender($mode) 514 { 515 switch ($mode) { 516 case 'full': 517 if ($this->_mimepart->getType() == 'application/pgp-keys') { 518 return true; 519 } 520 break; 521 522 case 'raw': 523 if (($this->_mimepart->getType() == 'application/pgp-signature') && 524 $this->_mimepart->getMetadata(Horde_Crypt_Pgp_Parse::SIG_RAW)) { 525 return true; 526 } 527 break; 528 } 529 530 return parent::canRender($mode); 531 } 532 533} 534