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