From 9d78c68cbf5ce86f02e8fa16814293cdfd204432 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Wed, 29 Jul 2015 14:38:21 -0400
Subject: [PATCH] Fix so imap folder attribute comparisons are case-insensitive (#1490466) + make in_array_nocase() much faster for ASCII strings
---
plugins/enigma/lib/enigma_engine.php | 463 ++++++++++++++++++++++++++++++++++++++++++++++++---------
1 files changed, 388 insertions(+), 75 deletions(-)
diff --git a/plugins/enigma/lib/enigma_engine.php b/plugins/enigma/lib/enigma_engine.php
index c3a2e50..85c2882 100644
--- a/plugins/enigma/lib/enigma_engine.php
+++ b/plugins/enigma/lib/enigma_engine.php
@@ -1,45 +1,46 @@
<?php
-/*
+
+/**
+-------------------------------------------------------------------------+
| Engine of the Enigma Plugin |
| |
- | This program is free software; you can redistribute it and/or modify |
- | it under the terms of the GNU General Public License version 2 |
- | as published by the Free Software Foundation. |
+ | Copyright (C) 2010-2015 The Roundcube Dev Team |
| |
- | This program is distributed in the hope that it will be useful, |
- | but WITHOUT ANY WARRANTY; without even the implied warranty of |
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
- | GNU General Public License for more details. |
- | |
- | You should have received a copy of the GNU General Public License along |
- | with this program; if not, write to the Free Software Foundation, Inc., |
- | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
| |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
-
*/
-/*
- 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 $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;
+ const SIGN_MODE_MIME = 3;
+
+ const ENCRYPT_MODE_BODY = 1;
+ const ENCRYPT_MODE_MIME = 2;
/**
@@ -50,8 +51,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();
+ }
}
/**
@@ -125,6 +130,175 @@
}
/**
+ * Handler for message signing
+ *
+ * @param Mail_mime Original message
+ * @param int Encryption mode
+ *
+ * @return enigma_error On error returns error object
+ */
+ function sign_message(&$message, $mode = null)
+ {
+ $mime = new enigma_mime_message($message, enigma_mime_message::PGP_SIGNED);
+ $from = $mime->getFromAddress();
+
+ // find private key
+ $key = $this->find_key($from, true);
+
+ if (empty($key)) {
+ return new enigma_error(enigma_error::E_KEYNOTFOUND);
+ }
+
+ // check if we have password for this key
+ $passwords = $this->get_passwords();
+ $pass = $passwords[$key->id];
+
+ if ($pass === null) {
+ // ask for password
+ $error = array('missing' => array($key->id => $key->name));
+ return new enigma_error(enigma_error::E_BADPASS, '', $error);
+ }
+
+ // select mode
+ switch ($mode) {
+ case self::SIGN_MODE_BODY:
+ $pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
+ break;
+
+ case self::SIGN_MODE_MIME:
+ $pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
+ break;
+/*
+ case self::SIGN_MODE_SEPARATE:
+ $pgp_mode = Crypt_GPG::SIGN_MODE_NORMAL;
+ break;
+*/
+ default:
+ if ($mime->isMultipart()) {
+ $pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
+ }
+ else {
+ $pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
+ }
+ }
+
+ // get message body
+ if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
+ // in this mode we'll replace text part
+ // with the one containing signature
+ $body = $message->getTXTBody();
+ }
+ else {
+ // here we'll build PGP/MIME message
+ $body = $mime->getOrigBody();
+ }
+
+ // sign the body
+ $result = $this->pgp_sign($body, $key->id, $pass, $pgp_mode);
+
+ if ($result !== true) {
+ if ($result->getCode() == enigma_error::E_BADPASS) {
+ // ask for password
+ $error = array('missing' => array($key->id => $key->name));
+ return new enigma_error(enigma_error::E_BADPASS, '', $error);
+ }
+
+ return $result;
+ }
+
+ // replace message body
+ if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
+ $message->setTXTBody($body);
+ }
+ else {
+ $mime->addPGPSignature($body);
+ $message = $mime;
+ }
+ }
+
+ /**
+ * Handler for message encryption
+ *
+ * @param Mail_mime Original message
+ * @param int Encryption mode
+ * @param bool Is draft-save action - use only sender's key for encryption
+ *
+ * @return enigma_error On error returns error object
+ */
+ function encrypt_message(&$message, $mode = null, $is_draft = false)
+ {
+ $mime = new enigma_mime_message($message, enigma_mime_message::PGP_ENCRYPTED);
+
+ // always use sender's key
+ $recipients = array($mime->getFromAddress());
+
+ // if it's not a draft we add all recipients' keys
+ if (!$is_draft) {
+ $recipients = array_merge($recipients, $mime->getRecipients());
+ }
+
+ if (empty($recipients)) {
+ return new enigma_error(enigma_error::E_KEYNOTFOUND);
+ }
+
+ $recipients = array_unique($recipients);
+
+ // find recipient public keys
+ foreach ((array) $recipients as $email) {
+ $key = $this->find_key($email);
+
+ if (empty($key)) {
+ return new enigma_error(enigma_error::E_KEYNOTFOUND, '', array(
+ 'missing' => $email
+ ));
+ }
+
+ $keys[] = $key->id;
+ }
+
+ // select mode
+ switch ($mode) {
+ case self::ENCRYPT_MODE_BODY:
+ $encrypt_mode = $mode;
+ break;
+
+ case self::ENCRYPT_MODE_MIME:
+ $encrypt_mode = $mode;
+ break;
+
+ default:
+ $encrypt_mode = $mime->isMultipart() ? self::ENCRYPT_MODE_MIME : self::ENCRYPT_MODE_BODY;
+ }
+
+ // get message body
+ if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
+ // in this mode we'll replace text part
+ // with the one containing encrypted message
+ $body = $message->getTXTBody();
+ }
+ else {
+ // here we'll build PGP/MIME message
+ $body = $mime->getOrigBody();
+ }
+
+ // sign the body
+ $result = $this->pgp_encrypt($body, $keys);
+
+ if ($result !== true) {
+ return $result;
+ }
+
+ // replace message body
+ if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
+ $message->setTXTBody($body);
+ }
+ else {
+ $mime->setPGPEncryptedBody($body);
+ $message = $mime;
+ }
+ }
+
+ /**
* Handler for message_part_structure hook.
* Called for every part of the message.
*
@@ -161,6 +335,8 @@
{
// encrypted attachment, see parse_plain_encrypted()
if ($p['part']->need_decryption && $p['part']->body === null) {
+ $this->load_pgp_driver();
+
$storage = $this->rc->get_storage();
$body = $storage->get_message_part($p['object']->uid, $p['part']->mime_id, $p['part'], null, null, true, 0, false);
$result = $this->pgp_decrypt($body);
@@ -186,7 +362,7 @@
$part = $p['structure'];
// exit, if we're already inside a decrypted message
- if ($part->encrypted) {
+ if (in_array($part->mime_id, $this->encrypted_parts)) {
return;
}
@@ -273,7 +449,9 @@
// Verify signature
if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
- $sig = $this->pgp_verify($body);
+ if ($this->rc->config->get('enigma_signatures', true)) {
+ $sig = $this->pgp_verify($body);
+ }
}
// @TODO: Handle big bodies using (temp) files
@@ -309,7 +487,7 @@
// Store signature data for display
if (!empty($sig)) {
$this->signed_parts[$part->mime_id] = $part->mime_id;
- $this->signatures[$part->mime_id] = $sig;
+ $this->signatures[$part->mime_id] = $sig;
}
fclose($fh);
@@ -319,41 +497,43 @@
* Handler for PGP/MIME signed message.
* Verifies signature.
*
- * @param array Reference to hook's parameters
+ * @param array Reference to hook's parameters
*/
private function parse_pgp_signed(&$p)
{
- // Verify signature
- if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
- $this->load_pgp_driver();
- $struct = $p['structure'];
+ if (!$this->rc->config->get('enigma_signatures', true)) {
+ return;
+ }
- $msg_part = $struct->parts[0];
- $sig_part = $struct->parts[1];
+ if ($this->rc->action != 'show' && $this->rc->action != 'preview') {
+ return;
+ }
- // 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);
+ $this->load_pgp_driver();
+ $struct = $p['structure'];
- // Verify
- $sig = $this->pgp_verify($msg_body, $sig_body);
+ $msg_part = $struct->parts[0];
+ $sig_part = $struct->parts[1];
- // Store signature data for display
- $this->signatures[$struct->mime_id] = $sig;
+ // 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);
- // 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;
- }
+ // Verify
+ $sig = $this->pgp_verify($msg_body, $sig_body);
- // Remove signature file from attachments list (?)
- unset($struct->parts[1]);
+ // 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;
}
}
@@ -366,6 +546,10 @@
private function parse_smime_signed(&$p)
{
return; // @TODO
+
+ if (!$this->rc->config->get('enigma_signatures', true)) {
+ return;
+ }
// Verify signature
if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
@@ -388,9 +572,6 @@
else {
$this->signed_parts[$msg_part->mime_id] = $struct->mime_id;
}
-
- // Remove signature file from attachments list
- unset($struct->parts[1]);
}
}
@@ -402,6 +583,10 @@
*/
private function parse_plain_encrypted(&$p, $body)
{
+ if (!$this->rc->config->get('enigma_decryption', true)) {
+ return;
+ }
+
$this->load_pgp_driver();
$part = $p['structure'];
@@ -411,26 +596,33 @@
// Store decryption status
$this->decryptions[$part->mime_id] = $result;
+ // find parent part ID
+ if (strpos($part->mime_id, '.')) {
+ $items = explode('.', $part->mime_id);
+ array_pop($items);
+ $parent = implode('.', $items);
+ }
+ else {
+ $parent = 0;
+ }
+
// Parse decrypted message
if ($result === true) {
$part->body = $body;
$part->body_modified = true;
- $part->encrypted = true;
+
+ // Remember it was decrypted
+ $this->encrypted_parts[] = $part->mime_id;
+
+ // PGP signed inside? verify signature
+ if (preg_match('/^-----BEGIN PGP SIGNED MESSAGE-----/', $body)) {
+ $this->parse_plain_signed($p, $body);
+ }
// Encrypted plain message may contain encrypted attachments
- // in such case attachments have .pgp extension and application/octet-stream.
+ // in such case attachments have .pgp extension and type application/octet-stream.
// This is what happens when you select "Encrypt each attachment separately
// and send the message using inline PGP" in Thunderbird's Enigmail.
-
- // find parent part ID
- if (strpos($part->mime_id, '.')) {
- $items = explode('.', $part->mime_id);
- array_pop($items);
- $parent = implode('.', $items);
- }
- else {
- $parent = 0;
- }
if ($p['object']->mime_parts[$parent]) {
foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
@@ -447,6 +639,19 @@
}
}
}
+ // decryption failed, but the message may have already
+ // been cached with the modified parts (see above),
+ // let's bring the original state back
+ else if ($p['object']->mime_parts[$parent]) {
+ foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
+ if ($p->need_decryption && !preg_match('/^(.*)\.pgp$/i', $p->filename, $m)) {
+ // modify filename
+ $p->filename .= '.pgp';
+ // flag the part, it will be decrypted when needed
+ unset($p->need_decryption);
+ }
+ }
+ }
}
/**
@@ -456,6 +661,10 @@
*/
private function parse_pgp_encrypted(&$p)
{
+ if (!$this->rc->config->get('enigma_decryption', true)) {
+ return;
+ }
+
$this->load_pgp_driver();
$struct = $p['structure'];
@@ -496,6 +705,10 @@
*/
private function parse_smime_encrypted(&$p)
{
+ if (!$this->rc->config->get('enigma_decryption', true)) {
+ return;
+ }
+
// $this->load_smime_driver();
}
@@ -510,7 +723,6 @@
private function pgp_verify(&$msg_body, $sig_body=null)
{
// @TODO: Handle big bodies using (temp) files
- // @TODO: caching of verification result
$sig = $this->pgp_driver->verify($msg_body, $sig_body);
if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::E_KEYNOTFOUND)
@@ -533,9 +745,68 @@
private function pgp_decrypt(&$msg_body)
{
// @TODO: Handle big bodies using (temp) files
- // @TODO: caching of verification result
$keys = $this->get_passwords();
$result = $this->pgp_driver->decrypt($msg_body, $keys);
+
+ if ($result instanceof enigma_error) {
+ $err_code = $result->getCode();
+ if (!in_array($err_code, array(enigma_error::E_KEYNOTFOUND, enigma_error::E_BADPASS)))
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Enigma plugin: " . $result->getMessage()
+ ), true, false);
+ return $result;
+ }
+
+ $msg_body = $result;
+
+ return true;
+ }
+
+ /**
+ * PGP message signing
+ *
+ * @param mixed Message body
+ * @param string Key ID
+ * @param string Key passphrase
+ * @param int Signing mode
+ *
+ * @return mixed True or enigma_error
+ */
+ private function pgp_sign(&$msg_body, $keyid, $password, $mode = null)
+ {
+ // @TODO: Handle big bodies using (temp) files
+ $result = $this->pgp_driver->sign($msg_body, $keyid, $password, $mode);
+
+ if ($result instanceof enigma_error) {
+ $err_code = $result->getCode();
+ if (!in_array($err_code, array(enigma_error::E_KEYNOTFOUND, enigma_error::E_BADPASS)))
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Enigma plugin: " . $result->getMessage()
+ ), true, false);
+ return $result;
+ }
+
+ $msg_body = $result;
+
+ return true;
+ }
+
+ /**
+ * PGP message encrypting
+ *
+ * @param mixed Message body
+ * @param array Keys
+ *
+ * @return mixed True or enigma_error
+ */
+ private function pgp_encrypt(&$msg_body, $keys)
+ {
+ // @TODO: Handle big bodies using (temp) files
+ $result = $this->pgp_driver->encrypt($msg_body, $keys);
if ($result instanceof enigma_error) {
$err_code = $result->getCode();
@@ -574,6 +845,39 @@
}
return $result;
+ }
+
+ /**
+ * Find PGP private/public key
+ *
+ * @param string E-mail address
+ * @param bool Need a key for signing?
+ *
+ * @return enigma_key The key
+ */
+ function find_key($email, $can_sign = false)
+ {
+ $this->load_pgp_driver();
+ $result = $this->pgp_driver->list_keys($email);
+
+ 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;
+ }
+
+ $mode = $can_sign ? enigma_key::CAN_SIGN : enigma_key::CAN_ENCRYPT;
+
+ // check key validity and type
+ foreach ($result as $key) {
+ if ($keyid = $key->find_subkey($email, $mode)) {
+ return $key;
+ }
+ }
}
/**
@@ -675,6 +979,9 @@
$this->rc->output->send();
}
+ /**
+ * 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);
@@ -685,6 +992,9 @@
}
}
+ /**
+ * Saves key/cert password in user session
+ */
function save_password($keyid, $password)
{
// we store passwords in session for specified time
@@ -698,6 +1008,9 @@
$_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
}
+ /**
+ * Returns currently stored passwords
+ */
function get_passwords()
{
if ($config = $_SESSION['enigma_pass']) {
@@ -705,12 +1018,12 @@
$config = @unserialize($config);
}
- $threshold = time() - self::PASSWORD_TIME;
+ $threshold = time() - $this->password_time;
$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;
}
@@ -802,14 +1115,14 @@
$part->body_modified = true;
$part->encoding = 'stream';
- // Cache the fact it was decrypted
- $part->encrypted = true;
-
// modify part identifier
if ($old_id) {
$part->mime_id = !$part->mime_id ? $old_id : ($old_id . '.' . $part->mime_id);
}
+ // Cache the fact it was decrypted
+ $this->encrypted_parts[] = $part->mime_id;
+
$msg->mime_parts[$part->mime_id] = $part;
// modify sub-parts
--
Gitblit v1.9.1