Aleksander Machniak
2016-03-30 007c9ddcb0c49733522688c4d2364d1a4ab9ec8e
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     /**
0878c8 316      * Handler for message_part_structure hook.
AM 317      * Called for every part of the message.
318      *
53fa08 319      * @param array  Original parameters
AM 320      * @param string Part body (will be set if used internally)
0878c8 321      *
AM 322      * @return array Modified parameters
323      */
53fa08 324     function part_structure($p, $body = null)
0878c8 325     {
AM 326         if ($p['mimetype'] == 'text/plain' || $p['mimetype'] == 'application/pgp') {
53fa08 327             $this->parse_plain($p, $body);
0878c8 328         }
AM 329         else if ($p['mimetype'] == 'multipart/signed') {
53fa08 330             $this->parse_signed($p, $body);
0878c8 331         }
AM 332         else if ($p['mimetype'] == 'multipart/encrypted') {
333             $this->parse_encrypted($p);
334         }
335         else if ($p['mimetype'] == 'application/pkcs7-mime') {
336             $this->parse_encrypted($p);
337         }
338
339         return $p;
340     }
341
342     /**
343      * Handler for message_part_body hook.
344      *
345      * @param array Original parameters
346      *
347      * @return array Modified parameters
348      */
349     function part_body($p)
350     {
351         // encrypted attachment, see parse_plain_encrypted()
352         if ($p['part']->need_decryption && $p['part']->body === null) {
1ad0e7 353             $this->load_pgp_driver();
AM 354
0878c8 355             $storage = $this->rc->get_storage();
AM 356             $body    = $storage->get_message_part($p['object']->uid, $p['part']->mime_id, $p['part'], null, null, true, 0, false);
357             $result  = $this->pgp_decrypt($body);
358
359             // @TODO: what to do on error?
360             if ($result === true) {
361                 $p['part']->body = $body;
362                 $p['part']->size = strlen($body);
363                 $p['part']->body_modified = true;
364             }
365         }
366
367         return $p;
368     }
369
370     /**
48e9c1 371      * Handler for plain/text message.
T 372      *
53fa08 373      * @param array  Reference to hook's parameters
AM 374      * @param string Part body (will be set if used internally)
48e9c1 375      */
53fa08 376     function parse_plain(&$p, $body = null)
48e9c1 377     {
T 378         $part = $p['structure'];
379
0878c8 380         // exit, if we're already inside a decrypted message
1ad0e7 381         if (in_array($part->mime_id, $this->encrypted_parts)) {
0878c8 382             return;
AM 383         }
48e9c1 384
0878c8 385         // Get message body from IMAP server
53fa08 386         if ($body === null) {
AM 387             $body = $this->get_part_body($p['object'], $part);
388         }
0878c8 389
007c9d 390         // In this way we can use fgets on string as on file handle
AM 391         // Don't use php://temp for security (body may come from an encrypted part)
392         $fd = fopen('php://memory', 'r+');
393         if (!$fd) {
394             return;
48e9c1 395         }
007c9d 396
AM 397         fwrite($fd, $body);
398         rewind($fd);
399
400         $body   = '';
401         $prefix = '';
402         $mode   = '';
403         $tokens = array(
404             'BEGIN PGP SIGNED MESSAGE' => 'signed-start',
405             'END PGP SIGNATURE'        => 'signed-end',
406             'BEGIN PGP MESSAGE'        => 'encrypted-start',
407             'END PGP MESSAGE'          => 'encrypted-end',
408         );
409         $regexp = '/^-----(' . implode('|', array_keys($tokens)) . ')-----[\r\n]*/';
410
411         while (($line = fgets($fd)) !== false) {
412             if ($line[0] === '-' && $line[4] === '-' && preg_match($regexp, $line, $m)) {
413                 switch ($tokens[$m[1]]) {
414                 case 'signed-start':
415                     $body = $line;
416                     $mode = 'signed';
417                     break;
418
419                 case 'signed-end':
420                     if ($mode === 'signed') {
421                         $body .= $line;
422                     }
423                     break 2; // ignore anything after this line
424
425                 case 'encrypted-start':
426                     $body = $line;
427                     $mode = 'encrypted';
428                     break;
429
430                 case 'encrypted-end':
431                     if ($mode === 'encrypted') {
432                         $body .= $line;
433                     }
434                     break 2; // ignore anything after this line
435                 }
436
437                 continue;
438             }
439
440             if ($mode === 'signed') {
441                 $body .= $line;
442             }
443             else if ($mode === 'encrypted') {
444                 $body .= $line;
445             }
446             else {
447                 $prefix .= $line;
448             }
449         }
450
451         fclose($fd);
452
453         if ($mode === 'signed') {
454             $this->parse_plain_signed($p, $body, $prefix);
455         }
456         else if ($mode === 'encrypted') {
457             $this->parse_plain_encrypted($p, $body, $prefix);
48e9c1 458         }
T 459     }
460
461     /**
462      * Handler for multipart/signed message.
463      *
53fa08 464      * @param array  Reference to hook's parameters
AM 465      * @param string Part body (will be set if used internally)
48e9c1 466      */
53fa08 467     function parse_signed(&$p, $body = null)
48e9c1 468     {
T 469         $struct = $p['structure'];
470
471         // S/MIME
472         if ($struct->parts[1] && $struct->parts[1]->mimetype == 'application/pkcs7-signature') {
53fa08 473             $this->parse_smime_signed($p, $body);
48e9c1 474         }
0878c8 475         // PGP/MIME: RFC3156
48e9c1 476         // The multipart/signed body MUST consist of exactly two parts.
T 477         // The first part contains the signed data in MIME canonical format,
478         // including a set of appropriate content headers describing the data.
479         // The second body MUST contain the PGP digital signature.  It MUST be
480         // labeled with a content type of "application/pgp-signature".
4e6f30 481         else if (count($struct->parts) == 2
0878c8 482             && $struct->parts[1] && $struct->parts[1]->mimetype == 'application/pgp-signature'
AM 483         ) {
53fa08 484             $this->parse_pgp_signed($p, $body);
48e9c1 485         }
T 486     }
487
488     /**
489      * Handler for multipart/encrypted message.
490      *
491      * @param array Reference to hook's parameters
492      */
493     function parse_encrypted(&$p)
494     {
495         $struct = $p['structure'];
496
497         // S/MIME
4e6f30 498         if ($p['mimetype'] == 'application/pkcs7-mime') {
48e9c1 499             $this->parse_smime_encrypted($p);
T 500         }
0878c8 501         // PGP/MIME: RFC3156
AM 502         // The multipart/encrypted MUST consist of exactly two parts. The first
48e9c1 503         // MIME body part must have a content type of "application/pgp-encrypted".
T 504         // This body contains the control information.
505         // The second MIME body part MUST contain the actual encrypted data.  It
506         // must be labeled with a content type of "application/octet-stream".
4e6f30 507         else if (count($struct->parts) == 2
0878c8 508             && $struct->parts[0] && $struct->parts[0]->mimetype == 'application/pgp-encrypted'
AM 509             && $struct->parts[1] && $struct->parts[1]->mimetype == 'application/octet-stream'
48e9c1 510         ) {
T 511             $this->parse_pgp_encrypted($p);
512         }
513     }
514
515     /**
516      * Handler for plain signed message.
517      * Excludes message and signature bodies and verifies signature.
518      *
0878c8 519      * @param array  Reference to hook's parameters
AM 520      * @param string Message (part) body
007c9d 521      * @param string Body prefix (additional text before the encrypted block)
48e9c1 522      */
007c9d 523     private function parse_plain_signed(&$p, $body, $prefix = '')
48e9c1 524     {
392ede 525         if (!$this->rc->config->get('enigma_signatures', true)) {
AM 526             return;
527         }
528
48e9c1 529         $this->load_pgp_driver();
T 530         $part = $p['structure'];
531
532         // Verify signature
392ede 533         if ($this->rc->action == 'show' || $this->rc->action == 'preview' || $this->rc->action == 'print') {
AM 534             $sig = $this->pgp_verify($body);
48e9c1 535         }
T 536
537         // In this way we can use fgets on string as on file handle
007c9d 538         // Don't use php://temp for security (body may come from an encrypted part)
AM 539         $fd = fopen('php://memory', 'r+');
540         if (!$fd) {
541             return;
48e9c1 542         }
007c9d 543
AM 544         fwrite($fd, $body);
545         rewind($fd);
0878c8 546
AM 547         $body = $part->body = null;
548         $part->body_modified = true;
48e9c1 549
T 550         // Extract body (and signature?)
007c9d 551         while (($line = fgets($fd, 1024)) !== false) {
48e9c1 552             if ($part->body === null)
T 553                 $part->body = '';
554             else if (preg_match('/^-----BEGIN PGP SIGNATURE-----/', $line))
555                 break;
556             else
557                 $part->body .= $line;
558         }
559
007c9d 560         fclose($fd);
AM 561
48e9c1 562         // Remove "Hash" Armor Headers
T 563         $part->body = preg_replace('/^.*\r*\n\r*\n/', '', $part->body);
564         // de-Dash-Escape (RFC2440)
565         $part->body = preg_replace('/(^|\n)- -/', '\\1-', $part->body);
566
007c9d 567         if ($prefix) {
AM 568             $part->body = $prefix . $part->body;
48e9c1 569         }
T 570
007c9d 571         // Store signature data for display
AM 572         if (!empty($sig)) {
573             $sig->partial = !empty($prefix);
574             $this->signatures[$part->mime_id] = $sig;
575         }
48e9c1 576     }
3e98f8 577
48e9c1 578     /**
T 579      * Handler for PGP/MIME signed message.
580      * Verifies signature.
581      *
53fa08 582      * @param array  Reference to hook's parameters
AM 583      * @param string Part body (will be set if used internally)
48e9c1 584      */
53fa08 585     private function parse_pgp_signed(&$p, $body = null)
48e9c1 586     {
765736 587         if (!$this->rc->config->get('enigma_signatures', true)) {
AM 588             return;
589         }
590
392ede 591         if ($this->rc->action != 'show' && $this->rc->action != 'preview' && $this->rc->action != 'print') {
58c279 592             return;
AM 593         }
3e98f8 594
58c279 595         $this->load_pgp_driver();
AM 596         $struct = $p['structure'];
3e98f8 597
58c279 598         $msg_part = $struct->parts[0];
AM 599         $sig_part = $struct->parts[1];
48e9c1 600
58c279 601         // Get bodies
AM 602         // Note: The first part body need to be full part body with headers
603         //       it also cannot be decoded
53fa08 604         if ($body !== null) {
AM 605             // set signed part body
606             list($msg_body, $sig_body) = $this->explode_signed_body($body, $struct->ctype_parameters['boundary']);
607         }
608         else {
609             $msg_body = $this->get_part_body($p['object'], $msg_part, true);
610             $sig_body = $this->get_part_body($p['object'], $sig_part);
611         }
48e9c1 612
58c279 613         // Verify
AM 614         $sig = $this->pgp_verify($msg_body, $sig_body);
48e9c1 615
58c279 616         // Store signature data for display
AM 617         $this->signatures[$struct->mime_id] = $sig;
8c626e 618         $this->signatures[$msg_part->mime_id] = $sig;
48e9c1 619     }
T 620
621     /**
622      * Handler for S/MIME signed message.
623      * Verifies signature.
624      *
53fa08 625      * @param array  Reference to hook's parameters
AM 626      * @param string Part body (will be set if used internally)
48e9c1 627      */
53fa08 628     private function parse_smime_signed(&$p, $body = null)
48e9c1 629     {
765736 630         if (!$this->rc->config->get('enigma_signatures', true)) {
AM 631             return;
632         }
633
53fa08 634         // @TODO
48e9c1 635     }
T 636
637     /**
638      * Handler for plain encrypted message.
639      *
0878c8 640      * @param array  Reference to hook's parameters
AM 641      * @param string Message (part) body
007c9d 642      * @param string Body prefix (additional text before the encrypted block)
48e9c1 643      */
007c9d 644     private function parse_plain_encrypted(&$p, $body, $prefix = '')
48e9c1 645     {
765736 646         if (!$this->rc->config->get('enigma_decryption', true)) {
AM 647             return;
648         }
649
48e9c1 650         $this->load_pgp_driver();
T 651         $part = $p['structure'];
3e98f8 652
AM 653         // Decrypt
0878c8 654         $result = $this->pgp_decrypt($body);
3e98f8 655
48e9c1 656         // Store decryption status
T 657         $this->decryptions[$part->mime_id] = $result;
3e98f8 658
1ad0e7 659         // find parent part ID
AM 660         if (strpos($part->mime_id, '.')) {
661             $items = explode('.', $part->mime_id);
662             array_pop($items);
663             $parent = implode('.', $items);
664         }
665         else {
666             $parent = 0;
667         }
668
48e9c1 669         // Parse decrypted message
T 670         if ($result === true) {
007c9d 671             $part->body          = $prefix . $body;
0878c8 672             $part->body_modified = true;
007c9d 673
AM 674             // it maybe PGP signed inside, verify signature
675             $this->parse_plain($p, $body);
1ad0e7 676
AM 677             // Remember it was decrypted
678             $this->encrypted_parts[] = $part->mime_id;
0878c8 679
007c9d 680             // Inform the user that only a part of the body was encrypted
AM 681             if ($prefix) {
682                 $this->decryptions[$part->mime_id] = self::ENCRYPTED_PARTIALLY;
04598b 683             }
AM 684
0878c8 685             // Encrypted plain message may contain encrypted attachments
1ad0e7 686             // in such case attachments have .pgp extension and type application/octet-stream.
0878c8 687             // This is what happens when you select "Encrypt each attachment separately
AM 688             // and send the message using inline PGP" in Thunderbird's Enigmail.
689
690             if ($p['object']->mime_parts[$parent]) {
691                 foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
692                     if ($p->disposition == 'attachment' && $p->mimetype == 'application/octet-stream'
693                         && preg_match('/^(.*)\.pgp$/i', $p->filename, $m)
694                     ) {
695                         // modify filename
696                         $p->filename = $m[1];
697                         // flag the part, it will be decrypted when needed
698                         $p->need_decryption = true;
699                         // disable caching
700                         $p->body_modified = true;
701                     }
1ad0e7 702                 }
AM 703             }
704         }
705         // decryption failed, but the message may have already
706         // been cached with the modified parts (see above),
707         // let's bring the original state back
708         else if ($p['object']->mime_parts[$parent]) {
709             foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
710                 if ($p->need_decryption && !preg_match('/^(.*)\.pgp$/i', $p->filename, $m)) {
711                     // modify filename
712                     $p->filename .= '.pgp';
713                     // flag the part, it will be decrypted when needed
714                     unset($p->need_decryption);
0878c8 715                 }
AM 716             }
48e9c1 717         }
T 718     }
3e98f8 719
48e9c1 720     /**
T 721      * Handler for PGP/MIME encrypted message.
722      *
723      * @param array Reference to hook's parameters
724      */
725     private function parse_pgp_encrypted(&$p)
726     {
765736 727         if (!$this->rc->config->get('enigma_decryption', true)) {
AM 728             return;
729         }
730
48e9c1 731         $this->load_pgp_driver();
0878c8 732
48e9c1 733         $struct = $p['structure'];
0878c8 734         $part   = $struct->parts[1];
AM 735
48e9c1 736         // Get body
c9e2ab 737         $body = $this->get_part_body($p['object'], $part);
48e9c1 738
T 739         // Decrypt
0878c8 740         $result = $this->pgp_decrypt($body);
48e9c1 741
T 742         if ($result === true) {
0878c8 743             // Parse decrypted message
AM 744             $struct = $this->parse_body($body);
c9e2ab 745
0878c8 746             // Modify original message structure
4e6f30 747             $this->modify_structure($p, $struct, strlen($body));
53fa08 748
AM 749             // Parse the structure (there may be encrypted/signed parts inside
750             $this->part_structure(array(
751                     'object'    => $p['object'],
752                     'structure' => $struct,
753                     'mimetype'  => $struct->mimetype
754                 ), $body);
0878c8 755
AM 756             // Attach the decryption message to all parts
757             $this->decryptions[$struct->mime_id] = $result;
758             foreach ((array) $struct->parts as $sp) {
759                 $this->decryptions[$sp->mime_id] = $result;
760             }
48e9c1 761         }
T 762         else {
0878c8 763             $this->decryptions[$part->mime_id] = $result;
AM 764
48e9c1 765             // Make sure decryption status message will be displayed
T 766             $part->type = 'content';
767             $p['object']->parts[] = $part;
b87a79 768
AM 769             // don't show encrypted part on attachments list
770             // don't show "cannot display encrypted message" text
771             $p['abort'] = true;
48e9c1 772         }
T 773     }
774
775     /**
776      * Handler for S/MIME encrypted message.
777      *
778      * @param array Reference to hook's parameters
779      */
780     private function parse_smime_encrypted(&$p)
781     {
765736 782         if (!$this->rc->config->get('enigma_decryption', true)) {
AM 783             return;
784         }
785
53fa08 786         // @TODO
48e9c1 787     }
T 788
789     /**
790      * PGP signature verification.
791      *
792      * @param mixed Message body
793      * @param mixed Signature body (for MIME messages)
794      *
795      * @return mixed enigma_signature or enigma_error
796      */
797     private function pgp_verify(&$msg_body, $sig_body=null)
798     {
799         // @TODO: Handle big bodies using (temp) files
ce89ec 800         $sig = $this->pgp_driver->verify($msg_body, $sig_body);
48e9c1 801
cffe97 802         if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::KEYNOTFOUND)
ce89ec 803             rcube::raise_error(array(
48e9c1 804                 'code' => 600, 'type' => 'php',
T 805                 'file' => __FILE__, 'line' => __LINE__,
ce89ec 806                 'message' => "Enigma plugin: " . $sig->getMessage()
48e9c1 807                 ), true, false);
T 808
809         return $sig;
810     }
811
812     /**
813      * PGP message decryption.
814      *
815      * @param mixed Message body
816      *
817      * @return mixed True or enigma_error
818      */
819     private function pgp_decrypt(&$msg_body)
820     {
821         // @TODO: Handle big bodies using (temp) files
0878c8 822         $keys   = $this->get_passwords();
AM 823         $result = $this->pgp_driver->decrypt($msg_body, $keys);
a99c34 824
AM 825         if ($result instanceof enigma_error) {
826             $err_code = $result->getCode();
cffe97 827             if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
a99c34 828                 rcube::raise_error(array(
AM 829                     'code' => 600, 'type' => 'php',
830                     'file' => __FILE__, 'line' => __LINE__,
831                     'message' => "Enigma plugin: " . $result->getMessage()
832                     ), true, false);
833             return $result;
834         }
835
836         $msg_body = $result;
837
838         return true;
839     }
840
841     /**
842      * PGP message signing
843      *
844      * @param mixed  Message body
845      * @param string Key ID
846      * @param string Key passphrase
847      * @param int    Signing mode
848      *
849      * @return mixed True or enigma_error
850      */
851     private function pgp_sign(&$msg_body, $keyid, $password, $mode = null)
852     {
853         // @TODO: Handle big bodies using (temp) files
854         $result = $this->pgp_driver->sign($msg_body, $keyid, $password, $mode);
855
856         if ($result instanceof enigma_error) {
857             $err_code = $result->getCode();
cffe97 858             if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
a99c34 859                 rcube::raise_error(array(
AM 860                     'code' => 600, 'type' => 'php',
861                     'file' => __FILE__, 'line' => __LINE__,
862                     'message' => "Enigma plugin: " . $result->getMessage()
863                     ), true, false);
864             return $result;
865         }
866
867         $msg_body = $result;
868
869         return true;
870     }
871
872     /**
873      * PGP message encrypting
874      *
875      * @param mixed Message body
876      * @param array Keys
877      *
878      * @return mixed True or enigma_error
879      */
880     private function pgp_encrypt(&$msg_body, $keys)
881     {
882         // @TODO: Handle big bodies using (temp) files
883         $result = $this->pgp_driver->encrypt($msg_body, $keys);
48e9c1 884
T 885         if ($result instanceof enigma_error) {
886             $err_code = $result->getCode();
cffe97 887             if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
61be82 888                 rcube::raise_error(array(
48e9c1 889                     'code' => 600, 'type' => 'php',
T 890                     'file' => __FILE__, 'line' => __LINE__,
891                     'message' => "Enigma plugin: " . $result->getMessage()
892                     ), true, false);
893             return $result;
894         }
895
0878c8 896         $msg_body = $result;
AM 897
48e9c1 898         return true;
T 899     }
900
901     /**
902      * PGP keys listing.
903      *
904      * @param mixed Key ID/Name pattern
905      *
906      * @return mixed Array of keys or enigma_error
907      */
0878c8 908     function list_keys($pattern = '')
48e9c1 909     {
T 910         $this->load_pgp_driver();
911         $result = $this->pgp_driver->list_keys($pattern);
ce89ec 912
48e9c1 913         if ($result instanceof enigma_error) {
61be82 914             rcube::raise_error(array(
48e9c1 915                 'code' => 600, 'type' => 'php',
T 916                 'file' => __FILE__, 'line' => __LINE__,
917                 'message' => "Enigma plugin: " . $result->getMessage()
918                 ), true, false);
919         }
ce89ec 920
48e9c1 921         return $result;
T 922     }
923
924     /**
a99c34 925      * Find PGP private/public key
AM 926      *
927      * @param string E-mail address
928      * @param bool   Need a key for signing?
929      *
930      * @return enigma_key The key
931      */
932     function find_key($email, $can_sign = false)
933     {
934         $this->load_pgp_driver();
935         $result = $this->pgp_driver->list_keys($email);
936
937         if ($result instanceof enigma_error) {
938             rcube::raise_error(array(
939                 'code' => 600, 'type' => 'php',
940                 'file' => __FILE__, 'line' => __LINE__,
941                 'message' => "Enigma plugin: " . $result->getMessage()
942                 ), true, false);
943
944             return;
945         }
946
947         $mode = $can_sign ? enigma_key::CAN_SIGN : enigma_key::CAN_ENCRYPT;
948
949         // check key validity and type
950         foreach ($result as $key) {
951             if ($keyid = $key->find_subkey($email, $mode)) {
952                 return $key;
953             }
954         }
955     }
956
957     /**
48e9c1 958      * PGP key details.
T 959      *
960      * @param mixed Key ID
961      *
962      * @return mixed enigma_key or enigma_error
963      */
964     function get_key($keyid)
965     {
966         $this->load_pgp_driver();
967         $result = $this->pgp_driver->get_key($keyid);
0878c8 968
48e9c1 969         if ($result instanceof enigma_error) {
61be82 970             rcube::raise_error(array(
48e9c1 971                 'code' => 600, 'type' => 'php',
T 972                 'file' => __FILE__, 'line' => __LINE__,
973                 'message' => "Enigma plugin: " . $result->getMessage()
974                 ), true, false);
975         }
0878c8 976
AM 977         return $result;
978     }
979
980     /**
981      * PGP key delete.
982      *
983      * @param string Key ID
984      *
985      * @return enigma_error|bool True on success
986      */
987     function delete_key($keyid)
988     {
989         $this->load_pgp_driver();
990         $result = $this->pgp_driver->delete_key($keyid);
991
992         if ($result instanceof enigma_error) {
993             rcube::raise_error(array(
994                 'code' => 600, 'type' => 'php',
995                 'file' => __FILE__, 'line' => __LINE__,
996                 'message' => "Enigma plugin: " . $result->getMessage()
997                 ), true, false);
998         }
999
48e9c1 1000         return $result;
T 1001     }
1002
1003     /**
a0dfcb 1004      * PGP keys pair generation.
AM 1005      *
1006      * @param array Key pair parameters
1007      *
1008      * @return mixed enigma_key or enigma_error
1009      */
1010     function generate_key($data)
1011     {
1012         $this->load_pgp_driver();
1013         $result = $this->pgp_driver->gen_key($data);
1014
1015         if ($result instanceof enigma_error) {
1016             rcube::raise_error(array(
1017                 'code' => 600, 'type' => 'php',
1018                 'file' => __FILE__, 'line' => __LINE__,
1019                 'message' => "Enigma plugin: " . $result->getMessage()
1020                 ), true, false);
1021         }
1022
1023         return $result;
1024     }
1025
1026     /**
48e9c1 1027      * PGP keys/certs importing.
T 1028      *
1029      * @param mixed   Import file name or content
1030      * @param boolean True if first argument is a filename
1031      *
1032      * @return mixed Import status data array or enigma_error
1033      */
1034     function import_key($content, $isfile=false)
1035     {
1036         $this->load_pgp_driver();
1037         $result = $this->pgp_driver->import($content, $isfile);
1038
1039         if ($result instanceof enigma_error) {
61be82 1040             rcube::raise_error(array(
48e9c1 1041                 'code' => 600, 'type' => 'php',
T 1042                 'file' => __FILE__, 'line' => __LINE__,
1043                 'message' => "Enigma plugin: " . $result->getMessage()
1044                 ), true, false);
1045         }
1046         else {
1047             $result['imported'] = $result['public_imported'] + $result['private_imported'];
1048             $result['unchanged'] = $result['public_unchanged'] + $result['private_unchanged'];
1049         }
1050
1051         return $result;
1052     }
1053
1054     /**
1055      * Handler for keys/certs import request action
1056      */
1057     function import_file()
1058     {
61be82 1059         $uid     = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
AM 1060         $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
1061         $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
2193f6 1062         $storage = $this->rc->get_storage();
48e9c1 1063
T 1064         if ($uid && $mime_id) {
2193f6 1065             $storage->set_folder($mbox);
AM 1066             $part = $storage->get_message_part($uid, $mime_id);
48e9c1 1067         }
T 1068
1069         if ($part && is_array($result = $this->import_key($part))) {
1070             $this->rc->output->show_message('enigma.keysimportsuccess', 'confirmation',
1071                 array('new' => $result['imported'], 'old' => $result['unchanged']));
1072         }
1073         else
1074             $this->rc->output->show_message('enigma.keysimportfailed', 'error');
1075
1076         $this->rc->output->send();
1077     }
1078
58c279 1079     /**
211929 1080      * PGP keys/certs export..
AM 1081      *
1082      * @param string   Key ID
1083      * @param resource Optional output stream
1084      *
1085      * @return mixed Key content or enigma_error
1086      */
1087     function export_key($key, $fp = null)
1088     {
1089         $this->load_pgp_driver();
1090         $result = $this->pgp_driver->export($key, $fp);
1091
1092         if ($result instanceof enigma_error) {
1093             rcube::raise_error(array(
1094                 'code' => 600, 'type' => 'php',
1095                 'file' => __FILE__, 'line' => __LINE__,
1096                 'message' => "Enigma plugin: " . $result->getMessage()
1097                 ), true, false);
1098
1099             return $result;
1100         }
1101
1102         if ($fp) {
1103             fwrite($fp, $result);
1104         }
1105         else {
1106             return $result;
1107         }
1108     }
1109
1110     /**
58c279 1111      * Registers password for specified key/cert sent by the password prompt.
AM 1112      */
0878c8 1113     function password_handler()
48e9c1 1114     {
0878c8 1115         $keyid  = rcube_utils::get_input_value('_keyid', rcube_utils::INPUT_POST);
AM 1116         $passwd = rcube_utils::get_input_value('_passwd', rcube_utils::INPUT_POST, true);
1117
1118         if ($keyid && $passwd !== null && strlen($passwd)) {
1119             $this->save_password($keyid, $passwd);
48e9c1 1120         }
T 1121     }
0878c8 1122
58c279 1123     /**
AM 1124      * Saves key/cert password in user session
1125      */
0878c8 1126     function save_password($keyid, $password)
AM 1127     {
1128         // we store passwords in session for specified time
1129         if ($config = $_SESSION['enigma_pass']) {
1130             $config = $this->rc->decrypt($config);
1131             $config = @unserialize($config);
1132         }
1133
1134         $config[$keyid] = array($password, time());
1135
1136         $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
1137     }
1138
58c279 1139     /**
AM 1140      * Returns currently stored passwords
1141      */
0878c8 1142     function get_passwords()
AM 1143     {
1144         if ($config = $_SESSION['enigma_pass']) {
1145             $config = $this->rc->decrypt($config);
1146             $config = @unserialize($config);
1147         }
cffe97 1148
AM 1149         $threshold = $this->password_time ? time() - $this->password_time : 0;
0878c8 1150         $keys      = array();
AM 1151
1152         // delete expired passwords
1153         foreach ((array) $config as $key => $value) {
58c279 1154             if ($threshold && $value[1] < $threshold) {
0878c8 1155                 unset($config[$key]);
AM 1156                 $modified = true;
1157             }
1158             else {
1159                 $keys[$key] = $value[0];
1160             }
1161         }
1162
1163         if ($modified) {
1164             $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
1165         }
1166
1167         return $keys;
1168     }
1169
1170     /**
1171      * Get message part body.
1172      *
c9e2ab 1173      * @param rcube_message      Message object
AM 1174      * @param rcube_message_part Message part
1175      * @param bool               Return raw body with headers
0878c8 1176      */
c9e2ab 1177     private function get_part_body($msg, $part, $full = false)
0878c8 1178     {
AM 1179         // @TODO: Handle big bodies using file handles
c9e2ab 1180
53fa08 1181         if ($full) {
0878c8 1182             $storage = $this->rc->get_storage();
c9e2ab 1183             $body    = $storage->get_raw_headers($msg->uid, $part->mime_id);
AM 1184             $body   .= $storage->get_raw_body($msg->uid, null, $part->mime_id);
0878c8 1185         }
AM 1186         else {
c9e2ab 1187             $body = $msg->get_part_body($part->mime_id, false);
0878c8 1188         }
AM 1189
1190         return $body;
1191     }
1192
1193     /**
1194      * Parse decrypted message body into structure
1195      *
1196      * @param string Message body
1197      *
1198      * @return array Message structure
1199      */
1200     private function parse_body(&$body)
1201     {
1202         // Mail_mimeDecode need \r\n end-line, but gpg may return \n
1203         $body = preg_replace('/\r?\n/', "\r\n", $body);
1204
1205         // parse the body into structure
1206         $struct = rcube_mime::parse_message($body);
1207
1208         return $struct;
1209     }
1210
1211     /**
1212      * Replace message encrypted structure with decrypted message structure
1213      *
4e6f30 1214      * @param array              Hook arguments
AM 1215      * @param rcube_message_part Part structure
1216      * @param int                Part size
0878c8 1217      */
4e6f30 1218     private function modify_structure(&$p, $struct, $size = 0)
0878c8 1219     {
AM 1220         // modify mime_parts property of the message object
1221         $old_id = $p['structure']->mime_id;
53fa08 1222
0878c8 1223         foreach (array_keys($p['object']->mime_parts) as $idx) {
AM 1224             if (!$old_id || $idx == $old_id || strpos($idx, $old_id . '.') === 0) {
1225                 unset($p['object']->mime_parts[$idx]);
1226             }
1227         }
4e6f30 1228
AM 1229         // set some part params used by Roundcube core
1230         $struct->headers  = array_merge($p['structure']->headers, $struct->headers);
1231         $struct->size     = $size;
1232         $struct->filename = $p['structure']->filename;
0878c8 1233
AM 1234         // modify the new structure to be correctly handled by Roundcube
1235         $this->modify_structure_part($struct, $p['object'], $old_id);
1236
1237         // replace old structure with the new one
1238         $p['structure'] = $struct;
1239         $p['mimetype']  = $struct->mimetype;
1240     }
1241
1242     /**
1243      * Modify decrypted message part
1244      *
1245      * @param rcube_message_part
1246      * @param rcube_message
1247      */
1248     private function modify_structure_part($part, $msg, $old_id)
1249     {
1250         // never cache the body
1251         $part->body_modified = true;
1252         $part->encoding      = 'stream';
1253
1254         // modify part identifier
1255         if ($old_id) {
1256             $part->mime_id = !$part->mime_id ? $old_id : ($old_id . '.' . $part->mime_id);
1257         }
1258
1ad0e7 1259         // Cache the fact it was decrypted
AM 1260         $this->encrypted_parts[] = $part->mime_id;
0878c8 1261         $msg->mime_parts[$part->mime_id] = $part;
AM 1262
1263         // modify sub-parts
1264         foreach ((array) $part->parts as $p) {
1265             $this->modify_structure_part($p, $msg, $old_id);
1266         }
1267     }
1268
1269     /**
53fa08 1270      * Extracts body and signature of multipart/signed message body
c9e2ab 1271      */
53fa08 1272     private function explode_signed_body($body, $boundary)
c9e2ab 1273     {
53fa08 1274         if (!$body) {
AM 1275             return array();
1276         }
1277
c9e2ab 1278         $boundary     = '--' . $boundary;
AM 1279         $boundary_len = strlen($boundary) + 2;
1280
53fa08 1281         // Find boundaries
AM 1282         $start = strpos($body, $boundary) + $boundary_len;
1283         $end   = strpos($body, $boundary, $start);
1284
1285         // Get signed body and signature
1286         $sig  = substr($body, $end + $boundary_len);
1287         $body = substr($body, $start, $end - $start - 2);
1288
1289         // Cleanup signature
1290         $sig = substr($sig, strpos($sig, "\r\n\r\n") + 4);
1291         $sig = substr($sig, 0, strpos($sig, $boundary));
1292
1293         return array($body, $sig);
c9e2ab 1294     }
AM 1295
1296     /**
0878c8 1297      * Checks if specified message part is a PGP-key or S/MIME cert data
AM 1298      *
1299      * @param rcube_message_part Part object
1300      *
1301      * @return boolean True if part is a key/cert
1302      */
1303     public function is_keys_part($part)
1304     {
1305         // @TODO: S/MIME
1306         return (
1307             // Content-Type: application/pgp-keys
1308             $part->mimetype == 'application/pgp-keys'
1309         );
1310     }
48e9c1 1311 }