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