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