comparison plugins/enigma/lib/enigma_engine.php @ 0:1e000243b222

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