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