Kyle Francis
2016-03-30 a9d399155d205ae41015d7d205c6dacd7ecfc0d2
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
a9d399 924     function get_gpg_pubkey_for_attach($email)
KF 925     {
926         $this->load_pgp_driver();
927         $result = $this->pgp_driver->pubkey_for_attach($email);
928
929         if ($result instanceof enigma_error) {
930             rcube::raise_error(array(
931                 'code' => 600, 'type' => 'php',
932                 'file' => __FILE__, 'line' => __LINE__,
933                 'message' => "Enigma plugin: " . $result->getMessage()
934                 ), true, false);
935         }
936
937         return $result;
938     }
939
940     function get_keyID($email)
941     {
942         $this->load_pgp_driver();
943         $result = $this->pgp_driver->get_keyID($email);
944
945         if ($result instanceof enigma_error) {
946             rcube::raise_error(array(
947                 'code' => 600, 'type' => 'php',
948                 'file' => __FILE__, 'line' => __LINE__,
949                 'message' => "Enigma plugin: " . $result->getMessage()
950                 ), true, false);
951         }
952
953         return $result;
954     }
955
48e9c1 956     /**
a99c34 957      * Find PGP private/public key
AM 958      *
959      * @param string E-mail address
960      * @param bool   Need a key for signing?
961      *
962      * @return enigma_key The key
963      */
964     function find_key($email, $can_sign = false)
965     {
966         $this->load_pgp_driver();
967         $result = $this->pgp_driver->list_keys($email);
968
969         if ($result instanceof enigma_error) {
970             rcube::raise_error(array(
971                 'code' => 600, 'type' => 'php',
972                 'file' => __FILE__, 'line' => __LINE__,
973                 'message' => "Enigma plugin: " . $result->getMessage()
974                 ), true, false);
975
976             return;
977         }
978
979         $mode = $can_sign ? enigma_key::CAN_SIGN : enigma_key::CAN_ENCRYPT;
980
981         // check key validity and type
982         foreach ($result as $key) {
983             if ($keyid = $key->find_subkey($email, $mode)) {
984                 return $key;
985             }
986         }
987     }
988
989     /**
48e9c1 990      * PGP key details.
T 991      *
992      * @param mixed Key ID
993      *
994      * @return mixed enigma_key or enigma_error
995      */
996     function get_key($keyid)
997     {
998         $this->load_pgp_driver();
999         $result = $this->pgp_driver->get_key($keyid);
0878c8 1000
48e9c1 1001         if ($result instanceof enigma_error) {
61be82 1002             rcube::raise_error(array(
48e9c1 1003                 'code' => 600, 'type' => 'php',
T 1004                 'file' => __FILE__, 'line' => __LINE__,
1005                 'message' => "Enigma plugin: " . $result->getMessage()
1006                 ), true, false);
1007         }
0878c8 1008
AM 1009         return $result;
1010     }
1011
1012     /**
1013      * PGP key delete.
1014      *
1015      * @param string Key ID
1016      *
1017      * @return enigma_error|bool True on success
1018      */
1019     function delete_key($keyid)
1020     {
1021         $this->load_pgp_driver();
1022         $result = $this->pgp_driver->delete_key($keyid);
1023
1024         if ($result instanceof enigma_error) {
1025             rcube::raise_error(array(
1026                 'code' => 600, 'type' => 'php',
1027                 'file' => __FILE__, 'line' => __LINE__,
1028                 'message' => "Enigma plugin: " . $result->getMessage()
1029                 ), true, false);
1030         }
1031
48e9c1 1032         return $result;
T 1033     }
1034
1035     /**
a0dfcb 1036      * PGP keys pair generation.
AM 1037      *
1038      * @param array Key pair parameters
1039      *
1040      * @return mixed enigma_key or enigma_error
1041      */
1042     function generate_key($data)
1043     {
1044         $this->load_pgp_driver();
1045         $result = $this->pgp_driver->gen_key($data);
1046
1047         if ($result instanceof enigma_error) {
1048             rcube::raise_error(array(
1049                 'code' => 600, 'type' => 'php',
1050                 'file' => __FILE__, 'line' => __LINE__,
1051                 'message' => "Enigma plugin: " . $result->getMessage()
1052                 ), true, false);
1053         }
1054
1055         return $result;
1056     }
1057
1058     /**
48e9c1 1059      * PGP keys/certs importing.
T 1060      *
1061      * @param mixed   Import file name or content
1062      * @param boolean True if first argument is a filename
1063      *
1064      * @return mixed Import status data array or enigma_error
1065      */
1066     function import_key($content, $isfile=false)
1067     {
1068         $this->load_pgp_driver();
1069         $result = $this->pgp_driver->import($content, $isfile);
1070
1071         if ($result instanceof enigma_error) {
61be82 1072             rcube::raise_error(array(
48e9c1 1073                 'code' => 600, 'type' => 'php',
T 1074                 'file' => __FILE__, 'line' => __LINE__,
1075                 'message' => "Enigma plugin: " . $result->getMessage()
1076                 ), true, false);
1077         }
1078         else {
1079             $result['imported'] = $result['public_imported'] + $result['private_imported'];
1080             $result['unchanged'] = $result['public_unchanged'] + $result['private_unchanged'];
1081         }
1082
1083         return $result;
1084     }
1085
1086     /**
1087      * Handler for keys/certs import request action
1088      */
1089     function import_file()
1090     {
61be82 1091         $uid     = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
AM 1092         $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
1093         $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
2193f6 1094         $storage = $this->rc->get_storage();
48e9c1 1095
T 1096         if ($uid && $mime_id) {
2193f6 1097             $storage->set_folder($mbox);
AM 1098             $part = $storage->get_message_part($uid, $mime_id);
48e9c1 1099         }
T 1100
1101         if ($part && is_array($result = $this->import_key($part))) {
1102             $this->rc->output->show_message('enigma.keysimportsuccess', 'confirmation',
1103                 array('new' => $result['imported'], 'old' => $result['unchanged']));
1104         }
1105         else
1106             $this->rc->output->show_message('enigma.keysimportfailed', 'error');
1107
1108         $this->rc->output->send();
1109     }
1110
58c279 1111     /**
211929 1112      * PGP keys/certs export..
AM 1113      *
1114      * @param string   Key ID
1115      * @param resource Optional output stream
1116      *
1117      * @return mixed Key content or enigma_error
1118      */
1119     function export_key($key, $fp = null)
1120     {
1121         $this->load_pgp_driver();
1122         $result = $this->pgp_driver->export($key, $fp);
1123
1124         if ($result instanceof enigma_error) {
1125             rcube::raise_error(array(
1126                 'code' => 600, 'type' => 'php',
1127                 'file' => __FILE__, 'line' => __LINE__,
1128                 'message' => "Enigma plugin: " . $result->getMessage()
1129                 ), true, false);
1130
1131             return $result;
1132         }
1133
1134         if ($fp) {
1135             fwrite($fp, $result);
1136         }
1137         else {
1138             return $result;
1139         }
1140     }
1141
1142     /**
58c279 1143      * Registers password for specified key/cert sent by the password prompt.
AM 1144      */
0878c8 1145     function password_handler()
48e9c1 1146     {
0878c8 1147         $keyid  = rcube_utils::get_input_value('_keyid', rcube_utils::INPUT_POST);
AM 1148         $passwd = rcube_utils::get_input_value('_passwd', rcube_utils::INPUT_POST, true);
1149
1150         if ($keyid && $passwd !== null && strlen($passwd)) {
1151             $this->save_password($keyid, $passwd);
48e9c1 1152         }
T 1153     }
0878c8 1154
58c279 1155     /**
AM 1156      * Saves key/cert password in user session
1157      */
0878c8 1158     function save_password($keyid, $password)
AM 1159     {
1160         // we store passwords in session for specified time
1161         if ($config = $_SESSION['enigma_pass']) {
1162             $config = $this->rc->decrypt($config);
1163             $config = @unserialize($config);
1164         }
1165
1166         $config[$keyid] = array($password, time());
1167
1168         $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
1169     }
1170
58c279 1171     /**
AM 1172      * Returns currently stored passwords
1173      */
0878c8 1174     function get_passwords()
AM 1175     {
1176         if ($config = $_SESSION['enigma_pass']) {
1177             $config = $this->rc->decrypt($config);
1178             $config = @unserialize($config);
1179         }
cffe97 1180
AM 1181         $threshold = $this->password_time ? time() - $this->password_time : 0;
0878c8 1182         $keys      = array();
AM 1183
1184         // delete expired passwords
1185         foreach ((array) $config as $key => $value) {
58c279 1186             if ($threshold && $value[1] < $threshold) {
0878c8 1187                 unset($config[$key]);
AM 1188                 $modified = true;
1189             }
1190             else {
1191                 $keys[$key] = $value[0];
1192             }
1193         }
1194
1195         if ($modified) {
1196             $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
1197         }
1198
1199         return $keys;
1200     }
1201
1202     /**
1203      * Get message part body.
1204      *
c9e2ab 1205      * @param rcube_message      Message object
AM 1206      * @param rcube_message_part Message part
1207      * @param bool               Return raw body with headers
0878c8 1208      */
c9e2ab 1209     private function get_part_body($msg, $part, $full = false)
0878c8 1210     {
AM 1211         // @TODO: Handle big bodies using file handles
c9e2ab 1212
53fa08 1213         if ($full) {
0878c8 1214             $storage = $this->rc->get_storage();
c9e2ab 1215             $body    = $storage->get_raw_headers($msg->uid, $part->mime_id);
AM 1216             $body   .= $storage->get_raw_body($msg->uid, null, $part->mime_id);
0878c8 1217         }
AM 1218         else {
c9e2ab 1219             $body = $msg->get_part_body($part->mime_id, false);
0878c8 1220         }
AM 1221
1222         return $body;
1223     }
1224
1225     /**
1226      * Parse decrypted message body into structure
1227      *
1228      * @param string Message body
1229      *
1230      * @return array Message structure
1231      */
1232     private function parse_body(&$body)
1233     {
1234         // Mail_mimeDecode need \r\n end-line, but gpg may return \n
1235         $body = preg_replace('/\r?\n/', "\r\n", $body);
1236
1237         // parse the body into structure
1238         $struct = rcube_mime::parse_message($body);
1239
1240         return $struct;
1241     }
1242
1243     /**
1244      * Replace message encrypted structure with decrypted message structure
1245      *
4e6f30 1246      * @param array              Hook arguments
AM 1247      * @param rcube_message_part Part structure
1248      * @param int                Part size
0878c8 1249      */
4e6f30 1250     private function modify_structure(&$p, $struct, $size = 0)
0878c8 1251     {
AM 1252         // modify mime_parts property of the message object
1253         $old_id = $p['structure']->mime_id;
53fa08 1254
0878c8 1255         foreach (array_keys($p['object']->mime_parts) as $idx) {
AM 1256             if (!$old_id || $idx == $old_id || strpos($idx, $old_id . '.') === 0) {
1257                 unset($p['object']->mime_parts[$idx]);
1258             }
1259         }
4e6f30 1260
AM 1261         // set some part params used by Roundcube core
1262         $struct->headers  = array_merge($p['structure']->headers, $struct->headers);
1263         $struct->size     = $size;
1264         $struct->filename = $p['structure']->filename;
0878c8 1265
AM 1266         // modify the new structure to be correctly handled by Roundcube
1267         $this->modify_structure_part($struct, $p['object'], $old_id);
1268
1269         // replace old structure with the new one
1270         $p['structure'] = $struct;
1271         $p['mimetype']  = $struct->mimetype;
1272     }
1273
1274     /**
1275      * Modify decrypted message part
1276      *
1277      * @param rcube_message_part
1278      * @param rcube_message
1279      */
1280     private function modify_structure_part($part, $msg, $old_id)
1281     {
1282         // never cache the body
1283         $part->body_modified = true;
1284         $part->encoding      = 'stream';
1285
1286         // modify part identifier
1287         if ($old_id) {
1288             $part->mime_id = !$part->mime_id ? $old_id : ($old_id . '.' . $part->mime_id);
1289         }
1290
1ad0e7 1291         // Cache the fact it was decrypted
AM 1292         $this->encrypted_parts[] = $part->mime_id;
0878c8 1293         $msg->mime_parts[$part->mime_id] = $part;
AM 1294
1295         // modify sub-parts
1296         foreach ((array) $part->parts as $p) {
1297             $this->modify_structure_part($p, $msg, $old_id);
1298         }
1299     }
1300
1301     /**
53fa08 1302      * Extracts body and signature of multipart/signed message body
c9e2ab 1303      */
53fa08 1304     private function explode_signed_body($body, $boundary)
c9e2ab 1305     {
53fa08 1306         if (!$body) {
AM 1307             return array();
1308         }
1309
c9e2ab 1310         $boundary     = '--' . $boundary;
AM 1311         $boundary_len = strlen($boundary) + 2;
1312
53fa08 1313         // Find boundaries
AM 1314         $start = strpos($body, $boundary) + $boundary_len;
1315         $end   = strpos($body, $boundary, $start);
1316
1317         // Get signed body and signature
1318         $sig  = substr($body, $end + $boundary_len);
1319         $body = substr($body, $start, $end - $start - 2);
1320
1321         // Cleanup signature
1322         $sig = substr($sig, strpos($sig, "\r\n\r\n") + 4);
1323         $sig = substr($sig, 0, strpos($sig, $boundary));
1324
1325         return array($body, $sig);
c9e2ab 1326     }
AM 1327
1328     /**
0878c8 1329      * Checks if specified message part is a PGP-key or S/MIME cert data
AM 1330      *
1331      * @param rcube_message_part Part object
1332      *
1333      * @return boolean True if part is a key/cert
1334      */
1335     public function is_keys_part($part)
1336     {
1337         // @TODO: S/MIME
1338         return (
1339             // Content-Type: application/pgp-keys
1340             $part->mimetype == 'application/pgp-keys'
1341         );
1342     }
48e9c1 1343 }