Aleksander Machniak
2016-02-06 5d49af37cc81c4fada69169c23cc12a5f9c4ce2e
commit | author | age
48e9c1 1 <?php
58c279 2
AM 3 /**
48e9c1 4  +-------------------------------------------------------------------------+
T 5  | Engine of the Enigma Plugin                                             |
6  |                                                                         |
a99c34 7  | Copyright (C) 2010-2015 The Roundcube Dev Team                          |
48e9c1 8  |                                                                         |
a99c34 9  | Licensed under the GNU General Public License version 3 or              |
AM 10  | any later version with exceptions for skins & plugins.                  |
11  | See the README file for a full license statement.                       |
48e9c1 12  |                                                                         |
T 13  +-------------------------------------------------------------------------+
14  | Author: Aleksander Machniak <alec@alec.pl>                              |
15  +-------------------------------------------------------------------------+
16 */
17
58c279 18 /**
AM 19  * Enigma plugin engine.
20  *
21  * RFC2440: OpenPGP Message Format
22  * RFC3156: MIME Security with OpenPGP
23  * RFC3851: S/MIME
24  */
48e9c1 25 class enigma_engine
T 26 {
27     private $rc;
28     private $enigma;
29     private $pgp_driver;
30     private $smime_driver;
765736 31     private $password_time;
48e9c1 32
1ad0e7 33     public $decryptions     = array();
AM 34     public $signatures      = array();
35     public $encrypted_parts = array();
a99c34 36
AM 37     const SIGN_MODE_BODY     = 1;
38     const SIGN_MODE_SEPARATE = 2;
39     const SIGN_MODE_MIME     = 3;
40
41     const ENCRYPT_MODE_BODY = 1;
42     const ENCRYPT_MODE_MIME = 2;
48e9c1 43
T 44
45     /**
46      * Plugin initialization.
47      */
48     function __construct($enigma)
49     {
0878c8 50         $this->rc     = rcmail::get_instance();
48e9c1 51         $this->enigma = $enigma;
0878c8 52
58c279 53         $this->password_time = $this->rc->config->get('enigma_password_time') * 60;
765736 54
0878c8 55         // this will remove passwords from session after some time
765736 56         if ($this->password_time) {
AM 57             $this->get_passwords();
58         }
48e9c1 59     }
T 60
61     /**
62      * PGP driver initialization.
63      */
64     function load_pgp_driver()
65     {
0878c8 66         if ($this->pgp_driver) {
48e9c1 67             return;
0878c8 68         }
48e9c1 69
0878c8 70         $driver   = 'enigma_driver_' . $this->rc->config->get('enigma_pgp_driver', 'gnupg');
48e9c1 71         $username = $this->rc->user->get_username();
T 72
73         // Load driver
74         $this->pgp_driver = new $driver($username);
75
76         if (!$this->pgp_driver) {
61be82 77             rcube::raise_error(array(
48e9c1 78                 'code' => 600, 'type' => 'php',
T 79                 'file' => __FILE__, 'line' => __LINE__,
80                 'message' => "Enigma plugin: Unable to load PGP driver: $driver"
81             ), true, true);
82         }
83
84         // Initialise driver
85         $result = $this->pgp_driver->init();
86
87         if ($result instanceof enigma_error) {
61be82 88             rcube::raise_error(array(
48e9c1 89                 'code' => 600, 'type' => 'php',
T 90                 'file' => __FILE__, 'line' => __LINE__,
91                 'message' => "Enigma plugin: ".$result->getMessage()
92             ), true, true);
93         }
94     }
95
96     /**
97      * S/MIME driver initialization.
98      */
99     function load_smime_driver()
100     {
0878c8 101         if ($this->smime_driver) {
48e9c1 102             return;
0878c8 103         }
48e9c1 104
0878c8 105         $driver   = 'enigma_driver_' . $this->rc->config->get('enigma_smime_driver', 'phpssl');
48e9c1 106         $username = $this->rc->user->get_username();
T 107
108         // Load driver
109         $this->smime_driver = new $driver($username);
110
111         if (!$this->smime_driver) {
61be82 112             rcube::raise_error(array(
48e9c1 113                 'code' => 600, 'type' => 'php',
T 114                 'file' => __FILE__, 'line' => __LINE__,
115                 'message' => "Enigma plugin: Unable to load S/MIME driver: $driver"
116             ), true, true);
117         }
118
119         // Initialise driver
120         $result = $this->smime_driver->init();
121
122         if ($result instanceof enigma_error) {
61be82 123             rcube::raise_error(array(
48e9c1 124                 'code' => 600, 'type' => 'php',
T 125                 'file' => __FILE__, 'line' => __LINE__,
126                 'message' => "Enigma plugin: ".$result->getMessage()
127             ), true, true);
a99c34 128         }
AM 129     }
130
131     /**
132      * Handler for message signing
133      *
134      * @param Mail_mime Original message
135      * @param int       Encryption mode
136      *
137      * @return enigma_error On error returns error object
138      */
139     function sign_message(&$message, $mode = null)
140     {
141         $mime = new enigma_mime_message($message, enigma_mime_message::PGP_SIGNED);
142         $from = $mime->getFromAddress();
143
144         // find private key
145         $key = $this->find_key($from, true);
146
147         if (empty($key)) {
cffe97 148             return new enigma_error(enigma_error::KEYNOTFOUND);
a99c34 149         }
AM 150
151         // check if we have password for this key
152         $passwords = $this->get_passwords();
153         $pass      = $passwords[$key->id];
154
155         if ($pass === null) {
156             // ask for password
157             $error = array('missing' => array($key->id => $key->name));
cffe97 158             return new enigma_error(enigma_error::BADPASS, '', $error);
a99c34 159         }
AM 160
161         // select mode
162         switch ($mode) {
163         case self::SIGN_MODE_BODY:
164             $pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
165             break;
166
167         case self::SIGN_MODE_MIME:
168             $pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
169             break;
170 /*
171         case self::SIGN_MODE_SEPARATE:
172             $pgp_mode = Crypt_GPG::SIGN_MODE_NORMAL;
173             break;
174 */
175         default:
176             if ($mime->isMultipart()) {
177                 $pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
178             }
179             else {
180                 $pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
181             }
182         }
183
184         // get message body
185         if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
186             // in this mode we'll replace text part
187             // with the one containing signature
188             $body = $message->getTXTBody();
5d49af 189
AM 190             $text_charset = $message->getParam('text_charset');
191             $line_length  = $this->rc->config->get('line_length', 72);
192
193             // We can't use format=flowed for signed messages
194             if (strpos($text_charset, 'format=flowed')) {
195                 list($charset, $params) = explode(';', $text_charset);
196                 $body = rcube_mime::unfold_flowed($body);
197                 $body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset);
198
199                 $text_charset = str_replace(";\r\n format=flowed", '', $text_charset);
200             }
a99c34 201         }
AM 202         else {
203             // here we'll build PGP/MIME message
204             $body = $mime->getOrigBody();
205         }
206
207         // sign the body
208         $result = $this->pgp_sign($body, $key->id, $pass, $pgp_mode);
209
210         if ($result !== true) {
cffe97 211             if ($result->getCode() == enigma_error::BADPASS) {
a99c34 212                 // ask for password
5a3065 213                 $error = array('bad' => array($key->id => $key->name));
cffe97 214                 return new enigma_error(enigma_error::BADPASS, '', $error);
a99c34 215             }
AM 216
217             return $result;
218         }
219
220         // replace message body
221         if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
222             $message->setTXTBody($body);
5d49af 223             $message->setParam('text_charset', $text_charset);
a99c34 224         }
AM 225         else {
226             $mime->addPGPSignature($body);
227             $message = $mime;
228         }
229     }
230
231     /**
232      * Handler for message encryption
233      *
234      * @param Mail_mime Original message
235      * @param int       Encryption mode
236      * @param bool      Is draft-save action - use only sender's key for encryption
237      *
238      * @return enigma_error On error returns error object
239      */
240     function encrypt_message(&$message, $mode = null, $is_draft = false)
241     {
242         $mime = new enigma_mime_message($message, enigma_mime_message::PGP_ENCRYPTED);
243
244         // always use sender's key
245         $recipients = array($mime->getFromAddress());
246
247         // if it's not a draft we add all recipients' keys
248         if (!$is_draft) {
249             $recipients = array_merge($recipients, $mime->getRecipients());
250         }
251
252         if (empty($recipients)) {
cffe97 253             return new enigma_error(enigma_error::KEYNOTFOUND);
a99c34 254         }
AM 255
256         $recipients = array_unique($recipients);
257
258         // find recipient public keys
259         foreach ((array) $recipients as $email) {
260             $key = $this->find_key($email);
261
262             if (empty($key)) {
cffe97 263                 return new enigma_error(enigma_error::KEYNOTFOUND, '', array(
a99c34 264                     'missing' => $email
AM 265                 ));
266             }
267
268             $keys[] = $key->id;
269         }
270
271         // select mode
272         switch ($mode) {
273         case self::ENCRYPT_MODE_BODY:
274             $encrypt_mode = $mode;
275             break;
276
277         case self::ENCRYPT_MODE_MIME:
278             $encrypt_mode = $mode;
279             break;
280
281         default:
282             $encrypt_mode = $mime->isMultipart() ? self::ENCRYPT_MODE_MIME : self::ENCRYPT_MODE_BODY;
283         }
284
285         // get message body
286         if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
287             // in this mode we'll replace text part
288             // with the one containing encrypted message
289             $body = $message->getTXTBody();
290         }
291         else {
292             // here we'll build PGP/MIME message
293             $body = $mime->getOrigBody();
294         }
295
296         // sign the body
297         $result = $this->pgp_encrypt($body, $keys);
298
299         if ($result !== true) {
300             return $result;
301         }
302
303         // replace message body
304         if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
305             $message->setTXTBody($body);
306         }
307         else {
308             $mime->setPGPEncryptedBody($body);
309             $message = $mime;
48e9c1 310         }
T 311     }
312
313     /**
0878c8 314      * Handler for message_part_structure hook.
AM 315      * Called for every part of the message.
316      *
53fa08 317      * @param array  Original parameters
AM 318      * @param string Part body (will be set if used internally)
0878c8 319      *
AM 320      * @return array Modified parameters
321      */
53fa08 322     function part_structure($p, $body = null)
0878c8 323     {
AM 324         if ($p['mimetype'] == 'text/plain' || $p['mimetype'] == 'application/pgp') {
53fa08 325             $this->parse_plain($p, $body);
0878c8 326         }
AM 327         else if ($p['mimetype'] == 'multipart/signed') {
53fa08 328             $this->parse_signed($p, $body);
0878c8 329         }
AM 330         else if ($p['mimetype'] == 'multipart/encrypted') {
331             $this->parse_encrypted($p);
332         }
333         else if ($p['mimetype'] == 'application/pkcs7-mime') {
334             $this->parse_encrypted($p);
335         }
336
337         return $p;
338     }
339
340     /**
341      * Handler for message_part_body hook.
342      *
343      * @param array Original parameters
344      *
345      * @return array Modified parameters
346      */
347     function part_body($p)
348     {
349         // encrypted attachment, see parse_plain_encrypted()
350         if ($p['part']->need_decryption && $p['part']->body === null) {
1ad0e7 351             $this->load_pgp_driver();
AM 352
0878c8 353             $storage = $this->rc->get_storage();
AM 354             $body    = $storage->get_message_part($p['object']->uid, $p['part']->mime_id, $p['part'], null, null, true, 0, false);
355             $result  = $this->pgp_decrypt($body);
356
357             // @TODO: what to do on error?
358             if ($result === true) {
359                 $p['part']->body = $body;
360                 $p['part']->size = strlen($body);
361                 $p['part']->body_modified = true;
362             }
363         }
364
365         return $p;
366     }
367
368     /**
48e9c1 369      * Handler for plain/text message.
T 370      *
53fa08 371      * @param array  Reference to hook's parameters
AM 372      * @param string Part body (will be set if used internally)
48e9c1 373      */
53fa08 374     function parse_plain(&$p, $body = null)
48e9c1 375     {
T 376         $part = $p['structure'];
377
0878c8 378         // exit, if we're already inside a decrypted message
1ad0e7 379         if (in_array($part->mime_id, $this->encrypted_parts)) {
0878c8 380             return;
AM 381         }
48e9c1 382
0878c8 383         // Get message body from IMAP server
53fa08 384         if ($body === null) {
AM 385             $body = $this->get_part_body($p['object'], $part);
386         }
0878c8 387
AM 388         // @TODO: big message body could be a file resource
48e9c1 389         // PGP signed message
0878c8 390         if (preg_match('/^-----BEGIN PGP SIGNED MESSAGE-----/', $body)) {
AM 391             $this->parse_plain_signed($p, $body);
48e9c1 392         }
T 393         // PGP encrypted message
0878c8 394         else if (preg_match('/^-----BEGIN PGP MESSAGE-----/', $body)) {
AM 395             $this->parse_plain_encrypted($p, $body);
48e9c1 396         }
T 397     }
398
399     /**
400      * Handler for multipart/signed message.
401      *
53fa08 402      * @param array  Reference to hook's parameters
AM 403      * @param string Part body (will be set if used internally)
48e9c1 404      */
53fa08 405     function parse_signed(&$p, $body = null)
48e9c1 406     {
T 407         $struct = $p['structure'];
408
409         // S/MIME
410         if ($struct->parts[1] && $struct->parts[1]->mimetype == 'application/pkcs7-signature') {
53fa08 411             $this->parse_smime_signed($p, $body);
48e9c1 412         }
0878c8 413         // PGP/MIME: RFC3156
48e9c1 414         // The multipart/signed body MUST consist of exactly two parts.
T 415         // The first part contains the signed data in MIME canonical format,
416         // including a set of appropriate content headers describing the data.
417         // The second body MUST contain the PGP digital signature.  It MUST be
418         // labeled with a content type of "application/pgp-signature".
0878c8 419         else if ($struct->ctype_parameters['protocol'] == 'application/pgp-signature'
AM 420             && count($struct->parts) == 2
421             && $struct->parts[1] && $struct->parts[1]->mimetype == 'application/pgp-signature'
422         ) {
53fa08 423             $this->parse_pgp_signed($p, $body);
48e9c1 424         }
T 425     }
426
427     /**
428      * Handler for multipart/encrypted message.
429      *
430      * @param array Reference to hook's parameters
431      */
432     function parse_encrypted(&$p)
433     {
434         $struct = $p['structure'];
435
436         // S/MIME
437         if ($struct->mimetype == 'application/pkcs7-mime') {
438             $this->parse_smime_encrypted($p);
439         }
0878c8 440         // PGP/MIME: RFC3156
AM 441         // The multipart/encrypted MUST consist of exactly two parts. The first
48e9c1 442         // MIME body part must have a content type of "application/pgp-encrypted".
T 443         // This body contains the control information.
444         // The second MIME body part MUST contain the actual encrypted data.  It
445         // must be labeled with a content type of "application/octet-stream".
0878c8 446         else if ($struct->ctype_parameters['protocol'] == 'application/pgp-encrypted'
AM 447             && count($struct->parts) == 2
448             && $struct->parts[0] && $struct->parts[0]->mimetype == 'application/pgp-encrypted'
449             && $struct->parts[1] && $struct->parts[1]->mimetype == 'application/octet-stream'
48e9c1 450         ) {
T 451             $this->parse_pgp_encrypted($p);
452         }
453     }
454
455     /**
456      * Handler for plain signed message.
457      * Excludes message and signature bodies and verifies signature.
458      *
0878c8 459      * @param array  Reference to hook's parameters
AM 460      * @param string Message (part) body
48e9c1 461      */
0878c8 462     private function parse_plain_signed(&$p, $body)
48e9c1 463     {
392ede 464         if (!$this->rc->config->get('enigma_signatures', true)) {
AM 465             return;
466         }
467
48e9c1 468         $this->load_pgp_driver();
T 469         $part = $p['structure'];
470
471         // Verify signature
392ede 472         if ($this->rc->action == 'show' || $this->rc->action == 'preview' || $this->rc->action == 'print') {
AM 473             $sig = $this->pgp_verify($body);
48e9c1 474         }
T 475
476         // @TODO: Handle big bodies using (temp) files
477
478         // In this way we can use fgets on string as on file handle
479         $fh = fopen('php://memory', 'br+');
480         // @TODO: fopen/fwrite errors handling
481         if ($fh) {
0878c8 482             fwrite($fh, $body);
48e9c1 483             rewind($fh);
T 484         }
0878c8 485
AM 486         $body = $part->body = null;
487         $part->body_modified = true;
48e9c1 488
T 489         // Extract body (and signature?)
490         while (!feof($fh)) {
491             $line = fgets($fh, 1024);
492
493             if ($part->body === null)
494                 $part->body = '';
495             else if (preg_match('/^-----BEGIN PGP SIGNATURE-----/', $line))
496                 break;
497             else
498                 $part->body .= $line;
499         }
500
501         // Remove "Hash" Armor Headers
502         $part->body = preg_replace('/^.*\r*\n\r*\n/', '', $part->body);
503         // de-Dash-Escape (RFC2440)
504         $part->body = preg_replace('/(^|\n)- -/', '\\1-', $part->body);
505
506         // Store signature data for display
507         if (!empty($sig)) {
8c626e 508             $this->signatures[$part->mime_id] = $sig;
48e9c1 509         }
T 510
511         fclose($fh);
512     }
3e98f8 513
48e9c1 514     /**
T 515      * Handler for PGP/MIME signed message.
516      * Verifies signature.
517      *
53fa08 518      * @param array  Reference to hook's parameters
AM 519      * @param string Part body (will be set if used internally)
48e9c1 520      */
53fa08 521     private function parse_pgp_signed(&$p, $body = null)
48e9c1 522     {
765736 523         if (!$this->rc->config->get('enigma_signatures', true)) {
AM 524             return;
525         }
526
392ede 527         if ($this->rc->action != 'show' && $this->rc->action != 'preview' && $this->rc->action != 'print') {
58c279 528             return;
AM 529         }
3e98f8 530
58c279 531         $this->load_pgp_driver();
AM 532         $struct = $p['structure'];
3e98f8 533
58c279 534         $msg_part = $struct->parts[0];
AM 535         $sig_part = $struct->parts[1];
48e9c1 536
58c279 537         // Get bodies
AM 538         // Note: The first part body need to be full part body with headers
539         //       it also cannot be decoded
53fa08 540         if ($body !== null) {
AM 541             // set signed part body
542             list($msg_body, $sig_body) = $this->explode_signed_body($body, $struct->ctype_parameters['boundary']);
543         }
544         else {
545             $msg_body = $this->get_part_body($p['object'], $msg_part, true);
546             $sig_body = $this->get_part_body($p['object'], $sig_part);
547         }
48e9c1 548
58c279 549         // Verify
AM 550         $sig = $this->pgp_verify($msg_body, $sig_body);
48e9c1 551
58c279 552         // Store signature data for display
AM 553         $this->signatures[$struct->mime_id] = $sig;
8c626e 554         $this->signatures[$msg_part->mime_id] = $sig;
48e9c1 555     }
T 556
557     /**
558      * Handler for S/MIME signed message.
559      * Verifies signature.
560      *
53fa08 561      * @param array  Reference to hook's parameters
AM 562      * @param string Part body (will be set if used internally)
48e9c1 563      */
53fa08 564     private function parse_smime_signed(&$p, $body = null)
48e9c1 565     {
765736 566         if (!$this->rc->config->get('enigma_signatures', true)) {
AM 567             return;
568         }
569
53fa08 570         // @TODO
48e9c1 571     }
T 572
573     /**
574      * Handler for plain encrypted message.
575      *
0878c8 576      * @param array  Reference to hook's parameters
AM 577      * @param string Message (part) body
48e9c1 578      */
0878c8 579     private function parse_plain_encrypted(&$p, $body)
48e9c1 580     {
765736 581         if (!$this->rc->config->get('enigma_decryption', true)) {
AM 582             return;
583         }
584
48e9c1 585         $this->load_pgp_driver();
T 586         $part = $p['structure'];
3e98f8 587
AM 588         // Decrypt
0878c8 589         $result = $this->pgp_decrypt($body);
3e98f8 590
48e9c1 591         // Store decryption status
T 592         $this->decryptions[$part->mime_id] = $result;
3e98f8 593
1ad0e7 594         // find parent part ID
AM 595         if (strpos($part->mime_id, '.')) {
596             $items = explode('.', $part->mime_id);
597             array_pop($items);
598             $parent = implode('.', $items);
599         }
600         else {
601             $parent = 0;
602         }
603
48e9c1 604         // Parse decrypted message
T 605         if ($result === true) {
0878c8 606             $part->body          = $body;
AM 607             $part->body_modified = true;
1ad0e7 608
AM 609             // Remember it was decrypted
610             $this->encrypted_parts[] = $part->mime_id;
0878c8 611
04598b 612             // PGP signed inside? verify signature
AM 613             if (preg_match('/^-----BEGIN PGP SIGNED MESSAGE-----/', $body)) {
614                 $this->parse_plain_signed($p, $body);
615             }
616
0878c8 617             // Encrypted plain message may contain encrypted attachments
1ad0e7 618             // in such case attachments have .pgp extension and type application/octet-stream.
0878c8 619             // This is what happens when you select "Encrypt each attachment separately
AM 620             // and send the message using inline PGP" in Thunderbird's Enigmail.
621
622             if ($p['object']->mime_parts[$parent]) {
623                 foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
624                     if ($p->disposition == 'attachment' && $p->mimetype == 'application/octet-stream'
625                         && preg_match('/^(.*)\.pgp$/i', $p->filename, $m)
626                     ) {
627                         // modify filename
628                         $p->filename = $m[1];
629                         // flag the part, it will be decrypted when needed
630                         $p->need_decryption = true;
631                         // disable caching
632                         $p->body_modified = true;
633                     }
1ad0e7 634                 }
AM 635             }
636         }
637         // decryption failed, but the message may have already
638         // been cached with the modified parts (see above),
639         // let's bring the original state back
640         else if ($p['object']->mime_parts[$parent]) {
641             foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
642                 if ($p->need_decryption && !preg_match('/^(.*)\.pgp$/i', $p->filename, $m)) {
643                     // modify filename
644                     $p->filename .= '.pgp';
645                     // flag the part, it will be decrypted when needed
646                     unset($p->need_decryption);
0878c8 647                 }
AM 648             }
48e9c1 649         }
T 650     }
3e98f8 651
48e9c1 652     /**
T 653      * Handler for PGP/MIME encrypted message.
654      *
655      * @param array Reference to hook's parameters
656      */
657     private function parse_pgp_encrypted(&$p)
658     {
765736 659         if (!$this->rc->config->get('enigma_decryption', true)) {
AM 660             return;
661         }
662
48e9c1 663         $this->load_pgp_driver();
0878c8 664
48e9c1 665         $struct = $p['structure'];
0878c8 666         $part   = $struct->parts[1];
AM 667
48e9c1 668         // Get body
c9e2ab 669         $body = $this->get_part_body($p['object'], $part);
48e9c1 670
T 671         // Decrypt
0878c8 672         $result = $this->pgp_decrypt($body);
48e9c1 673
T 674         if ($result === true) {
0878c8 675             // Parse decrypted message
AM 676             $struct = $this->parse_body($body);
c9e2ab 677
0878c8 678             // Modify original message structure
AM 679             $this->modify_structure($p, $struct);
53fa08 680
AM 681             // Parse the structure (there may be encrypted/signed parts inside
682             $this->part_structure(array(
683                     'object'    => $p['object'],
684                     'structure' => $struct,
685                     'mimetype'  => $struct->mimetype
686                 ), $body);
0878c8 687
AM 688             // Attach the decryption message to all parts
689             $this->decryptions[$struct->mime_id] = $result;
690             foreach ((array) $struct->parts as $sp) {
691                 $this->decryptions[$sp->mime_id] = $result;
692             }
48e9c1 693         }
T 694         else {
0878c8 695             $this->decryptions[$part->mime_id] = $result;
AM 696
48e9c1 697             // Make sure decryption status message will be displayed
T 698             $part->type = 'content';
699             $p['object']->parts[] = $part;
b87a79 700
AM 701             // don't show encrypted part on attachments list
702             // don't show "cannot display encrypted message" text
703             $p['abort'] = true;
48e9c1 704         }
T 705     }
706
707     /**
708      * Handler for S/MIME encrypted message.
709      *
710      * @param array Reference to hook's parameters
711      */
712     private function parse_smime_encrypted(&$p)
713     {
765736 714         if (!$this->rc->config->get('enigma_decryption', true)) {
AM 715             return;
716         }
717
53fa08 718         // @TODO
48e9c1 719     }
T 720
721     /**
722      * PGP signature verification.
723      *
724      * @param mixed Message body
725      * @param mixed Signature body (for MIME messages)
726      *
727      * @return mixed enigma_signature or enigma_error
728      */
729     private function pgp_verify(&$msg_body, $sig_body=null)
730     {
731         // @TODO: Handle big bodies using (temp) files
ce89ec 732         $sig = $this->pgp_driver->verify($msg_body, $sig_body);
48e9c1 733
cffe97 734         if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::KEYNOTFOUND)
ce89ec 735             rcube::raise_error(array(
48e9c1 736                 'code' => 600, 'type' => 'php',
T 737                 'file' => __FILE__, 'line' => __LINE__,
ce89ec 738                 'message' => "Enigma plugin: " . $sig->getMessage()
48e9c1 739                 ), true, false);
T 740
741         return $sig;
742     }
743
744     /**
745      * PGP message decryption.
746      *
747      * @param mixed Message body
748      *
749      * @return mixed True or enigma_error
750      */
751     private function pgp_decrypt(&$msg_body)
752     {
753         // @TODO: Handle big bodies using (temp) files
0878c8 754         $keys   = $this->get_passwords();
AM 755         $result = $this->pgp_driver->decrypt($msg_body, $keys);
a99c34 756
AM 757         if ($result instanceof enigma_error) {
758             $err_code = $result->getCode();
cffe97 759             if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
a99c34 760                 rcube::raise_error(array(
AM 761                     'code' => 600, 'type' => 'php',
762                     'file' => __FILE__, 'line' => __LINE__,
763                     'message' => "Enigma plugin: " . $result->getMessage()
764                     ), true, false);
765             return $result;
766         }
767
768         $msg_body = $result;
769
770         return true;
771     }
772
773     /**
774      * PGP message signing
775      *
776      * @param mixed  Message body
777      * @param string Key ID
778      * @param string Key passphrase
779      * @param int    Signing mode
780      *
781      * @return mixed True or enigma_error
782      */
783     private function pgp_sign(&$msg_body, $keyid, $password, $mode = null)
784     {
785         // @TODO: Handle big bodies using (temp) files
786         $result = $this->pgp_driver->sign($msg_body, $keyid, $password, $mode);
787
788         if ($result instanceof enigma_error) {
789             $err_code = $result->getCode();
cffe97 790             if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
a99c34 791                 rcube::raise_error(array(
AM 792                     'code' => 600, 'type' => 'php',
793                     'file' => __FILE__, 'line' => __LINE__,
794                     'message' => "Enigma plugin: " . $result->getMessage()
795                     ), true, false);
796             return $result;
797         }
798
799         $msg_body = $result;
800
801         return true;
802     }
803
804     /**
805      * PGP message encrypting
806      *
807      * @param mixed Message body
808      * @param array Keys
809      *
810      * @return mixed True or enigma_error
811      */
812     private function pgp_encrypt(&$msg_body, $keys)
813     {
814         // @TODO: Handle big bodies using (temp) files
815         $result = $this->pgp_driver->encrypt($msg_body, $keys);
48e9c1 816
T 817         if ($result instanceof enigma_error) {
818             $err_code = $result->getCode();
cffe97 819             if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
61be82 820                 rcube::raise_error(array(
48e9c1 821                     'code' => 600, 'type' => 'php',
T 822                     'file' => __FILE__, 'line' => __LINE__,
823                     'message' => "Enigma plugin: " . $result->getMessage()
824                     ), true, false);
825             return $result;
826         }
827
0878c8 828         $msg_body = $result;
AM 829
48e9c1 830         return true;
T 831     }
832
833     /**
834      * PGP keys listing.
835      *
836      * @param mixed Key ID/Name pattern
837      *
838      * @return mixed Array of keys or enigma_error
839      */
0878c8 840     function list_keys($pattern = '')
48e9c1 841     {
T 842         $this->load_pgp_driver();
843         $result = $this->pgp_driver->list_keys($pattern);
ce89ec 844
48e9c1 845         if ($result instanceof enigma_error) {
61be82 846             rcube::raise_error(array(
48e9c1 847                 'code' => 600, 'type' => 'php',
T 848                 'file' => __FILE__, 'line' => __LINE__,
849                 'message' => "Enigma plugin: " . $result->getMessage()
850                 ), true, false);
851         }
ce89ec 852
48e9c1 853         return $result;
T 854     }
855
856     /**
a99c34 857      * Find PGP private/public key
AM 858      *
859      * @param string E-mail address
860      * @param bool   Need a key for signing?
861      *
862      * @return enigma_key The key
863      */
864     function find_key($email, $can_sign = false)
865     {
866         $this->load_pgp_driver();
867         $result = $this->pgp_driver->list_keys($email);
868
869         if ($result instanceof enigma_error) {
870             rcube::raise_error(array(
871                 'code' => 600, 'type' => 'php',
872                 'file' => __FILE__, 'line' => __LINE__,
873                 'message' => "Enigma plugin: " . $result->getMessage()
874                 ), true, false);
875
876             return;
877         }
878
879         $mode = $can_sign ? enigma_key::CAN_SIGN : enigma_key::CAN_ENCRYPT;
880
881         // check key validity and type
882         foreach ($result as $key) {
883             if ($keyid = $key->find_subkey($email, $mode)) {
884                 return $key;
885             }
886         }
887     }
888
889     /**
48e9c1 890      * PGP key details.
T 891      *
892      * @param mixed Key ID
893      *
894      * @return mixed enigma_key or enigma_error
895      */
896     function get_key($keyid)
897     {
898         $this->load_pgp_driver();
899         $result = $this->pgp_driver->get_key($keyid);
0878c8 900
48e9c1 901         if ($result instanceof enigma_error) {
61be82 902             rcube::raise_error(array(
48e9c1 903                 'code' => 600, 'type' => 'php',
T 904                 'file' => __FILE__, 'line' => __LINE__,
905                 'message' => "Enigma plugin: " . $result->getMessage()
906                 ), true, false);
907         }
0878c8 908
AM 909         return $result;
910     }
911
912     /**
913      * PGP key delete.
914      *
915      * @param string Key ID
916      *
917      * @return enigma_error|bool True on success
918      */
919     function delete_key($keyid)
920     {
921         $this->load_pgp_driver();
922         $result = $this->pgp_driver->delete_key($keyid);
923
924         if ($result instanceof enigma_error) {
925             rcube::raise_error(array(
926                 'code' => 600, 'type' => 'php',
927                 'file' => __FILE__, 'line' => __LINE__,
928                 'message' => "Enigma plugin: " . $result->getMessage()
929                 ), true, false);
930         }
931
48e9c1 932         return $result;
T 933     }
934
935     /**
a0dfcb 936      * PGP keys pair generation.
AM 937      *
938      * @param array Key pair parameters
939      *
940      * @return mixed enigma_key or enigma_error
941      */
942     function generate_key($data)
943     {
944         $this->load_pgp_driver();
945         $result = $this->pgp_driver->gen_key($data);
946
947         if ($result instanceof enigma_error) {
948             rcube::raise_error(array(
949                 'code' => 600, 'type' => 'php',
950                 'file' => __FILE__, 'line' => __LINE__,
951                 'message' => "Enigma plugin: " . $result->getMessage()
952                 ), true, false);
953         }
954
955         return $result;
956     }
957
958     /**
48e9c1 959      * PGP keys/certs importing.
T 960      *
961      * @param mixed   Import file name or content
962      * @param boolean True if first argument is a filename
963      *
964      * @return mixed Import status data array or enigma_error
965      */
966     function import_key($content, $isfile=false)
967     {
968         $this->load_pgp_driver();
969         $result = $this->pgp_driver->import($content, $isfile);
970
971         if ($result instanceof enigma_error) {
61be82 972             rcube::raise_error(array(
48e9c1 973                 'code' => 600, 'type' => 'php',
T 974                 'file' => __FILE__, 'line' => __LINE__,
975                 'message' => "Enigma plugin: " . $result->getMessage()
976                 ), true, false);
977         }
978         else {
979             $result['imported'] = $result['public_imported'] + $result['private_imported'];
980             $result['unchanged'] = $result['public_unchanged'] + $result['private_unchanged'];
981         }
982
983         return $result;
984     }
985
986     /**
987      * Handler for keys/certs import request action
988      */
989     function import_file()
990     {
61be82 991         $uid     = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
AM 992         $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
993         $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
2193f6 994         $storage = $this->rc->get_storage();
48e9c1 995
T 996         if ($uid && $mime_id) {
2193f6 997             $storage->set_folder($mbox);
AM 998             $part = $storage->get_message_part($uid, $mime_id);
48e9c1 999         }
T 1000
1001         if ($part && is_array($result = $this->import_key($part))) {
1002             $this->rc->output->show_message('enigma.keysimportsuccess', 'confirmation',
1003                 array('new' => $result['imported'], 'old' => $result['unchanged']));
1004         }
1005         else
1006             $this->rc->output->show_message('enigma.keysimportfailed', 'error');
1007
1008         $this->rc->output->send();
1009     }
1010
58c279 1011     /**
211929 1012      * PGP keys/certs export..
AM 1013      *
1014      * @param string   Key ID
1015      * @param resource Optional output stream
1016      *
1017      * @return mixed Key content or enigma_error
1018      */
1019     function export_key($key, $fp = null)
1020     {
1021         $this->load_pgp_driver();
1022         $result = $this->pgp_driver->export($key, $fp);
1023
1024         if ($result instanceof enigma_error) {
1025             rcube::raise_error(array(
1026                 'code' => 600, 'type' => 'php',
1027                 'file' => __FILE__, 'line' => __LINE__,
1028                 'message' => "Enigma plugin: " . $result->getMessage()
1029                 ), true, false);
1030
1031             return $result;
1032         }
1033
1034         if ($fp) {
1035             fwrite($fp, $result);
1036         }
1037         else {
1038             return $result;
1039         }
1040     }
1041
1042     /**
58c279 1043      * Registers password for specified key/cert sent by the password prompt.
AM 1044      */
0878c8 1045     function password_handler()
48e9c1 1046     {
0878c8 1047         $keyid  = rcube_utils::get_input_value('_keyid', rcube_utils::INPUT_POST);
AM 1048         $passwd = rcube_utils::get_input_value('_passwd', rcube_utils::INPUT_POST, true);
1049
1050         if ($keyid && $passwd !== null && strlen($passwd)) {
1051             $this->save_password($keyid, $passwd);
48e9c1 1052         }
T 1053     }
0878c8 1054
58c279 1055     /**
AM 1056      * Saves key/cert password in user session
1057      */
0878c8 1058     function save_password($keyid, $password)
AM 1059     {
1060         // we store passwords in session for specified time
1061         if ($config = $_SESSION['enigma_pass']) {
1062             $config = $this->rc->decrypt($config);
1063             $config = @unserialize($config);
1064         }
1065
1066         $config[$keyid] = array($password, time());
1067
1068         $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
1069     }
1070
58c279 1071     /**
AM 1072      * Returns currently stored passwords
1073      */
0878c8 1074     function get_passwords()
AM 1075     {
1076         if ($config = $_SESSION['enigma_pass']) {
1077             $config = $this->rc->decrypt($config);
1078             $config = @unserialize($config);
1079         }
cffe97 1080
AM 1081         $threshold = $this->password_time ? time() - $this->password_time : 0;
0878c8 1082         $keys      = array();
AM 1083
1084         // delete expired passwords
1085         foreach ((array) $config as $key => $value) {
58c279 1086             if ($threshold && $value[1] < $threshold) {
0878c8 1087                 unset($config[$key]);
AM 1088                 $modified = true;
1089             }
1090             else {
1091                 $keys[$key] = $value[0];
1092             }
1093         }
1094
1095         if ($modified) {
1096             $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
1097         }
1098
1099         return $keys;
1100     }
1101
1102     /**
1103      * Get message part body.
1104      *
c9e2ab 1105      * @param rcube_message      Message object
AM 1106      * @param rcube_message_part Message part
1107      * @param bool               Return raw body with headers
0878c8 1108      */
c9e2ab 1109     private function get_part_body($msg, $part, $full = false)
0878c8 1110     {
AM 1111         // @TODO: Handle big bodies using file handles
c9e2ab 1112
53fa08 1113         if ($full) {
0878c8 1114             $storage = $this->rc->get_storage();
c9e2ab 1115             $body    = $storage->get_raw_headers($msg->uid, $part->mime_id);
AM 1116             $body   .= $storage->get_raw_body($msg->uid, null, $part->mime_id);
0878c8 1117         }
AM 1118         else {
c9e2ab 1119             $body = $msg->get_part_body($part->mime_id, false);
0878c8 1120         }
AM 1121
1122         return $body;
1123     }
1124
1125     /**
1126      * Parse decrypted message body into structure
1127      *
1128      * @param string Message body
1129      *
1130      * @return array Message structure
1131      */
1132     private function parse_body(&$body)
1133     {
1134         // Mail_mimeDecode need \r\n end-line, but gpg may return \n
1135         $body = preg_replace('/\r?\n/', "\r\n", $body);
1136
1137         // parse the body into structure
1138         $struct = rcube_mime::parse_message($body);
1139
1140         return $struct;
1141     }
1142
1143     /**
1144      * Replace message encrypted structure with decrypted message structure
1145      *
1146      * @param array
1147      * @param rcube_message_part
1148      */
1149     private function modify_structure(&$p, $struct)
1150     {
1151         // modify mime_parts property of the message object
1152         $old_id = $p['structure']->mime_id;
53fa08 1153
0878c8 1154         foreach (array_keys($p['object']->mime_parts) as $idx) {
AM 1155             if (!$old_id || $idx == $old_id || strpos($idx, $old_id . '.') === 0) {
1156                 unset($p['object']->mime_parts[$idx]);
1157             }
1158         }
1159
1160         // modify the new structure to be correctly handled by Roundcube
1161         $this->modify_structure_part($struct, $p['object'], $old_id);
1162
1163         // replace old structure with the new one
1164         $p['structure'] = $struct;
1165         $p['mimetype']  = $struct->mimetype;
1166     }
1167
1168     /**
1169      * Modify decrypted message part
1170      *
1171      * @param rcube_message_part
1172      * @param rcube_message
1173      */
1174     private function modify_structure_part($part, $msg, $old_id)
1175     {
1176         // never cache the body
1177         $part->body_modified = true;
1178         $part->encoding      = 'stream';
1179
1180         // modify part identifier
1181         if ($old_id) {
1182             $part->mime_id = !$part->mime_id ? $old_id : ($old_id . '.' . $part->mime_id);
1183         }
1184
1ad0e7 1185         // Cache the fact it was decrypted
AM 1186         $this->encrypted_parts[] = $part->mime_id;
1187
0878c8 1188         $msg->mime_parts[$part->mime_id] = $part;
AM 1189
1190         // modify sub-parts
1191         foreach ((array) $part->parts as $p) {
1192             $this->modify_structure_part($p, $msg, $old_id);
1193         }
1194     }
1195
1196     /**
53fa08 1197      * Extracts body and signature of multipart/signed message body
c9e2ab 1198      */
53fa08 1199     private function explode_signed_body($body, $boundary)
c9e2ab 1200     {
53fa08 1201         if (!$body) {
AM 1202             return array();
1203         }
1204
c9e2ab 1205         $boundary     = '--' . $boundary;
AM 1206         $boundary_len = strlen($boundary) + 2;
1207
53fa08 1208         // Find boundaries
AM 1209         $start = strpos($body, $boundary) + $boundary_len;
1210         $end   = strpos($body, $boundary, $start);
1211
1212         // Get signed body and signature
1213         $sig  = substr($body, $end + $boundary_len);
1214         $body = substr($body, $start, $end - $start - 2);
1215
1216         // Cleanup signature
1217         $sig = substr($sig, strpos($sig, "\r\n\r\n") + 4);
1218         $sig = substr($sig, 0, strpos($sig, $boundary));
1219
1220         return array($body, $sig);
c9e2ab 1221     }
AM 1222
1223     /**
0878c8 1224      * Checks if specified message part is a PGP-key or S/MIME cert data
AM 1225      *
1226      * @param rcube_message_part Part object
1227      *
1228      * @return boolean True if part is a key/cert
1229      */
1230     public function is_keys_part($part)
1231     {
1232         // @TODO: S/MIME
1233         return (
1234             // Content-Type: application/pgp-keys
1235             $part->mimetype == 'application/pgp-keys'
1236         );
1237     }
48e9c1 1238 }