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