]> git.donarmstrong.com Git - roundcube.git/blob - plugins/enigma/lib/enigma_engine.php
Imported Upstream version 0.6+dfsg
[roundcube.git] / plugins / enigma / lib / enigma_engine.php
1 <?php
2 /*
3  +-------------------------------------------------------------------------+
4  | Engine of the Enigma Plugin                                             |
5  |                                                                         |
6  | This program is free software; you can redistribute it and/or modify    |
7  | it under the terms of the GNU General Public License version 2          |
8  | as published by the Free Software Foundation.                           |
9  |                                                                         |
10  | This program is distributed in the hope that it will be useful,         |
11  | but WITHOUT ANY WARRANTY; without even the implied warranty of          |
12  | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           |
13  | GNU General Public License for more details.                            |
14  |                                                                         |
15  | You should have received a copy of the GNU General Public License along |
16  | with this program; if not, write to the Free Software Foundation, Inc., |
17  | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.             |
18  |                                                                         |
19  +-------------------------------------------------------------------------+
20  | Author: Aleksander Machniak <alec@alec.pl>                              |
21  +-------------------------------------------------------------------------+
22
23 */
24
25 /*
26     RFC2440: OpenPGP Message Format
27     RFC3156: MIME Security with OpenPGP
28     RFC3851: S/MIME
29 */
30
31 class enigma_engine
32 {
33     private $rc;
34     private $enigma;
35     private $pgp_driver;
36     private $smime_driver;
37
38     public $decryptions = array();
39     public $signatures = array();
40     public $signed_parts = array();
41
42
43     /**
44      * Plugin initialization.
45      */
46     function __construct($enigma)
47     {
48         $rcmail = rcmail::get_instance();
49         $this->rc = $rcmail;    
50         $this->enigma = $enigma;
51     }
52
53     /**
54      * PGP driver initialization.
55      */
56     function load_pgp_driver()
57     {
58         if ($this->pgp_driver)
59             return;
60
61         $driver = 'enigma_driver_' . $this->rc->config->get('enigma_pgp_driver', 'gnupg');
62         $username = $this->rc->user->get_username();
63
64         // Load driver
65         $this->pgp_driver = new $driver($username);
66
67         if (!$this->pgp_driver) {
68             raise_error(array(
69                 'code' => 600, 'type' => 'php',
70                 'file' => __FILE__, 'line' => __LINE__,
71                 'message' => "Enigma plugin: Unable to load PGP driver: $driver"
72             ), true, true);
73         }
74
75         // Initialise driver
76         $result = $this->pgp_driver->init();
77
78         if ($result instanceof enigma_error) {
79             raise_error(array(
80                 'code' => 600, 'type' => 'php',
81                 'file' => __FILE__, 'line' => __LINE__,
82                 'message' => "Enigma plugin: ".$result->getMessage()
83             ), true, true);
84         }
85     }
86
87     /**
88      * S/MIME driver initialization.
89      */
90     function load_smime_driver()
91     {
92         if ($this->smime_driver)
93             return;
94
95         // NOT IMPLEMENTED!
96         return;
97
98         $driver = 'enigma_driver_' . $this->rc->config->get('enigma_smime_driver', 'phpssl');
99         $username = $this->rc->user->get_username();
100
101         // Load driver
102         $this->smime_driver = new $driver($username);
103
104         if (!$this->smime_driver) {
105             raise_error(array(
106                 'code' => 600, 'type' => 'php',
107                 'file' => __FILE__, 'line' => __LINE__,
108                 'message' => "Enigma plugin: Unable to load S/MIME driver: $driver"
109             ), true, true);
110         }
111
112         // Initialise driver
113         $result = $this->smime_driver->init();
114
115         if ($result instanceof enigma_error) {
116             raise_error(array(
117                 'code' => 600, 'type' => 'php',
118                 'file' => __FILE__, 'line' => __LINE__,
119                 'message' => "Enigma plugin: ".$result->getMessage()
120             ), true, true);
121         }
122     }
123
124     /**
125      * Handler for plain/text message.
126      *
127      * @param array Reference to hook's parameters
128      */
129     function parse_plain(&$p)
130     {
131         $part = $p['structure'];
132
133         // Get message body from IMAP server
134         $this->set_part_body($part, $p['object']->uid);
135
136         // @TODO: big message body can be a file resource
137         // PGP signed message
138         if (preg_match('/^-----BEGIN PGP SIGNED MESSAGE-----/', $part->body)) {
139             $this->parse_plain_signed($p);
140         }
141         // PGP encrypted message
142         else if (preg_match('/^-----BEGIN PGP MESSAGE-----/', $part->body)) {
143             $this->parse_plain_encrypted($p);
144         }
145     }
146
147     /**
148      * Handler for multipart/signed message.
149      *
150      * @param array Reference to hook's parameters
151      */
152     function parse_signed(&$p)
153     {
154         $struct = $p['structure'];
155
156         // S/MIME
157         if ($struct->parts[1] && $struct->parts[1]->mimetype == 'application/pkcs7-signature') {
158             $this->parse_smime_signed($p);
159         }
160         // PGP/MIME:
161         // The multipart/signed body MUST consist of exactly two parts.
162         // The first part contains the signed data in MIME canonical format,
163         // including a set of appropriate content headers describing the data.
164         // The second body MUST contain the PGP digital signature.  It MUST be
165         // labeled with a content type of "application/pgp-signature".
166         else if ($struct->parts[1] && $struct->parts[1]->mimetype == 'application/pgp-signature') {
167             $this->parse_pgp_signed($p);
168         }
169     }
170
171     /**
172      * Handler for multipart/encrypted message.
173      *
174      * @param array Reference to hook's parameters
175      */
176     function parse_encrypted(&$p)
177     {
178         $struct = $p['structure'];
179
180         // S/MIME
181         if ($struct->mimetype == 'application/pkcs7-mime') {
182             $this->parse_smime_encrypted($p);
183         }
184         // PGP/MIME:
185         // The multipart/encrypted MUST consist of exactly two parts.  The first
186         // MIME body part must have a content type of "application/pgp-encrypted".
187         // This body contains the control information.
188         // The second MIME body part MUST contain the actual encrypted data.  It
189         // must be labeled with a content type of "application/octet-stream".
190         else if ($struct->parts[0] && $struct->parts[0]->mimetype == 'application/pgp-encrypted' &&
191             $struct->parts[1] && $struct->parts[1]->mimetype == 'application/octet-stream'
192         ) {
193             $this->parse_pgp_encrypted($p);
194         }
195     }
196
197     /**
198      * Handler for plain signed message.
199      * Excludes message and signature bodies and verifies signature.
200      *
201      * @param array Reference to hook's parameters
202      */
203     private function parse_plain_signed(&$p)
204     {
205         $this->load_pgp_driver();
206         $part = $p['structure'];
207
208         // Verify signature
209         if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
210             $sig = $this->pgp_verify($part->body);
211         }
212
213         // @TODO: Handle big bodies using (temp) files
214
215         // In this way we can use fgets on string as on file handle
216         $fh = fopen('php://memory', 'br+');
217         // @TODO: fopen/fwrite errors handling
218         if ($fh) {
219             fwrite($fh, $part->body);
220             rewind($fh);
221         }
222         $part->body = null;
223
224         // Extract body (and signature?)
225         while (!feof($fh)) {
226             $line = fgets($fh, 1024);
227
228             if ($part->body === null)
229                 $part->body = '';
230             else if (preg_match('/^-----BEGIN PGP SIGNATURE-----/', $line))
231                 break;
232             else
233                 $part->body .= $line;
234         }
235
236         // Remove "Hash" Armor Headers
237         $part->body = preg_replace('/^.*\r*\n\r*\n/', '', $part->body);
238         // de-Dash-Escape (RFC2440)
239         $part->body = preg_replace('/(^|\n)- -/', '\\1-', $part->body);
240
241         // Store signature data for display
242         if (!empty($sig)) {
243             $this->signed_parts[$part->mime_id] = $part->mime_id;
244             $this->signatures[$part->mime_id] = $sig;
245         }
246
247         fclose($fh);
248     }
249     
250     /**
251      * Handler for PGP/MIME signed message.
252      * Verifies signature.
253      *
254      * @param array Reference to hook's parameters
255      */
256     private function parse_pgp_signed(&$p)
257     {
258         $this->load_pgp_driver();
259         $struct = $p['structure'];
260         
261         // Verify signature
262         if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
263             $msg_part = $struct->parts[0];
264             $sig_part = $struct->parts[1];
265         
266             // Get bodies
267             $this->set_part_body($msg_part, $p['object']->uid);
268             $this->set_part_body($sig_part, $p['object']->uid);
269
270             // Verify
271             $sig = $this->pgp_verify($msg_part->body, $sig_part->body);
272
273             // Store signature data for display
274             $this->signatures[$struct->mime_id] = $sig;
275
276             // Message can be multipart (assign signature to each subpart)
277             if (!empty($msg_part->parts)) {
278                 foreach ($msg_part->parts as $part)
279                     $this->signed_parts[$part->mime_id] = $struct->mime_id;
280             }
281             else
282                 $this->signed_parts[$msg_part->mime_id] = $struct->mime_id;
283
284             // Remove signature file from attachments list
285             unset($struct->parts[1]);
286         }
287     }
288
289     /**
290      * Handler for S/MIME signed message.
291      * Verifies signature.
292      *
293      * @param array Reference to hook's parameters
294      */
295     private function parse_smime_signed(&$p)
296     {
297         $this->load_smime_driver();
298     }
299
300     /**
301      * Handler for plain encrypted message.
302      *
303      * @param array Reference to hook's parameters
304      */
305     private function parse_plain_encrypted(&$p)
306     {
307         $this->load_pgp_driver();
308         $part = $p['structure'];
309         
310         // Get body
311         $this->set_part_body($part, $p['object']->uid);
312
313         // Decrypt 
314         $result = $this->pgp_decrypt($part->body);
315         
316         // Store decryption status
317         $this->decryptions[$part->mime_id] = $result;
318         
319         // Parse decrypted message
320         if ($result === true) {
321             // @TODO
322         }
323     }
324     
325     /**
326      * Handler for PGP/MIME encrypted message.
327      *
328      * @param array Reference to hook's parameters
329      */
330     private function parse_pgp_encrypted(&$p)
331     {
332         $this->load_pgp_driver();
333         $struct = $p['structure'];
334         $part = $struct->parts[1];
335         
336         // Get body
337         $this->set_part_body($part, $p['object']->uid);
338
339         // Decrypt
340         $result = $this->pgp_decrypt($part->body);
341
342         $this->decryptions[$part->mime_id] = $result;
343 //print_r($part);
344         // Parse decrypted message
345         if ($result === true) {
346             // @TODO
347         }
348         else {
349             // Make sure decryption status message will be displayed
350             $part->type = 'content';
351             $p['object']->parts[] = $part;
352         }
353     }
354
355     /**
356      * Handler for S/MIME encrypted message.
357      *
358      * @param array Reference to hook's parameters
359      */
360     private function parse_smime_encrypted(&$p)
361     {
362         $this->load_smime_driver();
363     }
364
365     /**
366      * PGP signature verification.
367      *
368      * @param mixed Message body
369      * @param mixed Signature body (for MIME messages)
370      *
371      * @return mixed enigma_signature or enigma_error
372      */
373     private function pgp_verify(&$msg_body, $sig_body=null)
374     {
375         // @TODO: Handle big bodies using (temp) files
376         // @TODO: caching of verification result
377         
378          $sig = $this->pgp_driver->verify($msg_body, $sig_body);
379
380          if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::E_KEYNOTFOUND)
381              raise_error(array(
382                 'code' => 600, 'type' => 'php',
383                 'file' => __FILE__, 'line' => __LINE__,
384                 'message' => "Enigma plugin: " . $error->getMessage()
385                 ), true, false);
386
387 //print_r($sig);
388         return $sig;
389     }
390
391     /**
392      * PGP message decryption.
393      *
394      * @param mixed Message body
395      *
396      * @return mixed True or enigma_error
397      */
398     private function pgp_decrypt(&$msg_body)
399     {
400         // @TODO: Handle big bodies using (temp) files
401         // @TODO: caching of verification result
402         
403         $result = $this->pgp_driver->decrypt($msg_body, $key, $pass);
404
405 //print_r($result);
406
407         if ($result instanceof enigma_error) {
408             $err_code = $result->getCode();
409             if (!in_array($err_code, array(enigma_error::E_KEYNOTFOUND, enigma_error::E_BADPASS)))
410                 raise_error(array(
411                     'code' => 600, 'type' => 'php',
412                     'file' => __FILE__, 'line' => __LINE__,
413                     'message' => "Enigma plugin: " . $result->getMessage()
414                     ), true, false);
415             return $result;
416         }
417
418 //        $msg_body = $result;
419         return true;
420     }
421
422     /**
423      * PGP keys listing.
424      *
425      * @param mixed Key ID/Name pattern
426      *
427      * @return mixed Array of keys or enigma_error
428      */
429     function list_keys($pattern='')
430     {
431         $this->load_pgp_driver();
432         $result = $this->pgp_driver->list_keys($pattern);
433     
434         if ($result instanceof enigma_error) {
435             raise_error(array(
436                 'code' => 600, 'type' => 'php',
437                 'file' => __FILE__, 'line' => __LINE__,
438                 'message' => "Enigma plugin: " . $result->getMessage()
439                 ), true, false);
440         }
441         
442         return $result;
443     }
444
445     /**
446      * PGP key details.
447      *
448      * @param mixed Key ID
449      *
450      * @return mixed enigma_key or enigma_error
451      */
452     function get_key($keyid)
453     {
454         $this->load_pgp_driver();
455         $result = $this->pgp_driver->get_key($keyid);
456     
457         if ($result instanceof enigma_error) {
458             raise_error(array(
459                 'code' => 600, 'type' => 'php',
460                 'file' => __FILE__, 'line' => __LINE__,
461                 'message' => "Enigma plugin: " . $result->getMessage()
462                 ), true, false);
463         }
464         
465         return $result;
466     }
467
468     /**
469      * PGP keys/certs importing.
470      *
471      * @param mixed   Import file name or content
472      * @param boolean True if first argument is a filename
473      *
474      * @return mixed Import status data array or enigma_error
475      */
476     function import_key($content, $isfile=false)
477     {
478         $this->load_pgp_driver();
479         $result = $this->pgp_driver->import($content, $isfile);
480
481         if ($result instanceof enigma_error) {
482             raise_error(array(
483                 'code' => 600, 'type' => 'php',
484                 'file' => __FILE__, 'line' => __LINE__,
485                 'message' => "Enigma plugin: " . $result->getMessage()
486                 ), true, false);
487         }
488         else {
489             $result['imported'] = $result['public_imported'] + $result['private_imported'];
490             $result['unchanged'] = $result['public_unchanged'] + $result['private_unchanged'];
491         }
492
493         return $result;
494     }
495
496     /**
497      * Handler for keys/certs import request action
498      */
499     function import_file()
500     {
501         $uid = get_input_value('_uid', RCUBE_INPUT_POST);
502         $mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
503         $mime_id = get_input_value('_part', RCUBE_INPUT_POST);
504
505         if ($uid && $mime_id) {
506             $part = $this->rc->imap->get_message_part($uid, $mime_id);
507         }
508
509         if ($part && is_array($result = $this->import_key($part))) {
510             $this->rc->output->show_message('enigma.keysimportsuccess', 'confirmation',
511                 array('new' => $result['imported'], 'old' => $result['unchanged']));
512         }
513         else
514             $this->rc->output->show_message('enigma.keysimportfailed', 'error');
515     
516         $this->rc->output->send();
517     }
518
519     /**
520      * Checks if specified message part contains body data.
521      * If body is not set it will be fetched from IMAP server.
522      *
523      * @param rcube_message_part Message part object
524      * @param integer            Message UID
525      */
526     private function set_part_body($part, $uid)
527     {
528         // @TODO: Create such function in core
529         // @TODO: Handle big bodies using file handles
530         if (!isset($part->body)) {
531             $part->body = $this->rc->imap->get_message_part(
532                 $uid, $part->mime_id, $part);
533         }
534     }
535
536     /**
537      * Adds CSS style file to the page header.
538      */
539     private function add_css()
540     {
541         $skin = $this->rc->config->get('skin');
542         if (!file_exists($this->home . "/skins/$skin/enigma.css"))
543             $skin = 'default';
544
545         $this->include_stylesheet("skins/$skin/enigma.css");                                                
546     }
547 }