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