466 lines
16 KiB
PHP
466 lines
16 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 2000-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/gpl GPL
|
|
* @package IMP
|
|
*/
|
|
|
|
/**
|
|
* Renderer for viewing/decrypting of S/MIME v3.2 messages (RFC 5751).
|
|
*
|
|
* This class handles the following MIME types:
|
|
* application/pkcs7-mime
|
|
* application/x-pkcs7-mime
|
|
* application/pkcs7-signature (in multipart/signed part)
|
|
* application/x-pkcs7-signature (in multipart/signed part)
|
|
*
|
|
* This class may add the following parameters to the URL:
|
|
* 'smime_verify_msg' - (boolean) Do verification of S/MIME message.
|
|
* 'view_smime_key' - (boolean) Display the S/MIME Key.
|
|
*
|
|
* @author Mike Cochrane <mike@graftonhall.co.nz>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @copyright 2000-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/gpl GPL
|
|
* @package IMP
|
|
*/
|
|
class IMP_Mime_Viewer_Smime extends Horde_Mime_Viewer_Base
|
|
{
|
|
/**
|
|
* This driver's display capabilities.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_capability = array(
|
|
'full' => false,
|
|
'info' => false,
|
|
'inline' => true,
|
|
'raw' => false
|
|
);
|
|
|
|
/**
|
|
* Metadata for the current viewer/data.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_metadata = array(
|
|
'compressed' => false,
|
|
'embedded' => true,
|
|
'forceinline' => true
|
|
);
|
|
|
|
/**
|
|
* IMP_Crypt_Smime object.
|
|
*
|
|
* @var IMP_Crypt_Smime
|
|
*/
|
|
protected $_impsmime = null;
|
|
|
|
/**
|
|
* Init the S/MIME Horde_Crypt object.
|
|
*/
|
|
protected function _initSmime()
|
|
{
|
|
if (is_null($this->_impsmime) &&
|
|
$GLOBALS['prefs']->getValue('use_smime')) {
|
|
try {
|
|
$this->_impsmime = $GLOBALS['injector']->getInstance('IMP_Crypt_Smime');
|
|
$this->_impsmime->checkForOpenSSL();
|
|
} catch (Horde_Exception $e) {
|
|
$this->_impsmime = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the rendered inline version of the Horde_Mime_Part object.
|
|
*
|
|
* @return array See parent::render().
|
|
*/
|
|
protected function _renderInline()
|
|
{
|
|
/* Check to see if S/MIME support is available. */
|
|
$this->_initSmime();
|
|
|
|
if ($GLOBALS['injector']->getInstance('Horde_Variables')->view_smime_key) {
|
|
return $this->_outputSmimeKey();
|
|
}
|
|
|
|
$id = $this->_mimepart->getMimeId();
|
|
|
|
switch ($this->_mimepart->getType()) {
|
|
case 'multipart/signed':
|
|
if (!in_array($this->_mimepart->getContentTypeParameter('protocol'), array('application/pkcs7-signature', 'application/x-pkcs7-signature'))) {
|
|
return array();
|
|
}
|
|
$this->_parseSignedData(true);
|
|
// Fall-through
|
|
|
|
case 'application/pkcs7-mime':
|
|
case 'application/x-pkcs7-mime':
|
|
$cache = $this->getConfigParam('imp_contents')->getViewCache();
|
|
|
|
if (isset($cache->smime[$id])) {
|
|
$ret = array(
|
|
$id => array(
|
|
'data' => null,
|
|
'status' => $cache->smime[$id]['status'],
|
|
'type' => 'text/plain; charset=' . $this->getConfigParam('charset'),
|
|
'wrap' => $cache->smime[$id]['wrap']
|
|
)
|
|
);
|
|
if (isset($cache->smime[$id]['sig'])) {
|
|
$ret[$cache->smime[$id]['sig']] = null;
|
|
}
|
|
return $ret;
|
|
}
|
|
// Fall-through
|
|
|
|
default:
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If this MIME part can contain embedded MIME parts, and those embedded
|
|
* MIME parts exist, return an altered version of the Horde_Mime_Part that
|
|
* contains the embedded MIME part information.
|
|
*
|
|
* @return mixed A Horde_Mime_Part with the embedded MIME part information
|
|
* or null if no embedded MIME parts exist.
|
|
*/
|
|
protected function _getEmbeddedMimeParts()
|
|
{
|
|
if (!in_array($this->_mimepart->getType(), array('application/pkcs7-mime', 'application/x-pkcs7-mime'))) {
|
|
return null;
|
|
}
|
|
|
|
switch ($this->_getSmimeType($this->_mimepart)) {
|
|
case 'signed-data':
|
|
return $this->_parseSignedData();
|
|
|
|
case 'enveloped-data':
|
|
return $this->_parseEnvelopedData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse enveloped (encrypted) data.
|
|
*
|
|
* @return mixed See self::_getEmbeddedMimeParts().
|
|
*/
|
|
protected function _parseEnvelopedData()
|
|
{
|
|
$base_id = $this->_mimepart->getMimeId();
|
|
|
|
/* Initialize inline data. */
|
|
$status = new IMP_Mime_Status(_("The data in this part has been encrypted via S/MIME."));
|
|
$status->icon('mime/encryption.png', 'S/MIME');
|
|
|
|
$cache = $this->getConfigParam('imp_contents')->getViewCache();
|
|
$cache->smime[$base_id] = array(
|
|
'status' => $status,
|
|
'wrap' => ''
|
|
);
|
|
|
|
/* Is PGP active? */
|
|
$this->_initSmime();
|
|
if (empty($this->_impsmime)) {
|
|
$status->addText(_("S/MIME support is not currently enabled so the data is unable to be decrypted."));
|
|
return null;
|
|
}
|
|
|
|
if (!$this->_impsmime->getPersonalPrivateKey()) {
|
|
$status->addText(_("No personal private key exists so the data is unable to be decrypted."));
|
|
return null;
|
|
}
|
|
|
|
/* Make sure we have a passphrase. */
|
|
$passphrase = $this->_impsmime->getPassphrase();
|
|
if ($passphrase === false) {
|
|
$imple = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Imple')->create('IMP_Ajax_Imple_PassphraseDialog', array(
|
|
'type' => 'smimePersonal'
|
|
));
|
|
$status->addText(Horde::link('#', '', '', '', '', '', '', array('id' => $imple->getDomId())) . _("You must enter the passphrase for your S/MIME private key to view this data.") . '</a>');
|
|
return null;
|
|
}
|
|
|
|
$raw_text = $this->_getPartStream($this->_mimepart->getMimeId());
|
|
|
|
try {
|
|
$decrypted_data = $this->_impsmime->decryptMessage($this->_mimepart->replaceEOL($raw_text, Horde_Mime_Part::RFC_EOL));
|
|
} catch (Horde_Exception $e) {
|
|
$status->addText($e->getMessage());
|
|
return null;
|
|
}
|
|
|
|
$cache->smime[$base_id]['wrap'] = 'mimePartWrapValid';
|
|
|
|
$new_part = Horde_Mime_Part::parseMessage($decrypted_data, array(
|
|
'forcemime' => true
|
|
));
|
|
|
|
switch ($new_part->getType()) {
|
|
case 'application/pkcs7-mime':
|
|
case 'application/x-pkcs7-mime':
|
|
$signed_data = ($this->_getSmimeType($new_part) === 'signed-data');
|
|
break;
|
|
|
|
case 'multipart/signed':
|
|
$signed_data = true;
|
|
break;
|
|
|
|
default:
|
|
$signed_data = false;
|
|
break;
|
|
}
|
|
|
|
if ($signed_data) {
|
|
$hdrs = $this->getConfigParam('imp_contents')->getHeader();
|
|
|
|
$data = new Horde_Stream_Temp();
|
|
$data->add(
|
|
'From:' . $hdrs->getValue('from') . "\n" .
|
|
$decrypted_data
|
|
);
|
|
|
|
$new_part->setMetadata('imp-smime-decrypt', $data);
|
|
$new_part->setContents($decrypted_data, array(
|
|
'encoding' => 'binary'
|
|
));
|
|
}
|
|
|
|
return $new_part;
|
|
}
|
|
|
|
/**
|
|
* Parse signed data.
|
|
*
|
|
* @param boolean $sig_only Only do signature checking?
|
|
*
|
|
* @return mixed See self::_getEmbeddedMimeParts().
|
|
*/
|
|
protected function _parseSignedData($sig_only = false)
|
|
{
|
|
$partlist = array_keys($this->_mimepart->contentTypeMap());
|
|
$base_id = reset($partlist);
|
|
if (!$data_id = next($partlist)) {
|
|
$data_id = $base_id;
|
|
}
|
|
$sig_id = Horde_Mime::mimeIdArithmetic($data_id, 'next');
|
|
|
|
/* Initialize inline data. */
|
|
$status = new IMP_Mime_Status(_("The data in this part has been digitally signed via S/MIME."));
|
|
$status->icon('mime/encryption.png', 'S/MIME');
|
|
|
|
$cache = $this->getConfigParam('imp_contents')->getViewCache();
|
|
$cache->smime[$base_id] = array(
|
|
'sig' => $sig_id,
|
|
'status' => $status,
|
|
'wrap' => 'mimePartWrap'
|
|
);
|
|
|
|
if (!$GLOBALS['prefs']->getValue('use_smime')) {
|
|
$status->addText(_("S/MIME support is not enabled so the digital signature is unable to be verified."));
|
|
return null;
|
|
}
|
|
|
|
$imp_contents = $this->getConfigParam('imp_contents');
|
|
$stream = $imp_contents->isEmbedded($base_id)
|
|
? $this->_mimepart->getMetadata('imp-smime-decrypt')->stream
|
|
: $this->_getPartStream($base_id);
|
|
$raw_text = $this->_mimepart->replaceEOL($stream, Horde_Mime_Part::RFC_EOL);
|
|
|
|
$this->_initSmime();
|
|
$sig_result = null;
|
|
|
|
if ($GLOBALS['prefs']->getValue('smime_verify') ||
|
|
$GLOBALS['injector']->getInstance('Horde_Variables')->smime_verify_msg) {
|
|
try {
|
|
$sig_result = $this->_impsmime->verifySignature($raw_text);
|
|
if ($sig_result->verify) {
|
|
$status->action(IMP_Mime_Status::SUCCESS);
|
|
} else {
|
|
$status->action(IMP_Mime_Status::WARNING);
|
|
}
|
|
if (!is_array($sig_result->email)) {
|
|
$sig_result->email = array($sig_result->email);
|
|
}
|
|
$email = implode(', ', $sig_result->email);
|
|
$cache->smime[$base_id]['wrap'] = 'mimePartWrapValid';
|
|
$status->addText($sig_result->msg);
|
|
|
|
if (!empty($sig_result->cert)) {
|
|
$cert = $this->_impsmime->parseCert($sig_result->cert);
|
|
if (isset($cert['certificate']['subject']['CommonName']) &&
|
|
(strcasecmp($email, $cert['certificate']['subject']['CommonName']) !== 0)) {
|
|
$email = $cert['certificate']['subject']['CommonName'] . ' (' . trim($email) . ')';
|
|
}
|
|
}
|
|
|
|
if (!empty($sig_result->cert) &&
|
|
isset($sig_result->email) &&
|
|
$GLOBALS['registry']->hasMethod('contacts/addField') &&
|
|
$GLOBALS['prefs']->getValue('add_source')) {
|
|
$status->addText(sprintf(_("Sender: %s"), $imp_contents->linkViewJS($this->_mimepart, 'view_attach', htmlspecialchars($email), array(
|
|
'jstext' => _("View certificate details"),
|
|
'params' => array(
|
|
'mode' => IMP_Contents::RENDER_INLINE,
|
|
'view_smime_key' => 1
|
|
)
|
|
))));
|
|
|
|
foreach ($sig_result->email as $single_email) {
|
|
try {
|
|
$this->_impsmime->getPublicKey($single_email);
|
|
} catch (Horde_Exception $e) {
|
|
$imple = $GLOBALS['injector']
|
|
->getInstance('Horde_Core_Factory_Imple')
|
|
->create(
|
|
'IMP_Ajax_Imple_ImportEncryptKey',
|
|
array(
|
|
'mime_id' => $base_id,
|
|
'muid' => strval($imp_contents->getIndicesOb()),
|
|
'type' => 'smime'
|
|
)
|
|
);
|
|
$status->addText(
|
|
Horde::link(
|
|
'#', '', '', '', '', '', '',
|
|
array('id' => $imple->getDomId())
|
|
)
|
|
. _("Save the certificate to your Address Book.")
|
|
. '</a>'
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
} elseif (strlen($email)) {
|
|
$status->addText(sprintf(_("Sender: %s"), htmlspecialchars($email)));
|
|
}
|
|
} catch (Horde_Exception $e) {
|
|
$status->action(IMP_Mime_Status::ERROR);
|
|
$cache->smime[$base_id]['wrap'] = 'mimePartWrapInvalid';
|
|
$status->addText($e->getMessage());
|
|
}
|
|
} else {
|
|
switch ($GLOBALS['registry']->getView()) {
|
|
case Horde_Registry::VIEW_BASIC:
|
|
$status->addText(Horde::link(Horde::selfUrlParams()->add('smime_verify_msg', 1)) . _("Click HERE to verify the data.") . '</a>');
|
|
break;
|
|
|
|
case Horde_Registry::VIEW_DYNAMIC:
|
|
$status->addText(Horde::link('#', '', 'smimeVerifyMsg') . _("Click HERE to verify the data.") . '</a>');
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($sig_only) {
|
|
return;
|
|
}
|
|
|
|
$subpart = $imp_contents->getMIMEPart($sig_id);
|
|
if (empty($subpart)) {
|
|
try {
|
|
$msg_data = $this->_impsmime->extractSignedContents($raw_text);
|
|
$subpart = Horde_Mime_Part::parseMessage($msg_data, array('forcemime' => true));
|
|
} catch (Horde_Exception $e) {
|
|
$status->addText($e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return $subpart;
|
|
}
|
|
|
|
/**
|
|
* Generates HTML output for the S/MIME key.
|
|
*
|
|
* @return string The HTML output.
|
|
*/
|
|
protected function _outputSmimeKey()
|
|
{
|
|
if (empty($this->_impsmime)) {
|
|
return array();
|
|
}
|
|
|
|
$raw_text = $this->_getPartStream($this->_mimepart->getMimeId());
|
|
|
|
try {
|
|
$sig_result = $this->_impsmime->verifySignature($this->_mimepart->replaceEOL($raw_text, Horde_Mime_Part::RFC_EOL));
|
|
} catch (Horde_Exception $e) {
|
|
return array();
|
|
}
|
|
|
|
return array(
|
|
$this->_mimepart->getMimeId() => array(
|
|
'data' => $this->_impsmime->certToHTML($sig_result->cert),
|
|
'type' => 'text/html; charset=' . $this->getConfigParam('charset')
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
*/
|
|
protected function _getPartStream($id)
|
|
{
|
|
return $id
|
|
? $this->getConfigParam('imp_contents')->getBodyPart($id, array('mimeheaders' => true, 'stream' => true))->data
|
|
: $this->getConfigParam('imp_contents')->fullMessageText();
|
|
}
|
|
|
|
/**
|
|
* Determines the S/MIME type of a part. Uses the smime-type content
|
|
* parameter (if it exists), and falls back to ASN.1 parsing of data if
|
|
* it doesn't exist.
|
|
*
|
|
* @param Horde_Mime_Part $part MIME part with S/MIME data.
|
|
*
|
|
* @return string 'signed-data', 'enveloped-data', or null.
|
|
*/
|
|
protected function _getSmimeType(Horde_Mime_Part $part)
|
|
{
|
|
if ($type = $part->getContentTypeParameter('smime-type')) {
|
|
return strtolower($type);
|
|
}
|
|
|
|
if (!class_exists('File_ASN1')) {
|
|
return null;
|
|
}
|
|
|
|
$asn1 = new File_ASN1();
|
|
$decoded = $asn1->decodeBER($part->getContents());
|
|
|
|
foreach ($decoded as $val) {
|
|
if ($val['type'] == FILE_ASN1_TYPE_SEQUENCE) {
|
|
foreach ($val['content'] as $val2) {
|
|
if ($val2['type'] == FILE_ASN1_TYPE_OBJECT_IDENTIFIER) {
|
|
/* ASN.1 values from STD 70/RFC 5652 - CMS syntax */
|
|
switch ($val2['content']) {
|
|
case '1.2.840.113549.1.7.2':
|
|
return 'signed-data';
|
|
|
|
case '1.2.840.113549.1.7.3':
|
|
return 'enveloped-data';
|
|
|
|
default:
|
|
// Other types not supported as of now.
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
}
|