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