From 4e6f3019f5bf4d322c288b161a781d7d7147b4f0 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Fri, 25 Mar 2016 08:25:44 -0400 Subject: [PATCH] Enigma: Handle encrypted/signed content inside message/rfc822 attachments --- plugins/enigma/lib/enigma_engine.php | 350 +++++++++++++++++++++++++++++++++++++++------------------ 1 files changed, 239 insertions(+), 111 deletions(-) diff --git a/plugins/enigma/lib/enigma_engine.php b/plugins/enigma/lib/enigma_engine.php index 6c5ee3c..9ce1761 100644 --- a/plugins/enigma/lib/enigma_engine.php +++ b/plugins/enigma/lib/enigma_engine.php @@ -1,5 +1,6 @@ <?php -/* + +/** +-------------------------------------------------------------------------+ | Engine of the Enigma Plugin | | | @@ -14,26 +15,24 @@ +-------------------------------------------------------------------------+ */ -/* - RFC2440: OpenPGP Message Format - RFC3156: MIME Security with OpenPGP - RFC3851: S/MIME -*/ - +/** + * Enigma plugin engine. + * + * RFC2440: OpenPGP Message Format + * RFC3156: MIME Security with OpenPGP + * RFC3851: S/MIME + */ class enigma_engine { private $rc; private $enigma; private $pgp_driver; private $smime_driver; + private $password_time; public $decryptions = array(); public $signatures = array(); - public $signed_parts = array(); public $encrypted_parts = array(); - - - const PASSWORD_TIME = 120; const SIGN_MODE_BODY = 1; const SIGN_MODE_SEPARATE = 2; @@ -51,8 +50,12 @@ $this->rc = rcmail::get_instance(); $this->enigma = $enigma; + $this->password_time = $this->rc->config->get('enigma_password_time') * 60; + // this will remove passwords from session after some time - $this->get_passwords(); + if ($this->password_time) { + $this->get_passwords(); + } } /** @@ -142,7 +145,7 @@ $key = $this->find_key($from, true); if (empty($key)) { - return new enigma_error(enigma_error::E_KEYNOTFOUND); + return new enigma_error(enigma_error::KEYNOTFOUND); } // check if we have password for this key @@ -152,7 +155,7 @@ if ($pass === null) { // ask for password $error = array('missing' => array($key->id => $key->name)); - return new enigma_error(enigma_error::E_BADPASS, '', $error); + return new enigma_error(enigma_error::BADPASS, '', $error); } // select mode @@ -183,6 +186,18 @@ // in this mode we'll replace text part // with the one containing signature $body = $message->getTXTBody(); + + $text_charset = $message->getParam('text_charset'); + $line_length = $this->rc->config->get('line_length', 72); + + // We can't use format=flowed for signed messages + if (strpos($text_charset, 'format=flowed')) { + list($charset, $params) = explode(';', $text_charset); + $body = rcube_mime::unfold_flowed($body); + $body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset); + + $text_charset = str_replace(";\r\n format=flowed", '', $text_charset); + } } else { // here we'll build PGP/MIME message @@ -193,10 +208,10 @@ $result = $this->pgp_sign($body, $key->id, $pass, $pgp_mode); if ($result !== true) { - if ($result->getCode() == enigma_error::E_BADPASS) { + if ($result->getCode() == enigma_error::BADPASS) { // ask for password - $error = array('missing' => array($key->id => $key->name)); - return new enigma_error(enigma_error::E_BADPASS, '', $error); + $error = array('bad' => array($key->id => $key->name)); + return new enigma_error(enigma_error::BADPASS, '', $error); } return $result; @@ -205,6 +220,7 @@ // replace message body if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) { $message->setTXTBody($body); + $message->setParam('text_charset', $text_charset); } else { $mime->addPGPSignature($body); @@ -234,7 +250,7 @@ } if (empty($recipients)) { - return new enigma_error(enigma_error::E_KEYNOTFOUND); + return new enigma_error(enigma_error::KEYNOTFOUND); } $recipients = array_unique($recipients); @@ -244,7 +260,7 @@ $key = $this->find_key($email); if (empty($key)) { - return new enigma_error(enigma_error::E_KEYNOTFOUND, '', array( + return new enigma_error(enigma_error::KEYNOTFOUND, '', array( 'missing' => $email )); } @@ -298,17 +314,18 @@ * Handler for message_part_structure hook. * Called for every part of the message. * - * @param array Original parameters + * @param array Original parameters + * @param string Part body (will be set if used internally) * * @return array Modified parameters */ - function part_structure($p) + function part_structure($p, $body = null) { if ($p['mimetype'] == 'text/plain' || $p['mimetype'] == 'application/pgp') { - $this->parse_plain($p); + $this->parse_plain($p, $body); } else if ($p['mimetype'] == 'multipart/signed') { - $this->parse_signed($p); + $this->parse_signed($p, $body); } else if ($p['mimetype'] == 'multipart/encrypted') { $this->parse_encrypted($p); @@ -351,9 +368,10 @@ /** * Handler for plain/text message. * - * @param array Reference to hook's parameters + * @param array Reference to hook's parameters + * @param string Part body (will be set if used internally) */ - function parse_plain(&$p) + function parse_plain(&$p, $body = null) { $part = $p['structure']; @@ -363,7 +381,9 @@ } // Get message body from IMAP server - $body = $this->get_part_body($p['object'], $part->mime_id); + if ($body === null) { + $body = $this->get_part_body($p['object'], $part); + } // @TODO: big message body could be a file resource // PGP signed message @@ -379,15 +399,16 @@ /** * Handler for multipart/signed message. * - * @param array Reference to hook's parameters + * @param array Reference to hook's parameters + * @param string Part body (will be set if used internally) */ - function parse_signed(&$p) + function parse_signed(&$p, $body = null) { $struct = $p['structure']; // S/MIME if ($struct->parts[1] && $struct->parts[1]->mimetype == 'application/pkcs7-signature') { - $this->parse_smime_signed($p); + $this->parse_smime_signed($p, $body); } // PGP/MIME: RFC3156 // The multipart/signed body MUST consist of exactly two parts. @@ -395,11 +416,10 @@ // including a set of appropriate content headers describing the data. // The second body MUST contain the PGP digital signature. It MUST be // labeled with a content type of "application/pgp-signature". - else if ($struct->ctype_parameters['protocol'] == 'application/pgp-signature' - && count($struct->parts) == 2 + else if (count($struct->parts) == 2 && $struct->parts[1] && $struct->parts[1]->mimetype == 'application/pgp-signature' ) { - $this->parse_pgp_signed($p); + $this->parse_pgp_signed($p, $body); } } @@ -413,7 +433,7 @@ $struct = $p['structure']; // S/MIME - if ($struct->mimetype == 'application/pkcs7-mime') { + if ($p['mimetype'] == 'application/pkcs7-mime') { $this->parse_smime_encrypted($p); } // PGP/MIME: RFC3156 @@ -422,8 +442,7 @@ // This body contains the control information. // The second MIME body part MUST contain the actual encrypted data. It // must be labeled with a content type of "application/octet-stream". - else if ($struct->ctype_parameters['protocol'] == 'application/pgp-encrypted' - && count($struct->parts) == 2 + else if (count($struct->parts) == 2 && $struct->parts[0] && $struct->parts[0]->mimetype == 'application/pgp-encrypted' && $struct->parts[1] && $struct->parts[1]->mimetype == 'application/octet-stream' ) { @@ -440,11 +459,15 @@ */ private function parse_plain_signed(&$p, $body) { + if (!$this->rc->config->get('enigma_signatures', true)) { + return; + } + $this->load_pgp_driver(); $part = $p['structure']; // Verify signature - if ($this->rc->action == 'show' || $this->rc->action == 'preview') { + if ($this->rc->action == 'show' || $this->rc->action == 'preview' || $this->rc->action == 'print') { $sig = $this->pgp_verify($body); } @@ -480,7 +503,6 @@ // Store signature data for display if (!empty($sig)) { - $this->signed_parts[$part->mime_id] = $part->mime_id; $this->signatures[$part->mime_id] = $sig; } @@ -492,72 +514,58 @@ * Verifies signature. * * @param array Reference to hook's parameters + * @param string Part body (will be set if used internally) */ - private function parse_pgp_signed(&$p) + private function parse_pgp_signed(&$p, $body = null) { - // Verify signature - if ($this->rc->action == 'show' || $this->rc->action == 'preview') { - $this->load_pgp_driver(); - $struct = $p['structure']; - - $msg_part = $struct->parts[0]; - $sig_part = $struct->parts[1]; - - // Get bodies - // Note: The first part body need to be full part body with headers - // it also cannot be decoded - $msg_body = $this->get_part_body($p['object'], $msg_part->mime_id, true); - $sig_body = $this->get_part_body($p['object'], $sig_part->mime_id); - - // Verify - $sig = $this->pgp_verify($msg_body, $sig_body); - - // Store signature data for display - $this->signatures[$struct->mime_id] = $sig; - - // Message can be multipart (assign signature to each subpart) - if (!empty($msg_part->parts)) { - foreach ($msg_part->parts as $part) - $this->signed_parts[$part->mime_id] = $struct->mime_id; - } - else { - $this->signed_parts[$msg_part->mime_id] = $struct->mime_id; - } + if (!$this->rc->config->get('enigma_signatures', true)) { + return; } + + if ($this->rc->action != 'show' && $this->rc->action != 'preview' && $this->rc->action != 'print') { + return; + } + + $this->load_pgp_driver(); + $struct = $p['structure']; + + $msg_part = $struct->parts[0]; + $sig_part = $struct->parts[1]; + + // Get bodies + // Note: The first part body need to be full part body with headers + // it also cannot be decoded + if ($body !== null) { + // set signed part body + list($msg_body, $sig_body) = $this->explode_signed_body($body, $struct->ctype_parameters['boundary']); + } + else { + $msg_body = $this->get_part_body($p['object'], $msg_part, true); + $sig_body = $this->get_part_body($p['object'], $sig_part); + } + + // Verify + $sig = $this->pgp_verify($msg_body, $sig_body); + + // Store signature data for display + $this->signatures[$struct->mime_id] = $sig; + $this->signatures[$msg_part->mime_id] = $sig; } /** * Handler for S/MIME signed message. * Verifies signature. * - * @param array Reference to hook's parameters + * @param array Reference to hook's parameters + * @param string Part body (will be set if used internally) */ - private function parse_smime_signed(&$p) + private function parse_smime_signed(&$p, $body = null) { - return; // @TODO - - // Verify signature - if ($this->rc->action == 'show' || $this->rc->action == 'preview') { - $this->load_smime_driver(); - - $struct = $p['structure']; - $msg_part = $struct->parts[0]; - - // Verify - $sig = $this->smime_driver->verify($struct, $p['object']); - - // Store signature data for display - $this->signatures[$struct->mime_id] = $sig; - - // Message can be multipart (assign signature to each subpart) - if (!empty($msg_part->parts)) { - foreach ($msg_part->parts as $part) - $this->signed_parts[$part->mime_id] = $struct->mime_id; - } - else { - $this->signed_parts[$msg_part->mime_id] = $struct->mime_id; - } + if (!$this->rc->config->get('enigma_signatures', true)) { + return; } + + // @TODO } /** @@ -568,6 +576,10 @@ */ private function parse_plain_encrypted(&$p, $body) { + if (!$this->rc->config->get('enigma_decryption', true)) { + return; + } + $this->load_pgp_driver(); $part = $p['structure']; @@ -642,13 +654,17 @@ */ private function parse_pgp_encrypted(&$p) { + if (!$this->rc->config->get('enigma_decryption', true)) { + return; + } + $this->load_pgp_driver(); $struct = $p['structure']; $part = $struct->parts[1]; // Get body - $body = $this->get_part_body($p['object'], $part->mime_id); + $body = $this->get_part_body($p['object'], $part); // Decrypt $result = $this->pgp_decrypt($body); @@ -658,7 +674,14 @@ $struct = $this->parse_body($body); // Modify original message structure - $this->modify_structure($p, $struct); + $this->modify_structure($p, $struct, strlen($body)); + + // Parse the structure (there may be encrypted/signed parts inside + $this->part_structure(array( + 'object' => $p['object'], + 'structure' => $struct, + 'mimetype' => $struct->mimetype + ), $body); // Attach the decryption message to all parts $this->decryptions[$struct->mime_id] = $result; @@ -672,6 +695,10 @@ // Make sure decryption status message will be displayed $part->type = 'content'; $p['object']->parts[] = $part; + + // don't show encrypted part on attachments list + // don't show "cannot display encrypted message" text + $p['abort'] = true; } } @@ -682,7 +709,11 @@ */ private function parse_smime_encrypted(&$p) { -// $this->load_smime_driver(); + if (!$this->rc->config->get('enigma_decryption', true)) { + return; + } + + // @TODO } /** @@ -698,7 +729,7 @@ // @TODO: Handle big bodies using (temp) files $sig = $this->pgp_driver->verify($msg_body, $sig_body); - if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::E_KEYNOTFOUND) + if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::KEYNOTFOUND) rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, @@ -723,7 +754,7 @@ if ($result instanceof enigma_error) { $err_code = $result->getCode(); - if (!in_array($err_code, array(enigma_error::E_KEYNOTFOUND, enigma_error::E_BADPASS))) + if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS))) rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, @@ -754,7 +785,7 @@ if ($result instanceof enigma_error) { $err_code = $result->getCode(); - if (!in_array($err_code, array(enigma_error::E_KEYNOTFOUND, enigma_error::E_BADPASS))) + if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS))) rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, @@ -783,7 +814,7 @@ if ($result instanceof enigma_error) { $err_code = $result->getCode(); - if (!in_array($err_code, array(enigma_error::E_KEYNOTFOUND, enigma_error::E_BADPASS))) + if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS))) rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, @@ -900,6 +931,29 @@ } /** + * PGP keys pair generation. + * + * @param array Key pair parameters + * + * @return mixed enigma_key or enigma_error + */ + function generate_key($data) + { + $this->load_pgp_driver(); + $result = $this->pgp_driver->gen_key($data); + + if ($result instanceof enigma_error) { + rcube::raise_error(array( + 'code' => 600, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Enigma plugin: " . $result->getMessage() + ), true, false); + } + + return $result; + } + + /** * PGP keys/certs importing. * * @param mixed Import file name or content @@ -952,6 +1006,40 @@ $this->rc->output->send(); } + /** + * PGP keys/certs export.. + * + * @param string Key ID + * @param resource Optional output stream + * + * @return mixed Key content or enigma_error + */ + function export_key($key, $fp = null) + { + $this->load_pgp_driver(); + $result = $this->pgp_driver->export($key, $fp); + + if ($result instanceof enigma_error) { + rcube::raise_error(array( + 'code' => 600, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Enigma plugin: " . $result->getMessage() + ), true, false); + + return $result; + } + + if ($fp) { + fwrite($fp, $result); + } + else { + return $result; + } + } + + /** + * Registers password for specified key/cert sent by the password prompt. + */ function password_handler() { $keyid = rcube_utils::get_input_value('_keyid', rcube_utils::INPUT_POST); @@ -962,6 +1050,9 @@ } } + /** + * Saves key/cert password in user session + */ function save_password($keyid, $password) { // we store passwords in session for specified time @@ -975,6 +1066,9 @@ $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config)); } + /** + * Returns currently stored passwords + */ function get_passwords() { if ($config = $_SESSION['enigma_pass']) { @@ -982,12 +1076,12 @@ $config = @unserialize($config); } - $threshold = time() - self::PASSWORD_TIME; + $threshold = $this->password_time ? time() - $this->password_time : 0; $keys = array(); // delete expired passwords foreach ((array) $config as $key => $value) { - if ($value[1] < $threshold) { + if ($threshold && $value[1] < $threshold) { unset($config[$key]); $modified = true; } @@ -1006,20 +1100,21 @@ /** * Get message part body. * - * @param rcube_message Message object - * @param string Message part ID - * @param bool Return raw body with headers + * @param rcube_message Message object + * @param rcube_message_part Message part + * @param bool Return raw body with headers */ - private function get_part_body($msg, $part_id, $full = false) + private function get_part_body($msg, $part, $full = false) { // @TODO: Handle big bodies using file handles + if ($full) { $storage = $this->rc->get_storage(); - $body = $storage->get_raw_headers($msg->uid, $part_id); - $body .= $storage->get_raw_body($msg->uid, null, $part_id); + $body = $storage->get_raw_headers($msg->uid, $part->mime_id); + $body .= $storage->get_raw_body($msg->uid, null, $part->mime_id); } else { - $body = $msg->get_part_body($part_id, false); + $body = $msg->get_part_body($part->mime_id, false); } return $body; @@ -1046,18 +1141,25 @@ /** * Replace message encrypted structure with decrypted message structure * - * @param array - * @param rcube_message_part + * @param array Hook arguments + * @param rcube_message_part Part structure + * @param int Part size */ - private function modify_structure(&$p, $struct) + private function modify_structure(&$p, $struct, $size = 0) { // modify mime_parts property of the message object $old_id = $p['structure']->mime_id; + foreach (array_keys($p['object']->mime_parts) as $idx) { if (!$old_id || $idx == $old_id || strpos($idx, $old_id . '.') === 0) { unset($p['object']->mime_parts[$idx]); } } + + // set some part params used by Roundcube core + $struct->headers = array_merge($p['structure']->headers, $struct->headers); + $struct->size = $size; + $struct->filename = $p['structure']->filename; // modify the new structure to be correctly handled by Roundcube $this->modify_structure_part($struct, $p['object'], $old_id); @@ -1086,7 +1188,6 @@ // Cache the fact it was decrypted $this->encrypted_parts[] = $part->mime_id; - $msg->mime_parts[$part->mime_id] = $part; // modify sub-parts @@ -1096,6 +1197,33 @@ } /** + * Extracts body and signature of multipart/signed message body + */ + private function explode_signed_body($body, $boundary) + { + if (!$body) { + return array(); + } + + $boundary = '--' . $boundary; + $boundary_len = strlen($boundary) + 2; + + // Find boundaries + $start = strpos($body, $boundary) + $boundary_len; + $end = strpos($body, $boundary, $start); + + // Get signed body and signature + $sig = substr($body, $end + $boundary_len); + $body = substr($body, $start, $end - $start - 2); + + // Cleanup signature + $sig = substr($sig, strpos($sig, "\r\n\r\n") + 4); + $sig = substr($sig, 0, strpos($sig, $boundary)); + + return array($body, $sig); + } + + /** * Checks if specified message part is a PGP-key or S/MIME cert data * * @param rcube_message_part Part object -- Gitblit v1.9.1