diff --git a/CHANGELOG b/CHANGELOG index ac87bffe3..cc1371698 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ CHANGELOG Roundcube Webmail =========================== +- Enigma: Implement PGP encryption and signing in one go (#5302) +- Enigma: Display signature verification status for encrypted+signed messages (#5302) - Display different attachment icon on encrypted messages - Display different confirmation text when moving messages to Trash (#5220) - Indicate that a collapsed thread has flagged children (#5013) diff --git a/plugins/enigma/lib/enigma_driver.php b/plugins/enigma/lib/enigma_driver.php index 7fa334298..f3b117bdb 100644 --- a/plugins/enigma/lib/enigma_driver.php +++ b/plugins/enigma/lib/enigma_driver.php @@ -32,22 +32,27 @@ abstract class enigma_driver abstract function init(); /** - * Encryption. + * Encryption (and optional signing). * * @param string Message body * @param array List of key-password mapping + * @param string Optional signing Key ID + * @param string Optional signing Key password * * @return mixed Encrypted message or enigma_error on failure */ - abstract function encrypt($text, $keys); + abstract function encrypt($text, $keys, $sign_key = null, $sign_pass = null); /** - * Decryption. + * Decryption (and sig verification if sig exists). * - * @param string Encrypted message - * @param array List of key-password mapping + * @param string Encrypted message + * @param array List of key-password mapping + * @param enigma_signature Signature information (if available) + * + * @return mixed Decrypted message or enigma_error on failure */ - abstract function decrypt($text, $keys = array()); + abstract function decrypt($text, $keys = array(), &$signature = null); /** * Signing. diff --git a/plugins/enigma/lib/enigma_driver_gnupg.php b/plugins/enigma/lib/enigma_driver_gnupg.php index 2732f489d..9cd894546 100644 --- a/plugins/enigma/lib/enigma_driver_gnupg.php +++ b/plugins/enigma/lib/enigma_driver_gnupg.php @@ -95,20 +95,27 @@ class enigma_driver_gnupg extends enigma_driver } /** - * Encryption. + * Encryption (and optional signing). * * @param string Message body * @param array List of key-password mapping + * @param string Optional signing Key ID + * @param string Optional signing Key password * * @return mixed Encrypted message or enigma_error on failure */ - function encrypt($text, $keys) + function encrypt($text, $keys, $sign_key = null, $sign_pass = null) { try { foreach ($keys as $key) { $this->gpg->addEncryptKey($key); } + if ($sign_key) { + $this->gpg->addSignKey($sign_key, $sign_pass); + return $this->gpg->encryptAndSign($text, true); + } + return $this->gpg->encrypt($text, true); } catch (Exception $e) { @@ -117,21 +124,28 @@ class enigma_driver_gnupg extends enigma_driver } /** - * Decrypt a message + * Decrypt a message (and verify if signature found) * - * @param string Encrypted message - * @param array List of key-password mapping + * @param string Encrypted message + * @param array List of key-password mapping + * @param enigma_signature Signature information (if available) * * @return mixed Decrypted message or enigma_error on failure */ - function decrypt($text, $keys = array()) + function decrypt($text, $keys = array(), &$signature = null) { try { foreach ($keys as $key => $password) { $this->gpg->addDecryptKey($key, $password); } - return $this->gpg->decrypt($text); + $result = $this->gpg->decryptAndVerify($text); + + if (!empty($result['signatures'])) { + $signature = $this->parse_signature($result['signatures'][0]); + } + + return $result['data']; } catch (Exception $e) { return $this->get_error_from_exception($e); diff --git a/plugins/enigma/lib/enigma_driver_phpssl.php b/plugins/enigma/lib/enigma_driver_phpssl.php index a5aa6de13..26be2cfaf 100644 --- a/plugins/enigma/lib/enigma_driver_phpssl.php +++ b/plugins/enigma/lib/enigma_driver_phpssl.php @@ -67,11 +67,11 @@ class enigma_driver_phpssl extends enigma_driver } - function encrypt($text, $keys) + function encrypt($text, $keys, $sign_key = null, $sign_pass = null) { } - function decrypt($text, $keys = array()) + function decrypt($text, $keys = array(), &$signature = null) { } @@ -142,14 +142,6 @@ class enigma_driver_phpssl extends enigma_driver { } - public function delete_privkey($keyid) - { - } - - public function delete_pubkey($keyid) - { - } - /** * Converts Crypt_GPG_Key object into Enigma's key object * @@ -225,5 +217,4 @@ class enigma_driver_phpssl extends enigma_driver return $data; } - } diff --git a/plugins/enigma/lib/enigma_engine.php b/plugins/enigma/lib/enigma_engine.php index 413ec22e5..47d1d1df9 100644 --- a/plugins/enigma/lib/enigma_engine.php +++ b/plugins/enigma/lib/enigma_engine.php @@ -38,10 +38,11 @@ class enigma_engine const SIGN_MODE_BODY = 1; const SIGN_MODE_SEPARATE = 2; - const SIGN_MODE_MIME = 3; + const SIGN_MODE_MIME = 4; const ENCRYPT_MODE_BODY = 1; const ENCRYPT_MODE_MIME = 2; + const ENCRYPT_MODE_SIGN = 4; /** @@ -244,7 +245,28 @@ class enigma_engine $mime = new enigma_mime_message($message, enigma_mime_message::PGP_ENCRYPTED); // always use sender's key - $recipients = array($mime->getFromAddress()); + $from = $mime->getFromAddress(); + + // check senders key for signing + if ($mode & self::ENCRYPT_MODE_SIGN) { + $sign_key = $this->find_key($from, true); + + if (empty($sign_key)) { + return new enigma_error(enigma_error::KEYNOTFOUND); + } + + // check if we have password for this key + $passwords = $this->get_passwords(); + $sign_pass = $passwords[$sign_key->id]; + + if ($sign_pass === null) { + // ask for password + $error = array('missing' => array($sign_key->id => $sign_key->name)); + return new enigma_error(enigma_error::BADPASS, '', $error); + } + } + + $recipients = array($from); // if it's not a draft we add all recipients' keys if (!$is_draft) { @@ -271,16 +293,13 @@ class enigma_engine } // select mode - switch ($mode) { - case self::ENCRYPT_MODE_BODY: + if ($mode & self::ENCRYPT_MODE_BODY) { $encrypt_mode = $mode; - break; - - case self::ENCRYPT_MODE_MIME: + } + else if ($mode & self::ENCRYPT_MODE_MIME) { $encrypt_mode = $mode; - break; - - default: + } + else { $encrypt_mode = $mime->isMultipart() ? self::ENCRYPT_MODE_MIME : self::ENCRYPT_MODE_BODY; } @@ -296,9 +315,15 @@ class enigma_engine } // sign the body - $result = $this->pgp_encrypt($body, $keys); + $result = $this->pgp_encrypt($body, $keys, $sign_key ? $sign_key->id : null, $sign_pass); if ($result !== true) { + if ($result->getCode() == enigma_error::BADPASS) { + // ask for password + $error = array('bad' => array($sign_key->id => $sign_key->name)); + return new enigma_error(enigma_error::BADPASS, '', $error); + } + return $result; } @@ -678,11 +703,16 @@ class enigma_engine $part = $p['structure']; // Decrypt - $result = $this->pgp_decrypt($body); + $result = $this->pgp_decrypt($body, $signature); // Store decryption status $this->decryptions[$part->mime_id] = $result; + // Store signature data for display + if ($signature) { + $this->signatures[$part->mime_id] = $signature; + } + // find parent part ID if (strpos($part->mime_id, '.')) { $items = explode('.', $part->mime_id); @@ -764,7 +794,7 @@ class enigma_engine $body = $this->get_part_body($p['object'], $part); // Decrypt - $result = $this->pgp_decrypt($body); + $result = $this->pgp_decrypt($body, $signature); if ($result === true) { // Parse decrypted message @@ -784,6 +814,9 @@ class enigma_engine $this->decryptions[$struct->mime_id] = $result; foreach ((array) $struct->parts as $sp) { $this->decryptions[$sp->mime_id] = $result; + if ($signature) { + $this->signatures[$sp->mime_id] = $signature; + } } } else { @@ -839,15 +872,16 @@ class enigma_engine /** * PGP message decryption. * - * @param mixed Message body + * @param mixed &$msg_body Message body + * @param enigma_signature &$signature Signature verification result * * @return mixed True or enigma_error */ - private function pgp_decrypt(&$msg_body) + private function pgp_decrypt(&$msg_body, &$signature = null) { // @TODO: Handle big bodies using (temp) files $keys = $this->get_passwords(); - $result = $this->pgp_driver->decrypt($msg_body, $keys); + $result = $this->pgp_driver->decrypt($msg_body, $keys, $signature); if ($result instanceof enigma_error) { $err_code = $result->getCode(); @@ -899,15 +933,17 @@ class enigma_engine /** * PGP message encrypting * - * @param mixed Message body - * @param array Keys + * @param mixed Message body + * @param array Keys + * @param string Optional signing Key ID + * @param string Optional signing Key password * * @return mixed True or enigma_error */ - private function pgp_encrypt(&$msg_body, $keys) + private function pgp_encrypt(&$msg_body, $keys, $sign_key = null, $sign_pass = null) { // @TODO: Handle big bodies using (temp) files - $result = $this->pgp_driver->encrypt($msg_body, $keys); + $result = $this->pgp_driver->encrypt($msg_body, $keys, $sign_key, $sign_pass); if ($result instanceof enigma_error) { $err_code = $result->getCode(); @@ -917,6 +953,7 @@ class enigma_engine 'file' => __FILE__, 'line' => __LINE__, 'message' => "Enigma plugin: " . $result->getMessage() ), true, false); + return $result; } diff --git a/plugins/enigma/lib/enigma_ui.php b/plugins/enigma/lib/enigma_ui.php index 3fa0f82e3..b8193eb33 100644 --- a/plugins/enigma/lib/enigma_ui.php +++ b/plugins/enigma/lib/enigma_ui.php @@ -776,8 +776,9 @@ class enigma_ui return $p; } - $engine = $this->enigma->engine; - $part_id = $p['part']->mime_id; + $engine = $this->enigma->engine; + $part_id = $p['part']->mime_id; + $messages = array(); // Decryption status if (($found = $this->find_part_id($part_id, $engine->decryptions)) !== null @@ -818,7 +819,8 @@ class enigma_ui $msg = rcube::Q($this->enigma->gettext('decryptok')); } - $p['prefix'] .= html::div($attrib, $msg); + $attrib['msg'] = $msg; + $messages[] = $attrib; } // Signature verification status @@ -869,7 +871,19 @@ class enigma_ui // test // $msg .= '
'.$sig->body.'
'; - $p['prefix'] .= html::div($attrib, $msg); + $attrib['msg'] = $msg; + $messages[] = $attrib; + } + + if ($count = count($messages)) { + if ($count == 2 && $messages[0]['class'] == $messages[1]['class']) { + $p['prefix'] .= html::div($messages[0], $messages[0]['msg'] . ' ' . $messages[1]['msg']); + } + else { + foreach ($messages as $msg) { + $p['prefix'] .= html::div($msg, $msg['msg']); + } + } } if ($attach_scripts) { @@ -972,16 +986,16 @@ class enigma_ui $engine->attach_public_key($p['message']); } - if (!$savedraft && $sign_enable) { + if ($encrypt_enable) { $engine = $this->enigma->load_engine(); - $status = $engine->sign_message($p['message']); - $mode = 'sign'; + $mode = !$savedraft && $sign_enable ? enigma_engine::ENCRYPT_MODE_SIGN : null; + $status = $engine->encrypt_message($p['message'], $mode, $savedraft); + $mode = 'encrypt'; } - - if ((!$status instanceof enigma_error) && $encrypt_enable) { + else if (!$savedraft && $sign_enable) { $engine = $this->enigma->load_engine(); - $status = $engine->encrypt_message($p['message'], null, $savedraft); - $mode = 'encrypt'; + $status = $engine->sign_message($p['message']); + $mode = 'sign'; } if ($mode && ($status instanceof enigma_error)) {