Files
server/usr/share/psa-horde/imp/lib/Crypt/Smime.php
2026-01-07 20:52:11 +01:00

472 lines
15 KiB
PHP

<?php
/**
* Copyright 2002-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file COPYING for license information (GPL). If you
* did not receive this file, see http://www.horde.org/licenses/gpl.
*
* @category Horde
* @copyright 2002-2017 Horde LLC
* @license http://www.horde.org/licenses/gpl GPL
* @package IMP
*/
/**
* The IMP_Crypt_Smime:: class contains all functions related to handling
* S/MIME messages within IMP.
*
* @author Mike Cochrane <mike@graftonhall.co.nz>
* @category Horde
* @copyright 2002-2017 Horde LLC
* @license http://www.horde.org/licenses/gpl GPL
* @package IMP
*/
class IMP_Crypt_Smime extends Horde_Crypt_Smime
{
/* Name of the S/MIME public key field in addressbook. */
const PUBKEY_FIELD = 'smimePublicKey';
/* Encryption type constants. */
const ENCRYPT = 'smime_encrypt';
const SIGN = 'smime_sign';
const SIGNENC = 'smime_signenc';
/**
* Return the list of available encryption options for composing.
*
* @return array Keys are encryption type constants, values are gettext
* strings describing the encryption type.
*/
public function encryptList()
{
$ret = array();
if ($GLOBALS['registry']->hasMethod('contacts/getField') ||
$GLOBALS['injector']->getInstance('Horde_Core_Hooks')->hookExists('smime_key', 'imp')) {
$ret += array(
self::ENCRYPT => _("S/MIME Encrypt Message")
);
}
if ($this->getPersonalPrivateKey()) {
$ret += array(
self::SIGN => _("S/MIME Sign Message"),
self::SIGNENC => _("S/MIME Sign/Encrypt Message")
);
}
return $ret;
}
/**
* Add the personal public key to the prefs.
*
* @param mixed $key The public key to add (either string or array).
*/
public function addPersonalPublicKey($key)
{
$GLOBALS['prefs']->setValue('smime_public_key', (is_array($key)) ? implode('', $key) : $key);
}
/**
* Add the personal private key to the prefs.
*
* @param mixed $key The private key to add (either string or array).
*/
public function addPersonalPrivateKey($key)
{
$GLOBALS['prefs']->setValue('smime_private_key', (is_array($key)) ? implode('', $key) : $key);
}
/**
* Add the list of additional certs to the prefs.
*
* @param mixed $key The private key to add (either string or array).
*/
public function addAdditionalCert($key)
{
$GLOBALS['prefs']->setValue('smime_additional_cert', (is_array($key)) ? implode('', $key) : $key);
}
/**
* Get the personal public key from the prefs.
*
* @return string The personal S/MIME public key.
*/
public function getPersonalPublicKey()
{
return $GLOBALS['prefs']->getValue('smime_public_key');
}
/**
* Get the personal private key from the prefs.
*
* @return string The personal S/MIME private key.
*/
public function getPersonalPrivateKey()
{
return $GLOBALS['prefs']->getValue('smime_private_key');
}
/**
* Get any additional certificates from the prefs.
*
* @return string Additional signing certs for inclusion.
*/
public function getAdditionalCert()
{
return $GLOBALS['prefs']->getValue('smime_additional_cert');
}
/**
* Deletes the specified personal keys from the prefs.
*/
public function deletePersonalKeys()
{
$GLOBALS['prefs']->setValue('smime_public_key', '');
$GLOBALS['prefs']->setValue('smime_private_key', '');
$GLOBALS['prefs']->setValue('smime_additional_cert', '');
$this->unsetPassphrase();
}
/**
* Add a public key to an address book.
*
* @param string $cert A public certificate to add.
*
* @throws Horde_Exception
*/
public function addPublicKey($cert)
{
list($name, $email) = $this->publicKeyInfo($cert);
$GLOBALS['registry']->call('contacts/addField', array($email, $name, self::PUBKEY_FIELD, $cert, $GLOBALS['prefs']->getValue('add_source')));
}
/**
* Get information about a public certificate.
*
* @param string $cert The public certificate.
*
* @return array Two element array: the name and e-mail for the cert.
* @throws Horde_Crypt_Exception
*/
public function publicKeyInfo($cert)
{
/* Make sure the certificate is valid. */
$key_info = openssl_x509_parse($cert);
if (!is_array($key_info) || !isset($key_info['subject'])) {
throw new Horde_Crypt_Exception(_("Not a valid public key."));
}
/* Add key to the user's address book. */
$email = $this->getEmailFromKey($cert);
if (is_null($email)) {
throw new Horde_Crypt_Exception(_("No email information located in the public key."));
}
/* Get the name corresponding to this key. */
if (isset($key_info['subject']['CN'])) {
$name = $key_info['subject']['CN'];
} elseif (isset($key_info['subject']['OU'])) {
$name = $key_info['subject']['OU'];
} else {
$name = $email;
}
return array($name, $email);
}
/**
* Returns the params needed to encrypt a message being sent to the
* specified email address.
*
* @param Horde_Mail_Rfc822_Address $address The e-mail address of the
* recipient.
*
* @return array The list of parameters needed by encrypt().
* @throws Horde_Crypt_Exception
*/
protected function _encryptParameters(Horde_Mail_Rfc822_Address $address)
{
/* We can only encrypt if we are sending to a single person. */
return array(
'pubkey' => $this->getPublicKey($address->bare_address),
'type' => 'message'
);
}
/**
* Retrieves a public key by e-mail.
* The key will be retrieved from a user's address book(s).
*
* @param string $address The e-mail address to search for.
*
* @return string The S/MIME public key requested.
* @throws Horde_Exception
*/
public function getPublicKey($address)
{
global $injector, $registry;
try {
$key = $injector->getInstance('Horde_Core_Hooks')->callHook(
'smime_key',
'imp',
array($address)
);
if ($key) {
return $key;
}
} catch (Horde_Exception_HookNotSet $e) {}
$params = $injector->getInstance('IMP_Contacts')->getAddressbookSearchParams();
try {
$key = $registry->call('contacts/getField', array($address, self::PUBKEY_FIELD, $params['sources'], true, true));
} catch (Horde_Exception $e) {
/* See if the address points to the user's public key. */
$personal_pubkey = $this->getPersonalPublicKey();
if (!empty($personal_pubkey) &&
$injector->getInstance('IMP_Identity')->hasAddress($address)) {
return $personal_pubkey;
}
throw $e;
}
/* If more than one public key is returned, just return the first in
* the array. There is no way of knowing which is the "preferred" key,
* if the keys are different. */
return is_array($key) ? reset($key) : $key;
}
/**
* Retrieves all public keys from a user's address book(s).
*
* @return array All S/MIME public keys available.
* @throws Horde_Crypt_Exception
*/
public function listPublicKeys()
{
$params = $GLOBALS['injector']->getInstance('IMP_Contacts')->getAddressbookSearchParams();
return empty($params['sources'])
? array()
: $GLOBALS['registry']->call('contacts/getAllAttributeValues', array(self::PUBKEY_FIELD, $params['sources']));
}
/**
* Deletes a public key from a user's address book(s) by e-mail.
*
* @param string $email The e-mail address to delete.
*
* @throws Horde_Crypt_Exception
*/
public function deletePublicKey($email)
{
$params = $GLOBALS['injector']->getInstance('IMP_Contacts')->getAddressbookSearchParams();
$GLOBALS['registry']->call('contacts/deleteField', array($email, self::PUBKEY_FIELD, $params['sources']));
}
/**
* Returns the parameters needed for signing a message.
*
* @return array The list of parameters needed by encrypt().
*/
protected function _signParameters()
{
return array(
'type' => 'signature',
'pubkey' => $this->getPersonalPublicKey(),
'privkey' => $this->getPersonalPrivateKey(),
'passphrase' => $this->getPassphrase(),
'sigtype' => 'detach',
'certs' => $this->getAdditionalCert()
);
}
/**
* Verifies a signed message with a given public key.
*
* @param string $text The text to verify.
*
* @return stdClass See Horde_Crypt_Smime::verify().
* @throws Horde_Crypt_Exception
*/
public function verifySignature($text)
{
return $this->verify($text, empty($GLOBALS['conf']['openssl']['cafile']) ? array() : $GLOBALS['conf']['openssl']['cafile']);
}
/**
* Decrypt a message with user's public/private keypair.
*
* @param string $text The text to decrypt.
*
* @return string See Horde_Crypt_Smime::decrypt().
* @throws Horde_Crypt_Exception
*/
public function decryptMessage($text)
{
return $this->decrypt($text, array(
'type' => 'message',
'pubkey' => $this->getPersonalPublicKey(),
'privkey' => $this->getPersonalPrivateKey(),
'passphrase' => $this->getPassphrase()
));
}
/**
* Gets the user's passphrase from the session cache.
*
* @return mixed The passphrase, if set. Returns false if the passphrase
* has not been loaded yet. Returns null if no passphrase
* is needed.
*/
public function getPassphrase()
{
global $session;
$private_key = $GLOBALS['prefs']->getValue('smime_private_key');
if (empty($private_key)) {
return false;
}
if ($session->exists('imp', 'smime_passphrase')) {
return $session->get('imp', 'smime_passphrase');
} elseif (!$session->exists('imp', 'smime_null_passphrase')) {
$session->set(
'imp',
'smime_null_passphrase',
$this->verifyPassphrase($private_key, null)
? null
: false
);
}
return $session->get('imp', 'smime_null_passphrase');
}
/**
* Store's the user's passphrase in the session cache.
*
* @param string $passphrase The user's passphrase.
*
* @return boolean Returns true if correct passphrase, false if incorrect.
*/
public function storePassphrase($passphrase)
{
global $session;
if ($this->verifyPassphrase($this->getPersonalPrivateKey(), $passphrase) !== false) {
$session->set('imp', 'smime_passphrase', $passphrase, $session::ENCRYPT);
return true;
}
return false;
}
/**
* Clear the passphrase from the session cache.
*/
public function unsetPassphrase()
{
global $session;
$session->remove('imp', 'smime_null_passphrase');
$session->remove('imp', 'smime_passphrase');
}
/**
* Encrypt a MIME_Part using S/MIME using IMP defaults.
*
* @param Horde_Mime_Part $mime_part The object to encrypt.
* @param Horde_Mail_Rfc822_Address $to_address The e-mail address of the
* key to use for encryption.
*
* @return MIME_Part See Horde_Crypt_Smime::encryptMIMEPart().
* @throws Horde_Crypt_Exception
*/
public function IMPencryptMIMEPart($mime_part,
Horde_Mail_Rfc822_Address $to_address)
{
return $this->encryptMIMEPart($mime_part, $this->_encryptParameters($to_address));
}
/**
* Sign a MIME_Part using S/MIME using IMP defaults.
*
* @param MIME_Part $mime_part The MIME_Part object to sign.
*
* @return MIME_Part See Horde_Crypt_Smime::signMIMEPart().
* @throws Horde_Crypt_Exception
*/
public function IMPsignMIMEPart($mime_part)
{
return $this->signMIMEPart($mime_part, $this->_signParameters());
}
/**
* Sign and encrypt a MIME_Part using S/MIME using IMP defaults.
*
* @param Horde_Mime_Part $mime_part The object to sign and
* encrypt.
* @param Horde_Mail_Rfc822_Address $to_address The e-mail address of the
* key to use for encryption.
*
* @return MIME_Part See Horde_Crypt_Smime::signAndencryptMIMEPart().
* @throws Horde_Crypt_Exception
*/
public function IMPsignAndEncryptMIMEPart($mime_part,
Horde_Mail_Rfc822_Address $to_address)
{
return $this->signAndEncryptMIMEPart($mime_part, $this->_signParameters(), $this->_encryptParameters($to_address));
}
/**
* Store the public/private/additional certificates in the preferences
* from a given PKCS 12 file.
*
* @param string $pkcs12 The PKCS 12 data.
* @param string $password The password of the PKCS 12 file.
* @param string $pkpass The password to use to encrypt the private key.
*
* @throws Horde_Crypt_Exception
*/
public function addFromPKCS12($pkcs12, $password, $pkpass = null)
{
$sslpath = empty($GLOBALS['conf']['openssl']['path'])
? null
: $GLOBALS['conf']['openssl']['path'];
$params = array('sslpath' => $sslpath, 'password' => $password);
if (!empty($pkpass)) {
$params['newpassword'] = $pkpass;
}
$result = $this->parsePKCS12Data($pkcs12, $params);
$this->addPersonalPrivateKey($result->private);
$this->addPersonalPublicKey($result->public);
$this->addAdditionalCert($result->certs);
}
/**
* Extract the contents from signed S/MIME data.
*
* @param string $data The signed S/MIME data.
*
* @return string The contents embedded in the signed data.
* @throws Horde_Crypt_Exception
*/
public function extractSignedContents($data, $sslpath = null)
{
$sslpath = empty($GLOBALS['conf']['openssl']['path'])
? null
: $GLOBALS['conf']['openssl']['path'];
return parent::extractSignedContents($data, $sslpath);
}
}