Files
server/usr/share/psa-horde/imp/lib/Mime/Viewer/Smime.php
2026-01-07 20:52:11 +01:00

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;
}
}