From 007c9ddcb0c49733522688c4d2364d1a4ab9ec8e Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Wed, 30 Mar 2016 06:56:44 -0400 Subject: [PATCH] Enigma: Handle messages with text before an encrypted block (#5149) --- CHANGELOG | 1 plugins/enigma/lib/enigma_ui.php | 9 ++ plugins/enigma/localization/en_US.inc | 2 plugins/enigma/lib/enigma_signature.php | 4 + plugins/enigma/lib/enigma_engine.php | 126 ++++++++++++++++++++++++++++++++--------- 5 files changed, 112 insertions(+), 30 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3da1c49..d1958b1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Enigma: Handle messages with text before an encrypted block (#5149) - Enigma: Handle encrypted/signed content inside message/rfc822 attachments - Enigma: Fix missing html/plain switch on multipart/signed messages (#1490649) - Enigma: Disable format=flowed for signed plain text messages (#1490646) diff --git a/plugins/enigma/lib/enigma_engine.php b/plugins/enigma/lib/enigma_engine.php index 9ce1761..96f792d 100644 --- a/plugins/enigma/lib/enigma_engine.php +++ b/plugins/enigma/lib/enigma_engine.php @@ -4,7 +4,7 @@ +-------------------------------------------------------------------------+ | Engine of the Enigma Plugin | | | - | Copyright (C) 2010-2015 The Roundcube Dev Team | + | Copyright (C) 2010-2016 The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -33,6 +33,8 @@ public $decryptions = array(); public $signatures = array(); public $encrypted_parts = array(); + + const ENCRYPTED_PARTIALLY = 100; const SIGN_MODE_BODY = 1; const SIGN_MODE_SEPARATE = 2; @@ -385,14 +387,74 @@ $body = $this->get_part_body($p['object'], $part); } - // @TODO: big message body could be a file resource - // PGP signed message - if (preg_match('/^-----BEGIN PGP SIGNED MESSAGE-----/', $body)) { - $this->parse_plain_signed($p, $body); + // In this way we can use fgets on string as on file handle + // Don't use php://temp for security (body may come from an encrypted part) + $fd = fopen('php://memory', 'r+'); + if (!$fd) { + return; } - // PGP encrypted message - else if (preg_match('/^-----BEGIN PGP MESSAGE-----/', $body)) { - $this->parse_plain_encrypted($p, $body); + + fwrite($fd, $body); + rewind($fd); + + $body = ''; + $prefix = ''; + $mode = ''; + $tokens = array( + 'BEGIN PGP SIGNED MESSAGE' => 'signed-start', + 'END PGP SIGNATURE' => 'signed-end', + 'BEGIN PGP MESSAGE' => 'encrypted-start', + 'END PGP MESSAGE' => 'encrypted-end', + ); + $regexp = '/^-----(' . implode('|', array_keys($tokens)) . ')-----[\r\n]*/'; + + while (($line = fgets($fd)) !== false) { + if ($line[0] === '-' && $line[4] === '-' && preg_match($regexp, $line, $m)) { + switch ($tokens[$m[1]]) { + case 'signed-start': + $body = $line; + $mode = 'signed'; + break; + + case 'signed-end': + if ($mode === 'signed') { + $body .= $line; + } + break 2; // ignore anything after this line + + case 'encrypted-start': + $body = $line; + $mode = 'encrypted'; + break; + + case 'encrypted-end': + if ($mode === 'encrypted') { + $body .= $line; + } + break 2; // ignore anything after this line + } + + continue; + } + + if ($mode === 'signed') { + $body .= $line; + } + else if ($mode === 'encrypted') { + $body .= $line; + } + else { + $prefix .= $line; + } + } + + fclose($fd); + + if ($mode === 'signed') { + $this->parse_plain_signed($p, $body, $prefix); + } + else if ($mode === 'encrypted') { + $this->parse_plain_encrypted($p, $body, $prefix); } } @@ -456,8 +518,9 @@ * * @param array Reference to hook's parameters * @param string Message (part) body + * @param string Body prefix (additional text before the encrypted block) */ - private function parse_plain_signed(&$p, $body) + private function parse_plain_signed(&$p, $body, $prefix = '') { if (!$this->rc->config->get('enigma_signatures', true)) { return; @@ -471,23 +534,21 @@ $sig = $this->pgp_verify($body); } - // @TODO: Handle big bodies using (temp) files - // In this way we can use fgets on string as on file handle - $fh = fopen('php://memory', 'br+'); - // @TODO: fopen/fwrite errors handling - if ($fh) { - fwrite($fh, $body); - rewind($fh); + // Don't use php://temp for security (body may come from an encrypted part) + $fd = fopen('php://memory', 'r+'); + if (!$fd) { + return; } + + fwrite($fd, $body); + rewind($fd); $body = $part->body = null; $part->body_modified = true; // Extract body (and signature?) - while (!feof($fh)) { - $line = fgets($fh, 1024); - + while (($line = fgets($fd, 1024)) !== false) { if ($part->body === null) $part->body = ''; else if (preg_match('/^-----BEGIN PGP SIGNATURE-----/', $line)) @@ -496,17 +557,22 @@ $part->body .= $line; } + fclose($fd); + // Remove "Hash" Armor Headers $part->body = preg_replace('/^.*\r*\n\r*\n/', '', $part->body); // de-Dash-Escape (RFC2440) $part->body = preg_replace('/(^|\n)- -/', '\\1-', $part->body); - // Store signature data for display - if (!empty($sig)) { - $this->signatures[$part->mime_id] = $sig; + if ($prefix) { + $part->body = $prefix . $part->body; } - fclose($fh); + // Store signature data for display + if (!empty($sig)) { + $sig->partial = !empty($prefix); + $this->signatures[$part->mime_id] = $sig; + } } /** @@ -573,8 +639,9 @@ * * @param array Reference to hook's parameters * @param string Message (part) body + * @param string Body prefix (additional text before the encrypted block) */ - private function parse_plain_encrypted(&$p, $body) + private function parse_plain_encrypted(&$p, $body, $prefix = '') { if (!$this->rc->config->get('enigma_decryption', true)) { return; @@ -601,15 +668,18 @@ // Parse decrypted message if ($result === true) { - $part->body = $body; + $part->body = $prefix . $body; $part->body_modified = true; + + // it maybe PGP signed inside, verify signature + $this->parse_plain($p, $body); // 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); + // Inform the user that only a part of the body was encrypted + if ($prefix) { + $this->decryptions[$part->mime_id] = self::ENCRYPTED_PARTIALLY; } // Encrypted plain message may contain encrypted attachments diff --git a/plugins/enigma/lib/enigma_signature.php b/plugins/enigma/lib/enigma_signature.php index 4207435..8e9f1c9 100644 --- a/plugins/enigma/lib/enigma_signature.php +++ b/plugins/enigma/lib/enigma_signature.php @@ -25,4 +25,8 @@ public $name; public $comment; public $email; + + // Set it to true if signature is valid, but part of the message + // was out of the signed block + public $partial; } diff --git a/plugins/enigma/lib/enigma_ui.php b/plugins/enigma/lib/enigma_ui.php index ca28c32..6ae69da 100644 --- a/plugins/enigma/lib/enigma_ui.php +++ b/plugins/enigma/lib/enigma_ui.php @@ -791,6 +791,10 @@ $msg = rcube::Q($this->enigma->gettext('decrypterror')); } } + else if ($status === enigma_engine::ENCRYPTED_PARTIALLY) { + $attrib['class'] = 'enigmawarning'; + $msg = rcube::Q($this->enigma->gettext('decryptpartial')); + } else { $attrib['class'] = 'enigmanotice'; $msg = rcube::Q($this->enigma->gettext('decryptok')); @@ -821,8 +825,9 @@ $msg = rcube::Q($msg); } else if ($sig->valid) { - $attrib['class'] = 'enigmanotice'; - $msg = rcube::Q(str_replace('$sender', $sender, $this->enigma->gettext('sigvalid'))); + $attrib['class'] = $sig->partial ? 'enigmawarning' : 'enigmanotice'; + $label = 'sigvalid' . ($sig->partial ? 'partial' : ''); + $msg = rcube::Q(str_replace('$sender', $sender, $this->enigma->gettext($label))); } else { $attrib['class'] = 'enigmawarning'; diff --git a/plugins/enigma/localization/en_US.inc b/plugins/enigma/localization/en_US.inc index 817b323..0e4d2b4 100644 --- a/plugins/enigma/localization/en_US.inc +++ b/plugins/enigma/localization/en_US.inc @@ -85,6 +85,7 @@ $messages = array(); $messages['sigvalid'] = 'Verified signature from $sender.'; +$messages['sigvalidpartial'] = 'Verified signature from $sender, but part of the body was not signed.'; $messages['siginvalid'] = 'Invalid signature from $sender.'; $messages['sigunverified'] = 'Unverified signature. Certificate not verified. Certificate ID: $keyid.'; $messages['signokey'] = 'Unverified signature. Public key not found. Key ID: $keyid.'; @@ -94,6 +95,7 @@ $messages['decryptnokey'] = 'Decryption failed. Private key not found. Key ID: $keyid.'; $messages['decryptbadpass'] = 'Decryption failed. Bad password.'; $messages['decryptnopass'] = 'Decryption failed. Key password required.'; +$messages['decryptpartial'] = 'Message decrypted, but part of the body was not encrypted.'; $messages['signerror'] = 'Signing failed.'; $messages['signnokey'] = 'Signing failed. Private key not found.'; $messages['signbadpass'] = 'Signing failed. Bad password.'; -- Gitblit v1.9.1