1: <?php
2: /**
3: * Copyright 2002-2014 Horde LLC (http://www.horde.org/)
4: *
5: * See the enclosed file COPYING for license information (GPL). If you
6: * did not receive this file, see http://www.horde.org/licenses/gpl.
7: *
8: * @category Horde
9: * @copyright 2002-2014 Horde LLC
10: * @license http://www.horde.org/licenses/gpl GPL
11: * @package IMP
12: */
13:
14: /**
15: * The IMP_Crypt_Pgp:: class contains all functions related to handling
16: * PGP messages within IMP.
17: *
18: * @author Michael Slusarz <slusarz@horde.org>
19: * @category Horde
20: * @copyright 2002-2014 Horde LLC
21: * @license http://www.horde.org/licenses/gpl GPL
22: * @package IMP
23: */
24: class IMP_Crypt_Pgp extends Horde_Crypt_Pgp
25: {
26: /* Name of PGP public key field in addressbook. */
27: const PUBKEY_FIELD = 'pgpPublicKey';
28:
29: /* Encryption type constants. */
30: const ENCRYPT = 'pgp_encrypt';
31: const SIGN = 'pgp_sign';
32: const SIGNENC = 'pgp_signenc';
33: const SYM_ENCRYPT = 'pgp_sym_enc';
34: const SYM_SIGNENC = 'pgp_syn_sign';
35:
36: /**
37: * Return the list of available encryption options for composing.
38: *
39: * @return array Keys are encryption type constants, values are gettext
40: * strings describing the encryption type.
41: */
42: public function encryptList()
43: {
44: $ret = array(
45: self::ENCRYPT => _("PGP Encrypt Message")
46: );
47:
48: if ($this->getPersonalPrivateKey()) {
49: $ret += array(
50: self::SIGN => _("PGP Sign Message"),
51: self::SIGNENC => _("PGP Sign/Encrypt Message")
52: );
53: }
54:
55: return $ret + array(
56: self::SYM_ENCRYPT => _("PGP Encrypt Message with passphrase"),
57: self::SYM_SIGNENC => _("PGP Sign/Encrypt Message with passphrase")
58: );
59: }
60:
61: /**
62: * Generate the personal Public/Private keypair and store in prefs.
63: *
64: * @param string $name See Horde_Crypt_Pgp::.
65: * @param string $email See Horde_Crypt_Pgp::.
66: * @param string $passphrase See Horde_Crypt_Pgp::.
67: * @param string $comment See Horde_Crypt_Pgp::.
68: * @param string $keylength See Horde_Crypt_Pgp::.
69: * @param integer $expire See Horde_Crypt_Pgp::.
70: *
71: * @throws Horde_Crypt_Exception
72: */
73: public function generatePersonalKeys($name, $email, $passphrase,
74: $comment = '', $keylength = 1024,
75: $expire = null)
76: {
77: $keys = $this->generateKey($name, $email, $passphrase, $comment, $keylength, $expire);
78:
79: /* Store the keys in the user's preferences. */
80: $this->addPersonalPublicKey($keys['public']);
81: $this->addPersonalPrivateKey($keys['private']);
82: }
83:
84: /**
85: * Add the personal public key to the prefs.
86: *
87: * @param mixed $public_key The public key to add (either string or
88: * array).
89: */
90: public function addPersonalPublicKey($public_key)
91: {
92: $GLOBALS['prefs']->setValue('pgp_public_key', trim($public_key));
93: }
94:
95: /**
96: * Add the personal private key to the prefs.
97: *
98: * @param mixed $private_key The private key to add (either string or
99: * array).
100: */
101: public function addPersonalPrivateKey($private_key)
102: {
103: $GLOBALS['prefs']->setValue('pgp_private_key', trim($private_key));
104: }
105:
106: /**
107: * Get the personal public key from the prefs.
108: *
109: * @return string The personal PGP public key.
110: */
111: public function getPersonalPublicKey()
112: {
113: return $GLOBALS['prefs']->getValue('pgp_public_key');
114: }
115:
116: /**
117: * Get the personal private key from the prefs.
118: *
119: * @return string The personal PGP private key.
120: */
121: public function getPersonalPrivateKey()
122: {
123: return $GLOBALS['prefs']->getValue('pgp_private_key');
124: }
125:
126: /**
127: * Deletes the specified personal keys from the prefs.
128: */
129: public function deletePersonalKeys()
130: {
131: $GLOBALS['prefs']->setValue('pgp_public_key', '');
132: $GLOBALS['prefs']->setValue('pgp_private_key', '');
133:
134: $this->unsetPassphrase('personal');
135: }
136:
137: /**
138: * Add a public key to an address book.
139: *
140: * @param string $public_key An PGP public key.
141: *
142: * @return array See Horde_Crypt_Pgp::pgpPacketInformation()
143: * @throws Horde_Crypt_Exception
144: * @throws Horde_Exception
145: */
146: public function addPublicKey($public_key)
147: {
148: /* Make sure the key is valid. */
149: $key_info = $this->pgpPacketInformation($public_key);
150: if (!isset($key_info['signature'])) {
151: throw new Horde_Crypt_Exception(_("Not a valid public key."));
152: }
153:
154: /* Remove the '_SIGNATURE' entry. */
155: unset($key_info['signature']['_SIGNATURE']);
156:
157: /* Store all signatures that appear in the key. */
158: foreach ($key_info['signature'] as $id => $sig) {
159: /* Check to make sure the key does not already exist in ANY
160: * address book and remove the id from the key_info for a correct
161: * output. */
162: try {
163: $result = $this->getPublicKey($sig['email'], array('nocache' => true, 'noserver' => true));
164: if (!empty($result)) {
165: unset($key_info['signature'][$id]);
166: continue;
167: }
168: } catch (Horde_Crypt_Exception $e) {}
169:
170: /* Add key to the user's address book. */
171: $GLOBALS['registry']->call('contacts/addField', array($sig['email'], $sig['name'], self::PUBKEY_FIELD, $public_key, $GLOBALS['prefs']->getValue('add_source')));
172: }
173:
174: return $key_info;
175: }
176:
177: /**
178: * Retrieves a public key by e-mail.
179: *
180: * First, the key will be attempted to be retrieved from a user's address
181: * book(s).
182: * Second, if unsuccessful, the key is attempted to be retrieved via a
183: * public PGP keyserver.
184: *
185: * @param string $address The e-mail address to search by.
186: * @param array $options Additional options:
187: * - keyid: (string) The key ID of the user's key.
188: * DEFAULT: key ID not used
189: * - nocache: (boolean) Don't retrieve from cache?
190: * DEFAULT: false
191: * - noserver: (boolean) Whether to check the public key servers for the
192: * key.
193: * DEFAULT: false
194: *
195: * @return string The PGP public key requested.
196: * @throws Horde_Crypt_Exception
197: */
198: public function getPublicKey($address, $options = array())
199: {
200: global $injector, $registry;
201:
202: $keyid = empty($options['keyid'])
203: ? ''
204: : $options['keyid'];
205:
206: /* If there is a cache driver configured, try to get the public key
207: * from the cache. */
208: if (empty($options['nocache']) && ($cache = $injector->getInstance('Horde_Cache'))) {
209: $result = $cache->get("PGPpublicKey_" . $address . $keyid, 3600);
210: if ($result) {
211: Horde::log('PGPpublicKey: ' . serialize($result), 'DEBUG');
212: return $result;
213: }
214: }
215:
216: try {
217: $key = $injector->getInstance('Horde_Core_Hooks')->callHook(
218: 'pgp_key',
219: 'imp',
220: array($address, $keyid)
221: );
222: if ($key) {
223: return $key;
224: }
225: } catch (Horde_Exception_HookNotSet $e) {}
226:
227: /* Try retrieving by e-mail only first. */
228: $result = null;
229: try {
230: $result = $registry->call(
231: 'contacts/getField',
232: array(
233: $address,
234: self::PUBKEY_FIELD,
235: $injector->getInstance('IMP_Contacts')->sources,
236: true,
237: true
238: )
239: );
240: } catch (Horde_Exception $e) {}
241:
242: if (is_null($result)) {
243: /* TODO: Retrieve by ID. */
244:
245: /* See if the address points to the user's public key. */
246: $identity = $injector->getInstance('IMP_Identity');
247: $personal_pubkey = $this->getPersonalPublicKey();
248: if (!empty($personal_pubkey) && $identity->hasAddress($address)) {
249: $result = $personal_pubkey;
250: } elseif (empty($options['noserver'])) {
251: $result = null;
252:
253: try {
254: foreach ($this->_keyserverList() as $val) {
255: try {
256: $result = $val->get(
257: empty($keyid) ? $val->getKeyId($address) : $keyid
258: );
259: break;
260: } catch (Exception $e) {}
261: }
262:
263: if (is_null($result)) {
264: throw $e;
265: }
266:
267: /* If there is a cache driver configured and a cache
268: * object exists, store the retrieved public key in the
269: * cache. */
270: if (is_object($cache)) {
271: $cache->set("PGPpublicKey_" . $address . $keyid, $result, 3600);
272: }
273: } catch (Horde_Crypt_Exception $e) {
274: /* Return now, if no public key found at all. */
275: Horde::log('PGPpublicKey: ' . $e->getMessage(), 'DEBUG');
276: throw new Horde_Crypt_Exception(sprintf(_("Could not retrieve public key for %s."), $address));
277: }
278: } else {
279: $result = '';
280: }
281: }
282:
283: /* If more than one public key is returned, just return the first in
284: * the array. There is no way of knowing which is the "preferred" key,
285: * if the keys are different. */
286: if (is_array($result)) {
287: reset($result);
288: }
289:
290: return $result;
291: }
292:
293: /**
294: * Retrieves all public keys from a user's address book(s).
295: *
296: * @return array All PGP public keys available.
297: * @throws Horde_Crypt_Exception
298: */
299: public function listPublicKeys()
300: {
301: $sources = $GLOBALS['injector']->getInstance('IMP_Contacts')->sources;
302:
303: return empty($sources)
304: ? array()
305: : $GLOBALS['registry']->call('contacts/getAllAttributeValues', array(self::PUBKEY_FIELD, $sources));
306: }
307:
308: /**
309: * Deletes a public key from a user's address book(s) by e-mail.
310: *
311: * @param string $email The e-mail address to delete.
312: *
313: * @throws Horde_Crypt_Exception
314: */
315: public function deletePublicKey($email)
316: {
317: return $GLOBALS['registry']->call(
318: 'contacts/deleteField',
319: array(
320: $email,
321: self::PUBKEY_FIELD,
322: $GLOBALS['injector']->getInstance('IMP_Contacts')->sources
323: )
324: );
325: }
326:
327: /**
328: * Send a public key to a public PGP keyserver.
329: *
330: * @param string $pubkey The PGP public key.
331: *
332: * @throws Horde_Crypt_Exception
333: */
334: public function sendToPublicKeyserver($pubkey)
335: {
336: $servers = $this->_keyserverList();
337: $servers[0]->put($pubkey);
338: }
339:
340: /**
341: * Verifies a signed message with a given public key.
342: *
343: * @param string $text The text to verify.
344: * @param string $address E-mail address of public key.
345: * @param string $signature A PGP signature block.
346: * @param string $charset Charset to use.
347: *
348: * @return stdClass See Horde_Crypt_Pgp::decrypt().
349: * @throws Horde_Crypt_Exception
350: */
351: public function verifySignature($text, $address, $signature = '',
352: $charset = null)
353: {
354: if (!empty($signature)) {
355: $packet_info = $this->pgpPacketInformation($signature);
356: if (isset($packet_info['keyid'])) {
357: $keyid = $packet_info['keyid'];
358: }
359: }
360:
361: if (!isset($keyid)) {
362: $keyid = $this->getSignersKeyID($text);
363: }
364:
365: /* Get key ID of key. */
366: $public_key = $this->getPublicKey($address, array('keyid' => $keyid));
367:
368: if (empty($signature)) {
369: $options = array('type' => 'signature');
370: } else {
371: $options = array('type' => 'detached-signature', 'signature' => $signature);
372: }
373: $options['pubkey'] = $public_key;
374:
375: if (!empty($charset)) {
376: $options['charset'] = $charset;
377: }
378:
379: return $this->decrypt($text, $options);
380: }
381:
382: /**
383: * Decrypt a message with user's public/private keypair or a passphrase.
384: *
385: * @param string $text The text to decrypt.
386: * @param string $type Either 'literal', 'personal', or 'symmetric'.
387: * @param array $opts Additional options:
388: * - passphrase: (boolean) If $type is 'personal' or 'symmetrical', the
389: * passphrase to use.
390: * - sender: (string) The sender of the message (used to check signature
391: * if message is both encrypted & signed).
392: *
393: * @return stdClass See Horde_Crypt_Pgp::decrypt().
394: * @throws Horde_Crypt_Exception
395: */
396: public function decryptMessage($text, $type, array $opts = array())
397: {
398: $opts = array_merge(array(
399: 'passphrase' => null
400: ), $opts);
401:
402: $pubkey = $this->getPersonalPublicKey();
403: if (isset($opts['sender'])) {
404: try {
405: $pubkey .= "\n" . $this->getPublicKey($opts['sender']);
406: } catch (Horde_Crypt_Exception $e) {}
407: }
408:
409: switch ($type) {
410: case 'literal':
411: return $this->decrypt($text, array(
412: 'no_passphrase' => true,
413: 'pubkey' => $pubkey,
414: 'type' => 'message'
415: ));
416: break;
417:
418: case 'symmetric':
419: return $this->decrypt($text, array(
420: 'passphrase' => $opts['passphrase'],
421: 'pubkey' => $pubkey,
422: 'type' => 'message'
423: ));
424: break;
425:
426: case 'personal':
427: return $this->decrypt($text, array(
428: 'passphrase' => $opts['passphrase'],
429: 'privkey' => $this->getPersonalPrivateKey(),
430: 'pubkey' => $pubkey,
431: 'type' => 'message'
432: ));
433: }
434: }
435:
436: /**
437: * Gets a passphrase from the session cache.
438: *
439: * @param integer $type The type of passphrase. Either 'personal' or
440: * 'symmetric'.
441: * @param string $id If $type is 'symmetric', the ID of the stored
442: * passphrase.
443: *
444: * @return mixed The passphrase, if set, or null.
445: */
446: public function getPassphrase($type, $id = null)
447: {
448: if ($type == 'personal') {
449: $id = 'personal';
450: }
451:
452: return (($cache = $GLOBALS['session']->get('imp', 'pgp')) && isset($cache[$type][$id]))
453: ? $cache[$type][$id]
454: : null;
455: }
456:
457: /**
458: * Store's the user's passphrase in the session cache.
459: *
460: * @param integer $type The type of passphrase. Either 'personal' or
461: * 'symmetric'.
462: * @param string $passphrase The user's passphrase.
463: * @param string $id If $type is 'symmetric', the ID of the
464: * stored passphrase.
465: *
466: * @return boolean Returns true if correct passphrase, false if incorrect.
467: */
468: public function storePassphrase($type, $passphrase, $id = null)
469: {
470: global $session;
471:
472: if ($type == 'personal') {
473: if ($this->verifyPassphrase($this->getPersonalPublicKey(), $this->getPersonalPrivateKey(), $passphrase) === false) {
474: return false;
475: }
476: $id = 'personal';
477: }
478:
479: $cache = $session->get('imp', 'pgp', Horde_Session::TYPE_ARRAY);
480: $cache[$type][$id] = $passphrase;
481: $session->set('imp', 'pgp', $cache, $session::ENCRYPT);
482:
483: return true;
484: }
485:
486: /**
487: * Clear the passphrase from the session cache.
488: *
489: * @param integer $type The type of passphrase. Either 'personal' or
490: * 'symmetric'.
491: * @param string $id If $type is 'symmetric', the ID of the
492: * stored passphrase. Else, all passphrases
493: * are deleted.
494: */
495: public function unsetPassphrase($type, $id = null)
496: {
497: if ($cache = $GLOBALS['session']->get('imp', 'pgp')) {
498: if (($type == 'symmetric') && !is_null($id)) {
499: unset($cache['symmetric'][$id]);
500: } else {
501: unset($cache[$type]);
502: }
503: $GLOBALS['session']->set('imp', 'pgp', $cache);
504: }
505: }
506:
507: /**
508: * Generates a cache ID for symmetric message data.
509: *
510: * @param string $mailbox The mailbox of the message.
511: * @param integer $uid The UID of the message.
512: * @param string $id The MIME ID of the message.
513: *
514: * @return string A unique symmetric cache ID.
515: */
516: public function getSymmetricId($mailbox, $uid, $id)
517: {
518: return implode('|', array($mailbox, $uid, $id));
519: }
520:
521: /**
522: * Provide the list of parameters needed for signing a message.
523: *
524: * @return array The list of parameters needed by encrypt().
525: */
526: protected function _signParameters()
527: {
528: return array(
529: 'pubkey' => $this->getPersonalPublicKey(),
530: 'privkey' => $this->getPersonalPrivateKey(),
531: 'passphrase' => $this->getPassphrase('personal')
532: );
533: }
534:
535: /**
536: * Provide the list of parameters needed for encrypting a message.
537: *
538: * @param Horde_Mail_Rfc822_List $addresses The e-mail address of the
539: * keys to use for encryption.
540: * @param string $symmetric If true, the symmetric
541: * password to use for
542: * encrypting. If null, uses the
543: * personal key.
544: *
545: * @return array The list of parameters needed by encrypt().
546: * @throws Horde_Crypt_Exception
547: */
548: protected function _encryptParameters(Horde_Mail_Rfc822_List $addresses,
549: $symmetric)
550: {
551: if (!is_null($symmetric)) {
552: return array(
553: 'symmetric' => true,
554: 'passphrase' => $symmetric
555: );
556: }
557:
558: $addr_list = array();
559:
560: foreach ($addresses as $val) {
561: /* Get the public key for the address. */
562: $bare_addr = $val->bare_address;
563: $addr_list[$bare_addr] = $this->getPublicKey($bare_addr);
564: }
565:
566: return array('recips' => $addr_list);
567: }
568:
569: /**
570: * Sign a Horde_Mime_Part using PGP using IMP default parameters.
571: *
572: * @param Horde_Mime_Part $mime_part The object to sign.
573: *
574: * @return Horde_Mime_Part See Horde_Crypt_Pgp::signMIMEPart().
575: * @throws Horde_Crypt_Exception
576: */
577: public function impSignMimePart($mime_part)
578: {
579: return $this->signMimePart($mime_part, $this->_signParameters());
580: }
581:
582: /**
583: * Encrypt a Horde_Mime_Part using PGP using IMP default parameters.
584: *
585: * @param Horde_Mime_Part $mime_part The object to encrypt.
586: * @param Horde_Mail_Rfc822_List $addresses The e-mail address of the
587: * keys to use for encryption.
588: * @param string $symmetric If true, the symmetric
589: * password to use for
590: * encrypting. If null, uses the
591: * personal key.
592: *
593: * @return Horde_Mime_Part See Horde_Crypt_Pgp::encryptMimePart().
594: * @throws Horde_Crypt_Exception
595: */
596: public function impEncryptMimePart($mime_part,
597: Horde_Mail_Rfc822_List $addresses,
598: $symmetric = null)
599: {
600: return $this->encryptMimePart($mime_part, $this->_encryptParameters($addresses, $symmetric));
601: }
602:
603: /**
604: * Sign and Encrypt a Horde_Mime_Part using PGP using IMP default
605: * parameters.
606: *
607: * @param Horde_Mime_Part $mime_part The object to sign and
608: * encrypt.
609: * @param Horde_Mail_Rfc822_List $addresses The e-mail address of the
610: * keys to use for encryption.
611: * @param string $symmetric If true, the symmetric
612: * password to use for
613: * encrypting. If null, uses the
614: * personal key.
615: *
616: * @return Horde_Mime_Part See Horde_Crypt_Pgp::signAndencryptMimePart().
617: * @throws Horde_Crypt_Exception
618: */
619: public function impSignAndEncryptMimePart($mime_part,
620: Horde_Mail_Rfc822_List $addresses,
621: $symmetric = null)
622: {
623: return $this->signAndEncryptMimePart($mime_part, $this->_signParameters(), $this->_encryptParameters($addresses, $symmetric));
624: }
625:
626: /**
627: * Generate a Horde_Mime_Part object, in accordance with RFC 2015/3156,
628: * that contains the user's public key.
629: *
630: * @return Horde_Mime_Part See Horde_Crypt_Pgp::publicKeyMimePart().
631: */
632: public function publicKeyMimePart($key = null)
633: {
634: return parent::publicKeyMimePart($this->getPersonalPublicKey());
635: }
636:
637: /**
638: * Extracts public/private keys from armor data.
639: *
640: * @param string $data Armor text.
641: *
642: * @return array Array with these keys:
643: * - public: (array) Array of public keys.
644: * - private: (array) Array of private keys.
645: */
646: public function getKeys($data)
647: {
648: global $injector;
649:
650: $out = array(
651: 'public' => array(),
652: 'private' => array()
653: );
654:
655: foreach ($injector->getInstance('Horde_Crypt_Pgp_Parse')->parse($data) as $val) {
656: switch ($val['type']) {
657: case Horde_Crypt_Pgp::ARMOR_PUBLIC_KEY:
658: case Horde_Crypt_Pgp::ARMOR_PRIVATE_KEY:
659: $key = implode("\n", $val['data']);
660: if ($key_info = $this->pgpPacketInformation($key)) {
661: if (($val['type'] == Horde_Crypt_Pgp::ARMOR_PUBLIC_KEY) &&
662: !empty($key_info['public_key'])) {
663: $out['public'][] = $key;
664: } elseif (($val['type'] == Horde_Crypt_Pgp::ARMOR_PRIVATE_KEY) &&
665: !empty($key_info['secret_key'])) {
666: $out['private'][] = $key;
667: }
668: }
669: break;
670: }
671: }
672:
673: if (!empty($out['private']) &&
674: empty($out['public']) &&
675: $res = $this->getPublicKeyFromPrivateKey(reset($out['private']))) {
676: $out['public'][] = $res;
677: }
678:
679: return $out;
680: }
681:
682: /**
683: * Return list of keyserver objects.
684: *
685: * @return array List of Horde_Crypt_Pgp_Keyserver objects.
686: * @throws Horde_Crypt_Exception
687: */
688: protected function _keyserverList()
689: {
690: global $conf, $injector;
691:
692: if (empty($conf['gnupg']['keyserver'])) {
693: throw new Horde_Crypt_Exception(_("Public PGP keyserver support has been disabled."));
694: }
695:
696: $http = $injector->getInstance('Horde_Core_Factory_HttpClient')->create();
697: if (!empty($conf['gnupg']['timeout'])) {
698: $http->{'request.timeout'} = $conf['gnupg']['timeout'];
699: }
700:
701: $out = array();
702: foreach ($conf['gnupg']['keyserver'] as $server) {
703: $out[] = new Horde_Crypt_Pgp_Keyserver($this, array(
704: 'http' => $http,
705: 'keyserver' => $server
706: ));
707: }
708:
709: return $out;
710: }
711:
712: }
713: