534 lines
20 KiB
PHP
534 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2002-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (GPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/gpl.
|
|
*
|
|
* @category Horde
|
|
* @copyright 2002-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/gpl GPL
|
|
* @package IMP
|
|
*/
|
|
|
|
/**
|
|
* Renderer to allow viewing/decrypting of PGP formatted messages (RFC 3156).
|
|
*
|
|
* This class handles the following MIME types:
|
|
* - application/pgp-encrypted (in multipart/encrypted part)
|
|
* - application/pgp-keys
|
|
* - application/pgp-signature (in multipart/signed part)
|
|
*
|
|
* This driver may add the following parameters to the URL:
|
|
* - pgp_verify_msg: (boolean) Do verification of PGP signed data?
|
|
* - pgp_view_key: (boolean) View PGP key details?
|
|
*
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @copyright 2002-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/gpl GPL
|
|
* @package IMP
|
|
*/
|
|
class IMP_Mime_Viewer_Pgp extends Horde_Mime_Viewer_Base
|
|
{
|
|
/* Metadata constants. */
|
|
const PGP_SIGN_ENC = 'imp-pgp-signed-encrypted';
|
|
|
|
/**
|
|
* This driver's display capabilities.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_capability = array(
|
|
'full' => false,
|
|
'info' => false,
|
|
'inline' => true,
|
|
/* This driver *does* render raw data, but only for
|
|
* application/pgp-signature parts that have been processed by the
|
|
* text/plain driver and for displaying raw pgp keys. Altering this
|
|
* value is handled via the canRender() function. */
|
|
'raw' => false
|
|
);
|
|
|
|
/**
|
|
* Metadata for the current viewer/data.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_metadata = array(
|
|
'compressed' => false,
|
|
'embedded' => true,
|
|
'forceinline' => true
|
|
);
|
|
|
|
/**
|
|
* The address of the sender.
|
|
*
|
|
* @var Horde_Mail_Rfc822_Address
|
|
*/
|
|
protected $_sender = null;
|
|
|
|
/**
|
|
* Return the full rendered version of the Horde_Mime_Part object.
|
|
*
|
|
* @return array See parent::render().
|
|
*/
|
|
protected function _render()
|
|
{
|
|
switch ($this->_mimepart->getType()) {
|
|
case 'application/pgp-keys':
|
|
$vars = $GLOBALS['injector']->getInstance('Horde_Variables');
|
|
if ($vars->pgp_view_key) {
|
|
// Throws exception on error.
|
|
return array(
|
|
$this->_mimepart->getMimeId() => array(
|
|
'data' => '<html><body><tt>' . nl2br(str_replace(' ', ' ', $GLOBALS['injector']->getInstance('IMP_Crypt_Pgp')->pgpPrettyKey($this->_mimepart->getContents()))) . '</tt></body></html>',
|
|
'type' => 'text/html; charset=' . $this->getConfigParam('charset')
|
|
)
|
|
);
|
|
}
|
|
|
|
return array(
|
|
$this->_mimepart->getMimeId() => array(
|
|
'data' => $this->_mimepart->getContents(),
|
|
'type' => 'text/plain; charset=' . $this->_mimepart->getCharset()
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the full rendered version of the Horde_Mime_Part object.
|
|
*
|
|
* @return array See parent::render().
|
|
*/
|
|
protected function _renderRaw()
|
|
{
|
|
$ret = array(
|
|
'data' => '',
|
|
'type' => 'text/plain; charset=' . $this->getConfigParam('charset')
|
|
);
|
|
|
|
switch ($this->_mimepart->getType()) {
|
|
case 'application/pgp-signature':
|
|
$parts = $GLOBALS['injector']->getInstance('IMP_Crypt_Pgp_Parse')->parse($this->_mimepart->getContents());
|
|
foreach (array_keys($parts) as $key) {
|
|
if ($parts[$key]['type'] == Horde_Crypt_Pgp::ARMOR_SIGNATURE) {
|
|
$ret['data'] = implode("\r\n", $parts[$key]['data']);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return array(
|
|
$this->_mimepart->getMimeId() => $ret
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return the rendered inline version of the Horde_Mime_Part object.
|
|
*
|
|
* @return array See parent::render().
|
|
*/
|
|
protected function _renderInline()
|
|
{
|
|
$id = $this->_mimepart->getMimeId();
|
|
|
|
switch ($this->_mimepart->getType()) {
|
|
case 'application/pgp-keys':
|
|
return $this->_outputPGPKey();
|
|
|
|
case 'multipart/signed':
|
|
return $this->_outputPGPSigned();
|
|
|
|
case 'multipart/encrypted':
|
|
$cache = $this->getConfigParam('imp_contents')->getViewCache();
|
|
|
|
if (isset($cache->pgp[$id])) {
|
|
return array_merge(array(
|
|
$id => array(
|
|
'data' => null,
|
|
'status' => $cache->pgp[$id]['status'],
|
|
'type' => 'text/plain; charset=' . $this->getConfigParam('charset'),
|
|
'wrap' => $cache->pgp[$id]['wrap']
|
|
)
|
|
), $cache->pgp[$id]['other']);
|
|
}
|
|
// Fall-through
|
|
|
|
case 'application/pgp-encrypted':
|
|
case 'application/pgp-signature':
|
|
default:
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If this MIME part can contain embedded MIME part(s), and those part(s)
|
|
* exist, return a representation of that data.
|
|
*
|
|
* @return mixed A Horde_Mime_Part object representing the embedded data.
|
|
* Returns null if no embedded MIME part(s) exist.
|
|
*/
|
|
protected function _getEmbeddedMimeParts()
|
|
{
|
|
if ($this->_mimepart->getType() != 'multipart/encrypted') {
|
|
return null;
|
|
}
|
|
|
|
$partlist = array_keys($this->_mimepart->contentTypeMap());
|
|
$base_id = reset($partlist);
|
|
$version_id = next($partlist);
|
|
$data_id = Horde_Mime::mimeIdArithmetic($version_id, 'next');
|
|
|
|
$status = new IMP_Mime_Status();
|
|
$status->icon('mime/encryption.png', 'PGP');
|
|
|
|
$cache = $this->getConfigParam('imp_contents')->getViewCache();
|
|
$cache->pgp[$base_id] = array(
|
|
'status' => array($status),
|
|
'other' => array(
|
|
$version_id => null,
|
|
$data_id => null
|
|
),
|
|
'wrap' => ''
|
|
);
|
|
|
|
/* Is PGP active? */
|
|
if (empty($GLOBALS['conf']['gnupg']['path']) ||
|
|
!$GLOBALS['prefs']->getValue('use_pgp')) {
|
|
$status->addText(_("The data in this part has been encrypted via PGP, however, PGP support is disabled so the message cannot be decrypted."));
|
|
return null;
|
|
}
|
|
|
|
/* PGP version information appears in the first MIME subpart. We
|
|
* don't currently need to do anything with this information. The
|
|
* encrypted data appears in the second MIME subpart. */
|
|
$encrypted_part = $this->getConfigParam('imp_contents')->getMIMEPart($data_id);
|
|
$encrypted_data = $encrypted_part->getContents();
|
|
|
|
$symmetric_pass = $personal_pass = null;
|
|
|
|
/* Check if this a symmetrically encrypted message. */
|
|
try {
|
|
$imp_pgp = $GLOBALS['injector']->getInstance('IMP_Crypt_Pgp');
|
|
$symmetric = $imp_pgp->encryptedSymmetrically($encrypted_data);
|
|
if ($symmetric) {
|
|
$symmetric_id = $this->_getSymmetricID();
|
|
$symmetric_pass = $imp_pgp->getPassphrase('symmetric', $symmetric_id);
|
|
|
|
if (is_null($symmetric_pass)) {
|
|
$status->addText(_("The data in this part has been encrypted via PGP."));
|
|
|
|
/* Ask for the correct passphrase if this is encrypted
|
|
* symmetrically. */
|
|
$imple = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Imple')->create('IMP_Ajax_Imple_PassphraseDialog', array(
|
|
'params' => array(
|
|
'symmetricid' => $symmetric_id
|
|
),
|
|
'type' => 'pgpSymmetric'
|
|
));
|
|
$status->addText(Horde::link('#', '', '', '', '', '', '', array('id' => $imple->getDomId())) . _("You must enter the passphrase used to encrypt this message to view it.") . '</a>');
|
|
return null;
|
|
}
|
|
}
|
|
} catch (Horde_Exception $e) {
|
|
Horde::log($e, 'INFO');
|
|
return null;
|
|
}
|
|
|
|
/* Check if this is a literal compressed message. */
|
|
try {
|
|
$info = $imp_pgp->pgpPacketInformation($encrypted_data);
|
|
} catch (Horde_Exception $e) {
|
|
Horde::log($e, 'INFO');
|
|
return null;
|
|
}
|
|
|
|
$literal = !empty($info['literal']);
|
|
if ($literal) {
|
|
$status->addText(_("The data in this part has been compressed via PGP."));
|
|
} else {
|
|
$status->addText(_("The data in this part has been encrypted via PGP."));
|
|
|
|
if (!$symmetric) {
|
|
if ($imp_pgp->getPersonalPrivateKey()) {
|
|
$personal_pass = $imp_pgp->getPassphrase('personal');
|
|
if (is_null($personal_pass)) {
|
|
/* Ask for the private key's passphrase if this is
|
|
* encrypted asymmetrically. */
|
|
$imple = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Imple')->create('IMP_Ajax_Imple_PassphraseDialog', array(
|
|
'type' => 'pgpPersonal'
|
|
));
|
|
$status->addText(Horde::link('#', '', '', '', '', '', '', array('id' => $imple->getDomId())) . _("You must enter the passphrase for your PGP private key to view this message.") . '</a>');
|
|
return null;
|
|
}
|
|
} else {
|
|
/* Output if there is no personal private key to decrypt
|
|
* with. */
|
|
$status->addText(_("However, no personal private key exists so the message cannot be decrypted."));
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (!is_null($symmetric_pass)) {
|
|
$decrypted_data = $imp_pgp->decryptMessage($encrypted_data, 'symmetric', array(
|
|
'passphrase' => $symmetric_pass,
|
|
'sender' => $this->_getSender()->bare_address
|
|
));
|
|
} elseif (!is_null($personal_pass)) {
|
|
$decrypted_data = $imp_pgp->decryptMessage($encrypted_data, 'personal', array(
|
|
'passphrase' => $personal_pass,
|
|
'sender' => $this->_getSender()->bare_address
|
|
));
|
|
} else {
|
|
$decrypted_data = $imp_pgp->decryptMessage($encrypted_data, 'literal');
|
|
}
|
|
} catch (Horde_Exception $e) {
|
|
$status->addText(_("The data in this part does not appear to be a valid PGP encrypted message. Error: ") . $e->getMessage());
|
|
if (!is_null($symmetric_pass)) {
|
|
$imp_pgp->unsetPassphrase('symmetric', $this->_getSymmetricID());
|
|
return $this->_getEmbeddedMimeParts();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
$cache->pgp[$base_id]['wrap'] = 'mimePartWrapValid';
|
|
|
|
/* Check for combined encryption/signature data. */
|
|
if ($decrypted_data->result) {
|
|
$sig_text = is_bool($decrypted_data->result)
|
|
? _("The data in this part has been digitally signed via PGP.")
|
|
: $this->_textFilter($decrypted_data->result, 'text2html', array('parselevel' => Horde_Text_Filter_Text2html::NOHTML));
|
|
|
|
$status2 = new IMP_Mime_Status($sig_text);
|
|
$status2->action(IMP_Mime_Status::SUCCESS);
|
|
|
|
$cache->pgp[$base_id]['status'][] = $status2;
|
|
}
|
|
|
|
/* Force armor data as text/plain data. */
|
|
if ($this->_mimepart->getMetadata(Horde_Crypt_Pgp_Parse::PGP_ARMOR)) {
|
|
$decrypted_data->message = "Content-Type: text/plain\n\n" .
|
|
$decrypted_data->message;
|
|
}
|
|
|
|
$new_part = Horde_Mime_Part::parseMessage($decrypted_data->message, array(
|
|
'forcemime' => true
|
|
));
|
|
|
|
if ($new_part->getType() == 'multipart/signed') {
|
|
$data = new Horde_Stream_Temp();
|
|
try {
|
|
$data->add(Horde_Mime_Part::getRawPartText($decrypted_data->message, 'header', '1'));
|
|
$data->add("\n\n");
|
|
$data->add(Horde_Mime_Part::getRawPartText($decrypted_data->message, 'body', '1'));
|
|
} catch (Horde_Mime_Exception $e) {}
|
|
|
|
$new_part->setMetadata(self::PGP_SIGN_ENC, $data->stream);
|
|
$new_part->setContents($decrypted_data->message, array(
|
|
'encoding' => 'binary'
|
|
));
|
|
}
|
|
|
|
return $new_part;
|
|
}
|
|
|
|
/**
|
|
* Generates output for 'application/pgp-keys' MIME_Parts.
|
|
*
|
|
* @return string The HTML output.
|
|
*/
|
|
protected function _outputPGPKey()
|
|
{
|
|
/* Is PGP active? */
|
|
if (empty($GLOBALS['conf']['gnupg']['path']) ||
|
|
!$GLOBALS['prefs']->getValue('use_pgp')) {
|
|
return array();
|
|
}
|
|
|
|
/* Initialize status message. */
|
|
$status = new IMP_Mime_Status(_("A PGP Public Key is attached to the message."));
|
|
$status->icon('mime/encryption.png', 'PGP');
|
|
|
|
$imp_contents = $this->getConfigParam('imp_contents');
|
|
$mime_id = $this->_mimepart->getMimeId();
|
|
|
|
if ($GLOBALS['prefs']->getValue('use_pgp') &&
|
|
$GLOBALS['prefs']->getValue('add_source') &&
|
|
$GLOBALS['registry']->hasMethod('contacts/addField')) {
|
|
// TODO: Check for key existence.
|
|
$imple = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Imple')->create('IMP_Ajax_Imple_ImportEncryptKey', array(
|
|
'mime_id' => $mime_id,
|
|
'muid' => strval($imp_contents->getIndicesOb()),
|
|
'type' => 'pgp'
|
|
));
|
|
$status->addText(Horde::link('#', '', '', '', '', '', '', array('id' => $imple->getDomId())) . _("Save the key to your address book.") . '</a>');
|
|
}
|
|
$status->addText($imp_contents->linkViewJS($this->_mimepart, 'view_attach', _("View key details."), array('params' => array('mode' => IMP_Contents::RENDER_FULL, 'pgp_view_key' => 1))));
|
|
|
|
return array(
|
|
$mime_id => array(
|
|
'data' => '',
|
|
'status' => $status,
|
|
'type' => 'text/html; charset=' . $this->getConfigParam('charset')
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generates HTML output for 'multipart/signed' MIME parts.
|
|
*
|
|
* @return string The HTML output.
|
|
*/
|
|
protected function _outputPGPSigned()
|
|
{
|
|
global $conf, $injector, $prefs, $registry, $session;
|
|
|
|
$partlist = array_keys($this->_mimepart->contentTypeMap());
|
|
$base_id = reset($partlist);
|
|
$signed_id = next($partlist);
|
|
$sig_id = Horde_Mime::mimeIdArithmetic($signed_id, 'next');
|
|
|
|
if (!$prefs->getValue('use_pgp') || empty($conf['gnupg']['path'])) {
|
|
return array(
|
|
$sig_id => null
|
|
);
|
|
}
|
|
|
|
$status = new IMP_Mime_Status();
|
|
$status->addText(_("The data in this part has been digitally signed via PGP."));
|
|
$status->icon('mime/encryption.png', 'PGP');
|
|
|
|
$ret = array(
|
|
$base_id => array(
|
|
'data' => '',
|
|
'nosummary' => true,
|
|
'status' => array($status),
|
|
'type' => 'text/html; charset=' . $this->getConfigParam('charset'),
|
|
'wrap' => 'mimePartWrap'
|
|
),
|
|
$sig_id => null
|
|
);
|
|
|
|
if ($prefs->getValue('pgp_verify') ||
|
|
$injector->getInstance('Horde_Variables')->pgp_verify_msg) {
|
|
$imp_contents = $this->getConfigParam('imp_contents');
|
|
$sig_part = $imp_contents->getMIMEPart($sig_id);
|
|
|
|
$status2 = new IMP_Mime_Status();
|
|
|
|
if (!$sig_part) {
|
|
$status2->action(IMP_Mime_Status::ERROR);
|
|
$sig_text = _("This digitally signed message is broken.");
|
|
$ret[$base_id]['wrap'] = 'mimePartWrapInvalid';
|
|
} else {
|
|
/* Close session, since this may be a long-running
|
|
* operation. */
|
|
$session->close();
|
|
|
|
try {
|
|
$imp_pgp = $injector->getInstance('IMP_Crypt_Pgp');
|
|
if ($sig_raw = $sig_part->getMetadata(Horde_Crypt_Pgp_Parse::SIG_RAW)) {
|
|
$sig_result = $imp_pgp->verifySignature($sig_raw, $this->_getSender()->bare_address, null, $sig_part->getMetadata(Horde_Crypt_Pgp_Parse::SIG_CHARSET));
|
|
} else {
|
|
$stream = $imp_contents->isEmbedded($signed_id)
|
|
? $this->_mimepart->getMetadata(self::PGP_SIGN_ENC)
|
|
: $imp_contents->getBodyPart($signed_id, array('mimeheaders' => true, 'stream' => true))->data;
|
|
|
|
rewind($stream);
|
|
stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
|
|
stream_filter_append($stream, 'horde_eol', STREAM_FILTER_READ, array(
|
|
'eol' => Horde_Mime_Part::RFC_EOL
|
|
));
|
|
|
|
$sig_result = $imp_pgp->verifySignature(stream_get_contents($stream), $this->_getSender()->bare_address, $sig_part->getContents());
|
|
}
|
|
|
|
$status2->action(IMP_Mime_Status::SUCCESS);
|
|
$sig_text = $sig_result->message;
|
|
$ret[$base_id]['wrap'] = 'mimePartWrapValid';
|
|
} catch (Horde_Exception $e) {
|
|
$status2->action(IMP_Mime_Status::ERROR);
|
|
$sig_text = $e->getMessage();
|
|
$ret[$base_id]['wrap'] = 'mimePartWrapInvalid';
|
|
}
|
|
}
|
|
|
|
$status2->addText($this->_textFilter($sig_text, 'text2html', array(
|
|
'parselevel' => Horde_Text_Filter_Text2html::NOHTML
|
|
)));
|
|
$ret[$base_id]['status'][] = $status2;
|
|
} else {
|
|
switch ($registry->getView()) {
|
|
case Horde_Registry::VIEW_BASIC:
|
|
$status->addText(Horde::link(Horde::selfUrlParams()->add(array('pgp_verify_msg' => 1))) . _("Click HERE to verify the message.") . '</a>');
|
|
break;
|
|
|
|
case Horde_Registry::VIEW_DYNAMIC:
|
|
$status->addText(Horde::link('#', '', 'pgpVerifyMsg') . _("Click HERE to verify the message.") . '</a>');
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Generates the symmetric ID for this message.
|
|
*
|
|
* @return string Symmetric ID.
|
|
*/
|
|
protected function _getSymmetricID()
|
|
{
|
|
return $GLOBALS['injector']->getInstance('IMP_Crypt_Pgp')->getSymmetricID($this->getConfigParam('imp_contents')->getMailbox(), $this->getConfigParam('imp_contents')->getUid(), $this->_mimepart->getMimeId());
|
|
}
|
|
|
|
/**
|
|
* Determine the address of the sender.
|
|
*
|
|
* @return Horde_Mail_Rfc822_Address The from address.
|
|
*/
|
|
protected function _getSender()
|
|
{
|
|
if (is_null($this->_sender)) {
|
|
$headers = $this->getConfigParam('imp_contents')->getHeader();
|
|
$tmp = $headers->getOb('from');
|
|
$this->_sender = $tmp[0];
|
|
}
|
|
|
|
return $this->_sender;
|
|
}
|
|
|
|
/**
|
|
* Can this driver render the data?
|
|
*
|
|
* @param string $mode See parent::canRender().
|
|
*
|
|
* @return boolean See parent::canRender().
|
|
*/
|
|
public function canRender($mode)
|
|
{
|
|
switch ($mode) {
|
|
case 'full':
|
|
if ($this->_mimepart->getType() == 'application/pgp-keys') {
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 'raw':
|
|
if (($this->_mimepart->getType() == 'application/pgp-signature') &&
|
|
$this->_mimepart->getMetadata(Horde_Crypt_Pgp_Parse::SIG_RAW)) {
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return parent::canRender($mode);
|
|
}
|
|
|
|
}
|