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_Smime:: class contains all functions related to handling
16: * S/MIME messages within IMP.
17: *
18: * @author Mike Cochrane <mike@graftonhall.co.nz>
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_Smime extends Horde_Crypt_Smime
25: {
26: /* Name of the S/MIME public key field in addressbook. */
27: const PUBKEY_FIELD = 'smimePublicKey';
28:
29: /* Encryption type constants. */
30: const ENCRYPT = 'smime_encrypt';
31: const SIGN = 'smime_sign';
32: const SIGNENC = 'smime_signenc';
33:
34: /**
35: * Return the list of available encryption options for composing.
36: *
37: * @return array Keys are encryption type constants, values are gettext
38: * strings describing the encryption type.
39: */
40: public function encryptList()
41: {
42: $ret = array();
43:
44: if ($GLOBALS['registry']->hasMethod('contacts/getField') ||
45: $GLOBALS['injector']->getInstance('Horde_Core_Hooks')->hookExists('smime_key', 'imp')) {
46: $ret += array(
47: self::ENCRYPT => _("S/MIME Encrypt Message")
48: );
49: }
50:
51: if ($this->getPersonalPrivateKey()) {
52: $ret += array(
53: self::SIGN => _("S/MIME Sign Message"),
54: self::SIGNENC => _("S/MIME Sign/Encrypt Message")
55: );
56: }
57:
58: return $ret;
59: }
60:
61: /**
62: * Add the personal public key to the prefs.
63: *
64: * @param mixed $key The public key to add (either string or array).
65: */
66: public function addPersonalPublicKey($key)
67: {
68: $GLOBALS['prefs']->setValue('smime_public_key', (is_array($key)) ? implode('', $key) : $key);
69: }
70:
71: /**
72: * Add the personal private key to the prefs.
73: *
74: * @param mixed $key The private key to add (either string or array).
75: */
76: public function addPersonalPrivateKey($key)
77: {
78: $GLOBALS['prefs']->setValue('smime_private_key', (is_array($key)) ? implode('', $key) : $key);
79: }
80:
81: /**
82: * Add the list of additional certs to the prefs.
83: *
84: * @param mixed $key The private key to add (either string or array).
85: */
86: public function addAdditionalCert($key)
87: {
88: $GLOBALS['prefs']->setValue('smime_additional_cert', (is_array($key)) ? implode('', $key) : $key);
89: }
90:
91: /**
92: * Get the personal public key from the prefs.
93: *
94: * @return string The personal S/MIME public key.
95: */
96: public function getPersonalPublicKey()
97: {
98: return $GLOBALS['prefs']->getValue('smime_public_key');
99: }
100:
101: /**
102: * Get the personal private key from the prefs.
103: *
104: * @return string The personal S/MIME private key.
105: */
106: public function getPersonalPrivateKey()
107: {
108: return $GLOBALS['prefs']->getValue('smime_private_key');
109: }
110:
111: /**
112: * Get any additional certificates from the prefs.
113: *
114: * @return string Additional signing certs for inclusion.
115: */
116: public function getAdditionalCert()
117: {
118: return $GLOBALS['prefs']->getValue('smime_additional_cert');
119: }
120:
121: /**
122: * Deletes the specified personal keys from the prefs.
123: */
124: public function deletePersonalKeys()
125: {
126: $GLOBALS['prefs']->setValue('smime_public_key', '');
127: $GLOBALS['prefs']->setValue('smime_private_key', '');
128: $GLOBALS['prefs']->setValue('smime_additional_cert', '');
129: $this->unsetPassphrase();
130: }
131:
132: /**
133: * Add a public key to an address book.
134: *
135: * @param string $cert A public certificate to add.
136: *
137: * @throws Horde_Exception
138: */
139: public function addPublicKey($cert)
140: {
141: list($name, $email) = $this->publicKeyInfo($cert);
142:
143: $GLOBALS['registry']->call('contacts/addField', array($email, $name, self::PUBKEY_FIELD, $cert, $GLOBALS['prefs']->getValue('add_source')));
144: }
145:
146: /**
147: * Get information about a public certificate.
148: *
149: * @param string $cert The public certificate.
150: *
151: * @return array Two element array: the name and e-mail for the cert.
152: * @throws Horde_Crypt_Exception
153: */
154: public function publicKeyInfo($cert)
155: {
156: /* Make sure the certificate is valid. */
157: $key_info = openssl_x509_parse($cert);
158: if (!is_array($key_info) || !isset($key_info['subject'])) {
159: throw new Horde_Crypt_Exception(_("Not a valid public key."));
160: }
161:
162: /* Add key to the user's address book. */
163: $email = $this->getEmailFromKey($cert);
164: if (is_null($email)) {
165: throw new Horde_Crypt_Exception(_("No email information located in the public key."));
166: }
167:
168: /* Get the name corresponding to this key. */
169: if (isset($key_info['subject']['CN'])) {
170: $name = $key_info['subject']['CN'];
171: } elseif (isset($key_info['subject']['OU'])) {
172: $name = $key_info['subject']['OU'];
173: } else {
174: $name = $email;
175: }
176:
177: return array($name, $email);
178: }
179:
180: /**
181: * Returns the params needed to encrypt a message being sent to the
182: * specified email address.
183: *
184: * @param Horde_Mail_Rfc822_Address $address The e-mail address of the
185: * recipient.
186: *
187: * @return array The list of parameters needed by encrypt().
188: * @throws Horde_Crypt_Exception
189: */
190: protected function _encryptParameters(Horde_Mail_Rfc822_Address $address)
191: {
192: /* We can only encrypt if we are sending to a single person. */
193: return array(
194: 'pubkey' => $this->getPublicKey($address->bare_address),
195: 'type' => 'message'
196: );
197: }
198:
199: /**
200: * Retrieves a public key by e-mail.
201: * The key will be retrieved from a user's address book(s).
202: *
203: * @param string $address The e-mail address to search for.
204: *
205: * @return string The S/MIME public key requested.
206: * @throws Horde_Exception
207: */
208: public function getPublicKey($address)
209: {
210: global $injector, $registry;
211:
212: try {
213: $key = $injector->getInstance('Horde_Core_Hooks')->callHook(
214: 'smime_key',
215: 'imp',
216: array($address)
217: );
218: if ($key) {
219: return $key;
220: }
221: } catch (Horde_Exception_HookNotSet $e) {}
222:
223: $contacts = $injector->getInstance('IMP_Contacts');
224:
225: try {
226: $key = $registry->call('contacts/getField', array($address, self::PUBKEY_FIELD, $contacts->sources, true, true));
227: } catch (Horde_Exception $e) {
228: /* See if the address points to the user's public key. */
229: $personal_pubkey = $this->getPersonalPublicKey();
230: if (!empty($personal_pubkey) &&
231: $injector->getInstance('IMP_Identity')->hasAddress($address)) {
232: return $personal_pubkey;
233: }
234:
235: throw $e;
236: }
237:
238: /* If more than one public key is returned, just return the first in
239: * the array. There is no way of knowing which is the "preferred" key,
240: * if the keys are different. */
241: return is_array($key) ? reset($key) : $key;
242: }
243:
244: /**
245: * Retrieves all public keys from a user's address book(s).
246: *
247: * @return array All S/MIME public keys available.
248: * @throws Horde_Crypt_Exception
249: */
250: public function listPublicKeys()
251: {
252: $sources = $GLOBALS['injector']->getInstance('IMP_Contacts')->sources;
253:
254: return empty($sources)
255: ? array()
256: : $GLOBALS['registry']->call('contacts/getAllAttributeValues', array(self::PUBKEY_FIELD, $sources));
257: }
258:
259: /**
260: * Deletes a public key from a user's address book(s) by e-mail.
261: *
262: * @param string $email The e-mail address to delete.
263: *
264: * @throws Horde_Crypt_Exception
265: */
266: public function deletePublicKey($email)
267: {
268: global $injector, $registry;
269:
270: $registry->call(
271: 'contacts/deleteField',
272: array(
273: $email,
274: self::PUBKEY_FIELD,
275: $injector->getInstance('IMP_Contacts')->sources,
276: $params['sources']
277: )
278: );
279: }
280:
281: /**
282: * Returns the parameters needed for signing a message.
283: *
284: * @return array The list of parameters needed by encrypt().
285: */
286: protected function _signParameters()
287: {
288: return array(
289: 'type' => 'signature',
290: 'pubkey' => $this->getPersonalPublicKey(),
291: 'privkey' => $this->getPersonalPrivateKey(),
292: 'passphrase' => $this->getPassphrase(),
293: 'sigtype' => 'detach',
294: 'certs' => $this->getAdditionalCert()
295: );
296: }
297:
298: /**
299: * Verifies a signed message with a given public key.
300: *
301: * @param string $text The text to verify.
302: *
303: * @return stdClass See Horde_Crypt_Smime::verify().
304: * @throws Horde_Crypt_Exception
305: */
306: public function verifySignature($text)
307: {
308: return $this->verify($text, empty($GLOBALS['conf']['openssl']['cafile']) ? array() : $GLOBALS['conf']['openssl']['cafile']);
309: }
310:
311: /**
312: * Decrypt a message with user's public/private keypair.
313: *
314: * @param string $text The text to decrypt.
315: *
316: * @return string See Horde_Crypt_Smime::decrypt().
317: * @throws Horde_Crypt_Exception
318: */
319: public function decryptMessage($text)
320: {
321: return $this->decrypt($text, array(
322: 'type' => 'message',
323: 'pubkey' => $this->getPersonalPublicKey(),
324: 'privkey' => $this->getPersonalPrivateKey(),
325: 'passphrase' => $this->getPassphrase()
326: ));
327: }
328:
329: /**
330: * Gets the user's passphrase from the session cache.
331: *
332: * @return mixed The passphrase, if set. Returns false if the passphrase
333: * has not been loaded yet. Returns null if no passphrase
334: * is needed.
335: */
336: public function getPassphrase()
337: {
338: global $session;
339:
340: $private_key = $GLOBALS['prefs']->getValue('smime_private_key');
341: if (empty($private_key)) {
342: return false;
343: }
344:
345: if ($session->exists('imp', 'smime_passphrase')) {
346: return $session->get('imp', 'smime_passphrase');
347: } elseif (!$session->exists('imp', 'smime_null_passphrase')) {
348: $session->set(
349: 'imp',
350: 'smime_null_passphrase',
351: $this->verifyPassphrase($private_key, null)
352: ? null
353: : false
354: );
355: }
356:
357: return $session->get('imp', 'smime_null_passphrase');
358: }
359:
360: /**
361: * Store's the user's passphrase in the session cache.
362: *
363: * @param string $passphrase The user's passphrase.
364: *
365: * @return boolean Returns true if correct passphrase, false if incorrect.
366: */
367: public function storePassphrase($passphrase)
368: {
369: global $session;
370:
371: if ($this->verifyPassphrase($this->getPersonalPrivateKey(), $passphrase) !== false) {
372: $session->set('imp', 'smime_passphrase', $passphrase, $session::ENCRYPT);
373: return true;
374: }
375:
376: return false;
377: }
378:
379: /**
380: * Clear the passphrase from the session cache.
381: */
382: public function unsetPassphrase()
383: {
384: global $session;
385:
386: $session->remove('imp', 'smime_null_passphrase');
387: $session->remove('imp', 'smime_passphrase');
388: }
389:
390: /**
391: * Encrypt a MIME_Part using S/MIME using IMP defaults.
392: *
393: * @param Horde_Mime_Part $mime_part The object to encrypt.
394: * @param Horde_Mail_Rfc822_Address $to_address The e-mail address of the
395: * key to use for encryption.
396: *
397: * @return MIME_Part See Horde_Crypt_Smime::encryptMIMEPart().
398: * @throws Horde_Crypt_Exception
399: */
400: public function IMPencryptMIMEPart($mime_part,
401: Horde_Mail_Rfc822_Address $to_address)
402: {
403: return $this->encryptMIMEPart($mime_part, $this->_encryptParameters($to_address));
404: }
405:
406: /**
407: * Sign a MIME_Part using S/MIME using IMP defaults.
408: *
409: * @param MIME_Part $mime_part The MIME_Part object to sign.
410: *
411: * @return MIME_Part See Horde_Crypt_Smime::signMIMEPart().
412: * @throws Horde_Crypt_Exception
413: */
414: public function IMPsignMIMEPart($mime_part)
415: {
416: return $this->signMIMEPart($mime_part, $this->_signParameters());
417: }
418:
419: /**
420: * Sign and encrypt a MIME_Part using S/MIME using IMP defaults.
421: *
422: * @param Horde_Mime_Part $mime_part The object to sign and
423: * encrypt.
424: * @param Horde_Mail_Rfc822_Address $to_address The e-mail address of the
425: * key to use for encryption.
426: *
427: * @return MIME_Part See Horde_Crypt_Smime::signAndencryptMIMEPart().
428: * @throws Horde_Crypt_Exception
429: */
430: public function IMPsignAndEncryptMIMEPart($mime_part,
431: Horde_Mail_Rfc822_Address $to_address)
432: {
433: return $this->signAndEncryptMIMEPart($mime_part, $this->_signParameters(), $this->_encryptParameters($to_address));
434: }
435:
436: /**
437: * Store the public/private/additional certificates in the preferences
438: * from a given PKCS 12 file.
439: *
440: * @param string $pkcs12 The PKCS 12 data.
441: * @param string $password The password of the PKCS 12 file.
442: * @param string $pkpass The password to use to encrypt the private key.
443: *
444: * @throws Horde_Crypt_Exception
445: */
446: public function addFromPKCS12($pkcs12, $password, $pkpass = null)
447: {
448: $sslpath = empty($GLOBALS['conf']['openssl']['path'])
449: ? null
450: : $GLOBALS['conf']['openssl']['path'];
451:
452: $params = array('sslpath' => $sslpath, 'password' => $password);
453: if (!empty($pkpass)) {
454: $params['newpassword'] = $pkpass;
455: }
456:
457: $result = $this->parsePKCS12Data($pkcs12, $params);
458: $this->addPersonalPrivateKey($result->private);
459: $this->addPersonalPublicKey($result->public);
460: $this->addAdditionalCert($result->certs);
461: }
462:
463: /**
464: * Extract the contents from signed S/MIME data.
465: *
466: * @param string $data The signed S/MIME data.
467: *
468: * @return string The contents embedded in the signed data.
469: * @throws Horde_Crypt_Exception
470: */
471: public function extractSignedContents($data, $sslpath = null)
472: {
473: $sslpath = empty($GLOBALS['conf']['openssl']['path'])
474: ? null
475: : $GLOBALS['conf']['openssl']['path'];
476:
477: return parent::extractSignedContents($data, $sslpath);
478: }
479:
480: }
481: