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