class Horde_Crypt
{
/**
+ * Singleton instances.
+ *
+ * @var array
+ */
+ static protected $_instances = array();
+
+ /**
* The temporary directory to use.
*
* @var string
* @param array $params A hash containing any additional configuration or
* parameters a subclass might need.
*
- * @return Horde_Crypt The newly created concrete Horde_Crypt instance, or
- * false on an error.
+ * @return Horde_Crypt The newly created concrete Horde_Crypt instance.
+ * @throws Horde_Exception
*/
static public function factory($driver, $params = array())
{
}
/* Return a base Horde_Crypt object if no driver is specified. */
- if (empty($driver) || (strcmp($driver, 'none') == 0)) {
+ if (empty($driver) || (strcasecmp($driver, 'none') == 0)) {
return new Horde_Crypt();
}
- $class = 'Horde_Crypt_' . $driver;
- if (!empty($app)) {
- $class = $app . '_' . $class;
- }
+ $class = (empty($app) ? 'Horde' : $app) . '_Crypt_' . ucfirst($driver);
- if (!class_exists($class)) {
- if (empty($app)) {
- include_once dirname(__FILE__) . '/Crypt/' . $driver . '.php';
- } else {
- include_once $GLOBALS['registry']->get('fileroot', $app) . '/lib/Crypt/' . $driver . '.php';
- }
+ if (class_exists($class)) {
+ return new $class($params);
}
- return class_exists($class)
- ? new $class($params)
- : PEAR::raiseError('Class definition of ' . $class . ' not found.');
+ throw new Horde_Exception('Class definition of ' . $class . ' not found.');
}
/**
* This should be used if multiple crypto backends (and, thus,
* multiple Horde_Crypt instances) are required.
*
- * This method must be invoked as: $var = &Horde_Crypt::singleton()
+ * This method must be invoked as: $var = Horde_Crypt::singleton()
*
* @param mixed $driver The type of concrete Horde_Crypt subclass to
* return. If $driver is an array, then we will look
* @param array $params A hash containing any additional configuration or
* connection parameters a subclass might need.
*
- * @return Horde_Crypt The concrete Horde_Crypt reference, or false on an
- * error.
+ * @return Horde_Crypt The concrete Horde_Crypt reference.
+ * @throws Horde_Exception
*/
static public function &singleton($driver, $params = array())
{
- static $instances = array();
+ ksort($params);
+ $signature = hash('md5', serialize(array($driver, $params)));
- $signature = serialize(array($driver, $params));
- if (!isset($instances[$signature])) {
- $instances[$signature] = Horde_Crypt::factory($driver, $params);
+ if (!isset(self::$_instances[$signature])) {
+ self::$_instances[$signature] = Horde_Crypt::factory($driver, $params);
}
- return $instances[$signature];
+ return self::$_instances[$signature];
}
/**
- * Outputs error message if we are not using a secure connection.
+ * Throws exception if not using a secure connection.
*
- * @return PEAR_Error Returns a PEAR_Error object if there is no secure
- * connection.
+ * @throws Horde_Exception
*/
public function requireSecureConnection()
{
}
}
- return PEAR::raiseError(_("The encryption features require a secure web connection."));
+ throw new Horde_Exception (_("The encryption features require a secure web connection."));
}
/**
* @param array $params An array of arguments needed to decrypt the data.
*
* @return array The decrypted data.
+ * @throws Horde_Exception
*/
public function decrypt($data, $params = array())
{
* Create a temporary file that will be deleted at the end of this
* process.
*
- * @access private
- *
* @param string $descrip Description string to use in filename.
* @param boolean $delete Delete the file automatically?
*
--- /dev/null
+<?php
+/**
+ * Horde_Crypt_Pgp:: provides a framework for Horde applications to interact
+ * with the GNU Privacy Guard program ("GnuPG"). GnuPG implements the OpenPGP
+ * standard (RFC 2440).
+ *
+ * GnuPG Website: http://www.gnupg.org/
+ *
+ * This class has been developed with, and is only guaranteed to work with,
+ * Version 1.21 or above of GnuPG.
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Crypt
+ */
+class Horde_Crypt_Pgp extends Horde_Crypt
+{
+ /**
+ * Armor Header Lines - From RFC 2440:
+ *
+ * An Armor Header Line consists of the appropriate header line text
+ * surrounded by five (5) dashes ('-', 0x2D) on either side of the header
+ * line text. The header line text is chosen based upon the type of data
+ * that is being encoded in Armor, and how it is being encoded.
+ *
+ * All Armor Header Lines are prefixed with 'PGP'.
+ *
+ * The Armor Tail Line is composed in the same manner as the Armor Header
+ * Line, except the string "BEGIN" is replaced by the string "END."
+ */
+
+ /* Used for signed, encrypted, or compressed files. */
+ const ARMOR_MESSAGE = 1;
+
+ /* Used for signed files. */
+ const ARMOR_SIGNED_MESSAGE = 2;
+
+ /* Used for armoring public keys. */
+ const ARMOR_PUBLIC_KEY = 3;
+
+ /* Used for armoring private keys. */
+ const ARMOR_PRIVATE_KEY = 4;
+
+ /* Used for detached signatures, PGP/MIME signatures, and natures
+ * following clearsigned messages. */
+ const ARMOR_SIGNATURE = 5;
+
+ /* Regular text contained in an PGP message. */
+ const ARMOR_TEXT = 6;
+
+ /**
+ * Strings in armor header lines used to distinguish between the different
+ * types of PGP decryption/encryption.
+ *
+ * @var array
+ */
+ protected $_armor = array(
+ 'MESSAGE' => self::ARMOR_MESSAGE,
+ 'SIGNED MESSAGE' => self::ARMOR_SIGNED_MESSAGE,
+ 'PUBLIC KEY BLOCK' => self::ARMOR_PUBLIC_KEY,
+ 'PRIVATE KEY BLOCK' => self::ARMOR_PRIVATE_KEY,
+ 'SIGNATURE' => self::ARMOR_SIGNATURE
+ );
+
+ /* The default public PGP keyserver to use. */
+ const KEYSERVER_PUBLIC = 'pgp.mit.edu';
+
+ /* The number of times the keyserver refuses connection before an error is
+ * returned. */
+ const KEYSERVER_REFUSE = 3;
+
+ /* The number of seconds that PHP will attempt to connect to the keyserver
+ * before it will stop processing the request. */
+ const KEYSERVER_TIMEOUT = 10;
+
+ /**
+ * The list of PGP hash algorithms (from RFC 3156).
+ *
+ * @var array
+ */
+ protected $_hashAlg = array(
+ 1 => 'pgp-md5',
+ 2 => 'pgp-sha1',
+ 3 => 'pgp-ripemd160',
+ 5 => 'pgp-md2',
+ 6 => 'pgp-tiger192',
+ 7 => 'pgp-haval-5-160',
+ 8 => 'pgp-sha256',
+ 9 => 'pgp-sha384',
+ 10 => 'pgp-sha512',
+ 11 => 'pgp-sha224',
+ );
+
+ /**
+ * GnuPG program location/common options.
+ *
+ * @var array
+ */
+ protected $_gnupg;
+
+ /**
+ * Filename of the temporary public keyring.
+ *
+ * @var string
+ */
+ protected $_publicKeyring;
+
+ /**
+ * Filename of the temporary private keyring.
+ *
+ * @var string
+ */
+ protected $_privateKeyring;
+
+ /**
+ * Constructor.
+ *
+ * @param array $params Parameter array containing the path to the GnuPG
+ * binary (key = 'program') and to a temporary
+ * directory.
+ *
+ * @throws Horde_Exception
+ */
+ protected function __construct($params = array())
+ {
+ $this->_tempdir = Util::createTempDir(true, $params['temp']);
+
+ if (empty($params['program'])) {
+ Horde::fatal(new Horde_Exception('The location of the GnuPG binary must be given to the Horde_Crypt_Pgp:: class.'), __FILE__, __LINE__);
+ }
+
+ /* Store the location of GnuPG and set common options. */
+ $this->_gnupg = array(
+ $params['program'],
+ '--no-tty',
+ '--no-secmem-warning',
+ '--no-options',
+ '--no-default-keyring',
+ '--yes',
+ '--homedir ' . $this->_tempdir
+ );
+
+ if (strncasecmp(PHP_OS, 'WIN', 3)) {
+ array_unshift($this->_gnupg, 'LANG= ;');
+ }
+ }
+
+ /**
+ * Generates a personal Public/Private keypair combination.
+ *
+ * @param string $realname The name to use for the key.
+ * @param string $email The email to use for the key.
+ * @param string $passphrase The passphrase to use for the key.
+ * @param string $comment The comment to use for the key.
+ * @param integer $keylength The keylength to use for the key.
+ *
+ * @return array An array consisting of:
+ * <pre>
+ * Key Value
+ * --------------------------
+ * 'public' => Public Key
+ * 'private' => Private Key
+ * </pre>
+ * @throws Horde_Exception
+ */
+ public function generateKey($realname, $email, $passphrase, $comment = '',
+ $keylength = 1024)
+ {
+ /* Create temp files to hold the generated keys. */
+ $pub_file = $this->_createTempFile('horde-pgp');
+ $secret_file = $this->_createTempFile('horde-pgp');
+
+ /* Create the config file necessary for GnuPG to run in batch mode. */
+ /* TODO: Sanitize input, More user customizable? */
+ $input = array(
+ '%pubring ' . $pub_file,
+ '%secring ' . $secret_file,
+ 'Key-Type: DSA',
+ 'Key-Length: 1024',
+ 'Subkey-Type: ELG-E',
+ 'Subkey-Length: ' . $keylength,
+ 'Name-Real: ' . $realname,
+ 'Name-Email: ' . $email,
+ 'Expire-Date: 0',
+ 'Passphrase: ' . $passphrase
+ );
+ if (!empty($comment)) {
+ $input[] = 'Name-Comment: ' . $comment;
+ }
+ $input[] = '%commit';
+
+ /* Run through gpg binary. */
+ $cmdline = array(
+ '--gen-key',
+ '--batch',
+ '--armor'
+ );
+ $result = $this->_callGpg($cmdline, 'w', $input, true, true);
+
+ /* Get the keys from the temp files. */
+ $public_key = file($pub_file);
+ $secret_key = file($secret_file);
+
+ /* If either key is empty, something went wrong. */
+ if (empty($public_key) || empty($secret_key)) {
+ $msg = _("Public/Private keypair not generated successfully.");
+ if (!empty($result->stderr)) {
+ $msg .= ' ' . _("Returned error message:") . ' ' . $result->stderr;
+ }
+ throw new Horde_Exception($msg, 'horde.error');
+ }
+
+ return array('public' => $public_key, 'private' => $secret_key);
+ }
+
+ /**
+ * Returns information on a PGP data block.
+ *
+ * @param string $pgpdata The PGP data block.
+ *
+ * @return array An array with information on the PGP data block. If an
+ * element is not present in the data block, it will
+ * likewise not be set in the array.
+ * <pre>
+ * Array Format:
+ * -------------
+ * [public_key]/[secret_key] => Array
+ * (
+ * [created] => Key creation - UNIX timestamp
+ * [expires] => Key expiration - UNIX timestamp (0 = never expires)
+ * [size] => Size of the key in bits
+ * )
+ *
+ * [keyid] => Key ID of the PGP data (if available)
+ * 16-bit hex value (as of Horde 3.2)
+ *
+ * [signature] => Array (
+ * [id{n}/'_SIGNATURE'] => Array (
+ * [name] => Full Name
+ * [comment] => Comment
+ * [email] => E-mail Address
+ * [keyid] => 16-bit hex value (as of Horde 3.2)
+ * [created] => Signature creation - UNIX timestamp
+ * [expires] => Signature expiration - UNIX timestamp
+ * [micalg] => The hash used to create the signature
+ * [sig_{hex}] => Array [details of a sig verifying the ID] (
+ * [created] => Signature creation - UNIX timestamp
+ * [expires] => Signature expiration - UNIX timestamp
+ * [keyid] => 16-bit hex value (as of Horde 3.2)
+ * [micalg] => The hash used to create the signature
+ * )
+ * )
+ * )
+ * </pre>
+ *
+ * Each user ID will be stored in the array 'signature' and have data
+ * associated with it, including an array for information on each
+ * signature that has signed that UID. Signatures not associated with a
+ * UID (e.g. revocation signatures and sub keys) will be stored under the
+ * special keyword '_SIGNATURE'.
+ */
+ public function pgpPacketInformation($pgpdata)
+ {
+ $data_array = array();
+ $keyid = '';
+ $header = null;
+ $input = $this->_createTempFile('horde-pgp');
+ $sig_id = $uid_idx = 0;
+
+ /* Store message in temporary file. */
+ file_put_contents($input, $pgpdata);
+
+ $cmdline = array(
+ '--list-packets',
+ $input
+ );
+ $result = $this->_callGpg($cmdline, 'r');
+
+ foreach (explode("\n", $result->stdout) as $line) {
+ /* Headers are prefaced with a ':' as the first character on the
+ line. */
+ if (strpos($line, ':') === 0) {
+ $lowerLine = String::lower($line);
+
+ /* If we have a key (rather than a signature block), get the
+ key's ID */
+ if (strpos($lowerLine, ':public key packet:') !== false ||
+ strpos($lowerLine, ':secret key packet:') !== false) {
+ $cmdline = array(
+ '--with-colons',
+ $input
+ );
+ $data = $this->_callGpg($cmdline, 'r');
+ if (preg_match("/(sec|pub):.*:.*:.*:([A-F0-9]{16}):/", $data->stdout, $matches)) {
+ $keyid = $matches[2];
+ }
+ }
+
+ if (strpos($lowerLine, ':public key packet:') !== false) {
+ $header = 'public_key';
+ } elseif (strpos($lowerLine, ':secret key packet:') !== false) {
+ $header = 'secret_key';
+ } elseif (strpos($lowerLine, ':user id packet:') !== false) {
+ $uid_idx++;
+ $line = preg_replace_callback('/\\\\x([0-9a-f]{2})/', array($this, '_pgpPacketInformationHelper'), $line);
+ if (preg_match("/\"([^\<]+)\<([^\>]+)\>\"/", $line, $matches)) {
+ $header = 'id' . $uid_idx;
+ if (preg_match('/([^\(]+)\((.+)\)$/', trim($matches[1]), $comment_matches)) {
+ $data_array['signature'][$header]['name'] = trim($comment_matches[1]);
+ $data_array['signature'][$header]['comment'] = $comment_matches[2];
+ } else {
+ $data_array['signature'][$header]['name'] = trim($matches[1]);
+ $data_array['signature'][$header]['comment'] = '';
+ }
+ $data_array['signature'][$header]['email'] = $matches[2];
+ $data_array['signature'][$header]['keyid'] = $keyid;
+ }
+ } elseif (strpos($lowerLine, ':signature packet:') !== false) {
+ if (empty($header) || empty($uid_idx)) {
+ $header = '_SIGNATURE';
+ }
+ if (preg_match("/keyid\s+([0-9A-F]+)/i", $line, $matches)) {
+ $sig_id = $matches[1];
+ $data_array['signature'][$header]['sig_' . $sig_id]['keyid'] = $matches[1];
+ $data_array['keyid'] = $matches[1];
+ }
+ } elseif (strpos($lowerLine, ':literal data packet:') !== false) {
+ $header = 'literal';
+ } elseif (strpos($lowerLine, ':encrypted data packet:') !== false) {
+ $header = 'encrypted';
+ } else {
+ $header = null;
+ }
+ } else {
+ if ($header == 'secret_key' || $header == 'public_key') {
+ if (preg_match("/created\s+(\d+),\s+expires\s+(\d+)/i", $line, $matches)) {
+ $data_array[$header]['created'] = $matches[1];
+ $data_array[$header]['expires'] = $matches[2];
+ } elseif (preg_match("/\s+[sp]key\[0\]:\s+\[(\d+)/i", $line, $matches)) {
+ $data_array[$header]['size'] = $matches[1];
+ }
+ } elseif ($header == 'literal' || $header == 'encrypted') {
+ $data_array[$header] = true;
+ } elseif ($header) {
+ if (preg_match("/version\s+\d+,\s+created\s+(\d+)/i", $line, $matches)) {
+ $data_array['signature'][$header]['sig_' . $sig_id]['created'] = $matches[1];
+ } elseif (isset($data_array['signature'][$header]['sig_' . $sig_id]['created']) &&
+ preg_match('/expires after (\d+y\d+d\d+h\d+m)\)$/', $line, $matches)) {
+ $expires = $matches[1];
+ preg_match('/^(\d+)y(\d+)d(\d+)h(\d+)m$/', $expires, $matches);
+ list(, $years, $days, $hours, $minutes) = $matches;
+ $data_array['signature'][$header]['sig_' . $sig_id]['expires'] =
+ strtotime('+ ' . $years . ' years + ' . $days . ' days + ' . $hours . ' hours + ' . $minutes . ' minutes', $data_array['signature'][$header]['sig_' . $sig_id]['created']);
+ } elseif (preg_match("/digest algo\s+(\d{1})/", $line, $matches)) {
+ $micalg = $this->_hashAlg[$matches[1]];
+ $data_array['signature'][$header]['sig_' . $sig_id]['micalg'] = $micalg;
+ if ($header == '_SIGNATURE') {
+ /* Likely a signature block, not a key. */
+ $data_array['signature']['_SIGNATURE']['micalg'] = $micalg;
+ }
+ if ($sig_id == $keyid) {
+ /* Self signing signature - we can assume
+ * the micalg value from this signature is
+ * that for the key */
+ $data_array['signature']['_SIGNATURE']['micalg'] = $micalg;
+ $data_array['signature'][$header]['micalg'] = $micalg;
+ }
+ }
+ }
+ }
+ }
+
+ $keyid && $data_array['keyid'] = $keyid;
+
+ return $data_array;
+ }
+
+ /**
+ * TODO
+ */
+ protected function _pgpPacketInformationHelper($a)
+ {
+ return chr(hexdec($a[1]));
+ }
+
+ /**
+ * Returns human readable information on a PGP key.
+ *
+ * @param string $pgpdata The PGP data block.
+ *
+ * @return string Tabular information on the PGP key.
+ */
+ public function pgpPrettyKey($pgpdata)
+ {
+ $msg = '';
+ $packet_info = $this->pgpPacketInformation($pgpdata);
+ $fingerprints = $this->getFingerprintsFromKey($pgpdata);
+
+ if (!empty($packet_info['signature'])) {
+ /* Making the property names the same width for all
+ * localizations .*/
+ $leftrow = array(_("Name"), _("Key Type"), _("Key Creation"),
+ _("Expiration Date"), _("Key Length"),
+ _("Comment"), _("E-Mail"), _("Hash-Algorithm"),
+ _("Key ID"), _("Key Fingerprint"));
+ $leftwidth = array_map('strlen', $leftrow);
+ $maxwidth = max($leftwidth) + 2;
+ array_walk($leftrow, array($this, '_pgpPrettyKeyFormatter'), $maxwidth);
+
+ foreach (array_keys($packet_info['signature']) as $uid_idx) {
+ if ($uid_idx == '_SIGNATURE') {
+ continue;
+ }
+ $key_info = $this->pgpPacketSignatureByUidIndex($pgpdata, $uid_idx);
+
+ if (!empty($key_info['keyid'])) {
+ $key_info['keyid'] = $this->_getKeyIDString($key_info['keyid']);
+ } else {
+ $key_info['keyid'] = null;
+ }
+
+ $fingerprint = isset($fingerprints[$key_info['keyid']]) ? $fingerprints[$key_info['keyid']] : null;
+
+ $msg .= $leftrow[0] . (isset($key_info['name']) ? stripcslashes($key_info['name']) : '') . "\n"
+ . $leftrow[1] . (($key_info['key_type'] == 'public_key') ? _("Public Key") : _("Private Key")) . "\n"
+ . $leftrow[2] . strftime("%D", $key_info['key_created']) . "\n"
+ . $leftrow[3] . (empty($key_info['key_expires']) ? '[' . _("Never") . ']' : strftime("%D", $key_info['key_expires'])) . "\n"
+ . $leftrow[4] . $key_info['key_size'] . " Bytes\n"
+ . $leftrow[5] . (empty($key_info['comment']) ? '[' . _("None") . ']' : $key_info['comment']) . "\n"
+ . $leftrow[6] . (empty($key_info['email']) ? '[' . _("None") . ']' : $key_info['email']) . "\n"
+ . $leftrow[7] . (empty($key_info['micalg']) ? '[' . _("Unknown") . ']' : $key_info['micalg']) . "\n"
+ . $leftrow[8] . (empty($key_info['keyid']) ? '[' . _("Unknown") . ']' : $key_info['keyid']) . "\n"
+ . $leftrow[9] . (empty($fingerprint) ? '[' . _("Unknown") . ']' : $fingerprint) . "\n\n";
+ }
+ }
+
+ return $msg;
+ }
+
+ /**
+ * TODO
+ */
+ protected function _pgpPrettyKeyFormatter(&$s, $k, $m)
+ {
+ $s .= ':' . str_repeat(' ', $m - String::length($s));
+ }
+
+ /**
+ * TODO
+ */
+ protected function _getKeyIDString($keyid)
+ {
+ /* Get the 8 character key ID string. */
+ if (strpos($keyid, '0x') === 0) {
+ $keyid = substr($keyid, 2);
+ }
+ if (strlen($keyid) > 8) {
+ $keyid = substr($keyid, -8);
+ }
+ return '0x' . $keyid;
+ }
+
+ /**
+ * Returns only information on the first ID that matches the email address
+ * input.
+ *
+ * @param string $pgpdata The PGP data block.
+ * @param string $email An e-mail address.
+ *
+ * @return array An array with information on the PGP data block. If an
+ * element is not present in the data block, it will
+ * likewise not be set in the array.
+ * <pre>
+ * Array Fields:
+ * -------------
+ * key_created => Key creation - UNIX timestamp
+ * key_expires => Key expiration - UNIX timestamp (0 = never expires)
+ * key_size => Size of the key in bits
+ * key_type => The key type (public_key or secret_key)
+ * name => Full Name
+ * comment => Comment
+ * email => E-mail Address
+ * keyid => 16-bit hex value
+ * created => Signature creation - UNIX timestamp
+ * micalg => The hash used to create the signature
+ * </pre>
+ */
+ public function pgpPacketSignature($pgpdata, $email)
+ {
+ $data = $this->pgpPacketInformation($pgpdata);
+ $key_type = null;
+ $return_array = array();
+
+ /* Check that [signature] key exists. */
+ if (!isset($data['signature'])) {
+ return $return_array;
+ }
+
+ /* Store the signature information now. */
+ if (($email == '_SIGNATURE') &&
+ isset($data['signature']['_SIGNATURE'])) {
+ foreach ($data['signature'][$email] as $key => $value) {
+ $return_array[$key] = $value;
+ }
+ } else {
+ $uid_idx = 1;
+
+ while (isset($data['signature']['id' . $uid_idx])) {
+ if ($data['signature']['id' . $uid_idx]['email'] == $email) {
+ foreach ($data['signature']['id' . $uid_idx] as $key => $val) {
+ $return_array[$key] = $val;
+ }
+ break;
+ }
+ $uid_idx++;
+ }
+ }
+
+ return $this->_pgpPacketSignature($data, $return_array);
+ }
+
+ /**
+ * Returns information on a PGP signature embedded in PGP data. Similar
+ * to pgpPacketSignature(), but returns information by unique User ID
+ * Index (format id{n} where n is an integer of 1 or greater).
+ *
+ * @param string $pgpdata See pgpPacketSignature().
+ * @param string $uid_idx The UID index.
+ *
+ * @return array See pgpPacketSignature().
+ */
+ public function pgpPacketSignatureByUidIndex($pgpdata, $uid_idx)
+ {
+ $data = $this->pgpPacketInformation($pgpdata);
+ $key_type = null;
+ $return_array = array();
+
+ /* Search for the UID index. */
+ if (!isset($data['signature']) ||
+ !isset($data['signature'][$uid_idx])) {
+ return $return_array;
+ }
+
+ /* Store the signature information now. */
+ foreach ($data['signature'][$uid_idx] as $key => $value) {
+ $return_array[$key] = $value;
+ }
+
+ return $this->_pgpPacketSignature($data, $return_array);
+ }
+
+ /**
+ * Adds some data to the pgpPacketSignature*() function array.
+ *
+ * @param array $data See pgpPacketSignature().
+ * @param array $retarray The return array.
+ *
+ * @return array The return array.
+ */
+ protected function _pgpPacketSignature($data, $retarray)
+ {
+ /* If empty, return now. */
+ if (empty($retarray)) {
+ return $retarray;
+ }
+
+ $key_type = null;
+
+ /* Store any public/private key information. */
+ if (isset($data['public_key'])) {
+ $key_type = 'public_key';
+ } elseif (isset($data['secret_key'])) {
+ $key_type = 'secret_key';
+ }
+
+ if ($key_type) {
+ $retarray['key_type'] = $key_type;
+ if (isset($data[$key_type]['created'])) {
+ $retarray['key_created'] = $data[$key_type]['created'];
+ }
+ if (isset($data[$key_type]['expires'])) {
+ $retarray['key_expires'] = $data[$key_type]['expires'];
+ }
+ if (isset($data[$key_type]['size'])) {
+ $retarray['key_size'] = $data[$key_type]['size'];
+ }
+ }
+
+ return $retarray;
+ }
+
+ /**
+ * Returns the key ID of the key used to sign a block of PGP data.
+ *
+ * @param string $text The PGP signed text block.
+ *
+ * @return string The key ID of the key used to sign $text.
+ */
+ public function getSignersKeyID($text)
+ {
+ $keyid = null;
+
+ $input = $this->_createTempFile('horde-pgp');
+ file_put_contents($input, $text);
+
+ $cmdline = array(
+ '--verify',
+ $input
+ );
+ $result = $this->_callGpg($cmdline, 'r', null, true, true);
+ if (preg_match('/gpg:\sSignature\smade.*ID\s+([A-F0-9]{8})\s+/', $result->stderr, $matches)) {
+ $keyid = $matches[1];
+ }
+
+ return $keyid;
+ }
+
+ /**
+ * Verify a passphrase for a given public/private keypair.
+ *
+ * @param string $public_key The user's PGP public key.
+ * @param string $private_key The user's PGP private key.
+ * @param string $passphrase The user's passphrase.
+ *
+ * @return boolean Returns true on valid passphrase, false on invalid
+ * passphrase.
+ */
+ public function verifyPassphrase($public_key, $private_key, $passphrase)
+ {
+ /* Encrypt a test message. */
+ try {
+ $this->encrypt('Test', array('type' => 'message', 'pubkey' => $public_key));
+ } catch (Horde_Exception $e) {
+ return false;
+ }
+
+ /* Try to decrypt the message. */
+ try {
+ $this->decrypt($result, array('type' => 'message', 'pubkey' => $public_key, 'privkey' => $private_key, 'passphrase' => $passphrase));
+ } catch (Horde_Exception $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parses a message into text and PGP components.
+ *
+ * @param string $text The text to parse.
+ *
+ * @return array An array with the parsed text, returned in blocks of
+ * text corresponding to their actual order. Keys:
+ * <pre>
+ * 'type' - (integer) The type of data contained in block.
+ * Valid types are defined at the top of this class
+ * (the ARMOR_* constants).
+ * 'data' - (array) The data for each section. Each line has been stripped
+ * of EOL characters.
+ * </pre>
+ */
+ public function parsePGPData($text)
+ {
+ $data = array();
+ $temp = array(
+ 'type' => self::ARMOR_TEXT
+ );
+
+ $buffer = explode("\n", $text);
+ while (list(,$val) = each($buffer)) {
+ $val = rtrim($val, "\r");
+ if (preg_match('/^-----(BEGIN|END) PGP ([^-]+)-----\s*$/', $val, $matches)) {
+ if (isset($temp['data'])) {
+ $data[] = $temp;
+ }
+ $temp= array();
+
+ if ($matches[1] == 'BEGIN') {
+ $temp['type'] = $this->_armor[$matches[2]];
+ $temp['data'][] = $val;
+ } elseif ($matches[1] == 'END') {
+ $temp['type'] = self::ARMOR_TEXT;
+ $data[count($data) - 1]['data'][] = $val;
+ }
+ } else {
+ $temp['data'][] = $val;
+ }
+ }
+
+ if (isset($temp['data']) &&
+ ((count($temp['data']) > 1) || !empty($temp['data'][0]))) {
+ $data[] = $temp;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Returns a PGP public key from a public keyserver.
+ *
+ * @param string $keyid The key ID of the PGP key.
+ * @param string $server The keyserver to use.
+ * @param float $timeout The keyserver timeout.
+ * @param string $address The email address of the PGP key.
+ *
+ * @return string The PGP public key.
+ */
+ public function getPublicKeyserver($keyid,
+ $server = self::KEYSERVER_PUBLIC,
+ $timeout = self::KEYSERVER_TIMEOUT,
+ $address = null)
+ {
+ if (empty($keyid) && !empty($address)) {
+ $keyid = $this->getKeyID($address, $server, $timeout);
+ }
+
+ /* Connect to the public keyserver. */
+ $uri = '/pks/lookup?op=get&search=' . $this->_getKeyIDString($keyid);
+ $output = $this->_connectKeyserver('GET', $server, $uri, '', $timeout);
+
+ /* Strip HTML Tags from output. */
+ if (($start = strstr($output, '-----BEGIN'))) {
+ $length = strpos($start, '-----END') + 34;
+ return substr($start, 0, $length);
+ }
+
+ throw new Horde_Exception(_("Could not obtain public key from the keyserver."), 'horde.error');
+ }
+
+ /**
+ * Sends a PGP public key to a public keyserver.
+ *
+ * @param string $pubkey The PGP public key
+ * @param string $server The keyserver to use.
+ * @param float $timeout The keyserver timeout.
+ *
+ * @throws Horde_Exception
+ */
+ public function putPublicKeyserver($pubkey,
+ $server = self::KEYSERVER_PUBLIC,
+ $timeout = self::KEYSERVER_TIMEOUT)
+ {
+ /* Get the key ID of the public key. */
+ $info = $this->pgpPacketInformation($pubkey);
+
+ /* See if the public key already exists on the keyserver. */
+ try {
+ $this->getPublicKeyserver($info['keyid'], $server, $timeout);
+ } catch (Horde_Exception $e) {
+ throw new Horde_Exception(_("Key already exists on the public keyserver."), 'horde.warning');
+ }
+
+ /* Connect to the public keyserver. _connectKeyserver() */
+ $pubkey = 'keytext=' . urlencode(rtrim($pubkey));
+ $cmd = array(
+ 'Host: ' . $server . ':11371',
+ 'User-Agent: Horde Application Framework 3.2',
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Content-Length: ' . strlen($pubkey),
+ 'Connection: close',
+ '',
+ $pubkey
+ );
+
+ return $this->_connectKeyserver('POST', $server, '/pks/add', implode("\r\n", $cmd), $timeout);
+ }
+
+ /**
+ * Returns the first matching key ID for an email address from a
+ * public keyserver.
+ *
+ * @param string $address The email address of the PGP key.
+ * @param string $server The keyserver to use.
+ * @param float $timeout The keyserver timeout.
+ *
+ * @return string The PGP key ID.
+ * @throws Horde_Exception
+ */
+ public function getKeyID($address, $server = self::KEYSERVER_PUBLIC,
+ $timeout = self::KEYSERVER_TIMEOUT)
+ {
+ /* Connect to the public keyserver. */
+ $uri = '/pks/lookup?op=index&options=mr&search=' . urlencode($address);
+ $output = $this->_connectKeyserver('GET', $server, $uri, '', $timeout);
+
+ if (($start = strstr($output, '-----BEGIN PGP PUBLIC KEY BLOCK'))) {
+ /* The server returned the matching key immediately. */
+ $length = strpos($start, '-----END PGP PUBLIC KEY BLOCK') + 34;
+ $info = $this->pgpPacketInformation(substr($start, 0, $length));
+ if (!empty($info['keyid']) &&
+ (empty($info['public_key']['expires']) ||
+ $info['public_key']['expires'] > time())) {
+ return $info['keyid'];
+ }
+ } elseif (strpos($output, 'pub:') !== false) {
+ $output = explode("\n", $output);
+ $keyids = array();
+ foreach ($output as $line) {
+ if (substr($line, 0, 4) == 'pub:') {
+ $line = explode(':', $line);
+ /* Ignore invalid lines and expired keys. */
+ if (count($line) != 7 ||
+ (!empty($line[5]) && $line[5] <= time())) {
+ continue;
+ }
+ $keyids[$line[4]] = $line[1];
+ }
+ }
+ /* Sort by timestamp to use the newest key. */
+ if (count($keyids)) {
+ ksort($keyids);
+ return array_pop($keyids);
+ }
+ }
+
+ throw new Horde_Exception(_("Could not obtain public key from the keyserver."));
+ }
+
+ /**
+ * Get the fingerprints from a key block.
+ *
+ * @param string $pgpdata The PGP data block.
+ *
+ * @return array The fingerprints in $pgpdata indexed by key id.
+ */
+ public function getFingerprintsFromKey($pgpdata)
+ {
+ $fingerprints = array();
+
+ /* Store the key in a temporary keyring. */
+ $keyring = $this->_putInKeyring($pgpdata);
+
+ /* Options for the GPG binary. */
+ $cmdline = array(
+ '--fingerprint',
+ $keyring,
+ );
+
+ $result = $this->_callGpg($cmdline, 'r');
+ if (!$result || !$result->stdout) {
+ return $fingerprints;
+ }
+
+ /* Parse fingerprints and key ids from output. */
+ $lines = explode("\n", $result->stdout);
+ $keyid = null;
+ foreach ($lines as $line) {
+ if (preg_match('/pub\s+\w+\/(\w{8})/', $line, $matches)) {
+ $keyid = '0x' . $matches[1];
+ } elseif ($keyid && preg_match('/^\s+[\s\w]+=\s*([\w\s]+)$/m', $line, $matches)) {
+ $fingerprints[$keyid] = trim($matches[1]);
+ $keyid = null;
+ }
+ }
+
+ return $fingerprints;
+ }
+
+ /**
+ * Connects to a public key server via HKP (Horrowitz Keyserver Protocol).
+ *
+ * @param string $method POST, GET, etc.
+ * @param string $server The keyserver to use.
+ * @param string $uri The URI to access (relative to the server).
+ * @param string $command The PGP command to run.
+ * @param float $timeout The timeout value.
+ *
+ * @return string The text from standard output on success.
+ * @throws Horde_Exception
+ */
+ protected function _connectKeyserver($method, $server, $resource,
+ $command, $timeout)
+ {
+ $connRefuse = 0;
+ $output = '';
+
+ $port = '11371';
+ if (!empty($GLOBALS['conf']['http']['proxy']['proxy_host'])) {
+ $resource = 'http://' . $server . ':' . $port . $resource;
+
+ $server = $GLOBALS['conf']['http']['proxy']['proxy_host'];
+ if (!empty($GLOBALS['conf']['http']['proxy']['proxy_port'])) {
+ $port = $GLOBALS['conf']['http']['proxy']['proxy_port'];
+ } else {
+ $port = 80;
+ }
+ }
+
+ $command = $method . ' ' . $resource . ' HTTP/1.0' . ($command ? "\r\n" . $command : '');
+
+ /* Attempt to get the key from the keyserver. */
+ do {
+ $errno = $errstr = null;
+
+ /* The HKP server is located on port 11371. */
+ $fp = @fsockopen($server, $port, $errno, $errstr, $timeout);
+ if ($fp) {
+ fputs($fp, $command . "\n\n");
+ while (!feof($fp)) {
+ $output .= fgets($fp, 1024);
+ }
+ fclose($fp);
+ return $output;
+ }
+ } while (++$connRefuse < self::KEYSERVER_REFUSE);
+
+ if ($errno == 0) {
+ throw new Horde_Exception(_("Connection refused to the public keyserver."), 'horde.error');
+ } else {
+ throw new Horde_Exception(sprintf(_("Connection refused to the public keyserver. Reason: %s (%s)"), String::convertCharset($errstr, NLS::getExternalCharset()), $errno), 'horde.error');
+ }
+ }
+
+ /**
+ * Encrypts text using PGP.
+ *
+ * @param string $text The text to be PGP encrypted.
+ * @param array $params The parameters needed for encryption.
+ * See the individual _encrypt*() functions for the
+ * parameter requirements.
+ *
+ * @return string The encrypted message.
+ * @throws Horde_Exception
+ */
+ public function encrypt($text, $params = array())
+ {
+ if (isset($params['type'])) {
+ if ($params['type'] === 'message') {
+ return $this->_encryptMessage($text, $params);
+ } elseif ($params['type'] === 'signature') {
+ return $this->_encryptSignature($text, $params);
+ }
+ }
+ }
+
+ /**
+ * Decrypts text using PGP.
+ *
+ * @param string $text The text to be PGP decrypted.
+ * @param array $params The parameters needed for decryption.
+ * See the individual _decrypt*() functions for the
+ * parameter requirements.
+ *
+ * @return stdClass An object with the following properties:
+ * <pre>
+ * 'message' - (string) The signature result text.
+ * 'result' - (boolean) The result of the signature test.
+ * </pre>
+ * @throws Horde_Exception
+ */
+ public function decrypt($text, $params = array())
+ {
+ if (isset($params['type'])) {
+ if ($params['type'] === 'message') {
+ return $this->_decryptMessage($text, $params);
+ } elseif (($params['type'] === 'signature') ||
+ ($params['type'] === 'detached-signature')) {
+ return $this->_decryptSignature($text, $params);
+ }
+ }
+ }
+
+ /**
+ * Returns whether a text has been encrypted symmetrically.
+ *
+ * @param string $text The PGP encrypted text.
+ *
+ * @return boolean True if the text is symmetricallly encrypted.
+ */
+ public function encryptedSymmetrically($text)
+ {
+ $cmdline = array(
+ '--decrypt',
+ '--batch'
+ );
+ $result = $this->_callGpg($cmdline, 'w', $text, true, true, true);
+ return strpos($result->stderr, 'gpg: encrypted with 1 passphrase') !== false;
+ }
+
+ /**
+ * Creates a temporary gpg keyring.
+ *
+ * @param string $type The type of key to analyze. Either 'public'
+ * (Default) or 'private'
+ *
+ * @return string Command line keystring option to use with gpg program.
+ */
+ protected function _createKeyring($type = 'public')
+ {
+ $type = String::lower($type);
+
+ if ($type === 'public') {
+ if (empty($this->_publicKeyring)) {
+ $this->_publicKeyring = $this->_createTempFile('horde-pgp');
+ }
+ return '--keyring ' . $this->_publicKeyring;
+ } elseif ($type === 'private') {
+ if (empty($this->_privateKeyring)) {
+ $this->_privateKeyring = $this->_createTempFile('horde-pgp');
+ }
+ return '--secret-keyring ' . $this->_privateKeyring;
+ }
+ }
+
+ /**
+ * Adds PGP keys to the keyring.
+ *
+ * @param mixed $keys A single key or an array of key(s) to add to the
+ * keyring.
+ * @param string $type The type of key(s) to add. Either 'public'
+ * (Default) or 'private'
+ *
+ * @return string Command line keystring option to use with gpg program.
+ */
+ protected function _putInKeyring($keys = array(), $type = 'public')
+ {
+ $type = String::lower($type);
+
+ if (!is_array($keys)) {
+ $keys = array($keys);
+ }
+
+ /* Create the keyrings if they don't already exist. */
+ $keyring = $this->_createKeyring($type);
+
+ /* Store the key(s) in the keyring. */
+ $cmdline = array(
+ '--allow-secret-key-import',
+ '--fast-import',
+ $keyring
+ );
+ $this->_callGpg($cmdline, 'w', array_values($keys));
+
+ return $keyring;
+ }
+
+ /**
+ * Encrypts a message in PGP format using a public key.
+ *
+ * @param string $text The text to be encrypted.
+ * @param array $params The parameters needed for encryption.
+ * <pre>
+ * Parameters:
+ * ===========
+ * 'type' => 'message' (REQUIRED)
+ * 'symmetric' => Whether to use symmetric instead of asymmetric
+ * encryption (defaults to false)
+ * 'recips' => An array with the e-mail address of the recipient as
+ * the key and that person's public key as the value.
+ * (REQUIRED if 'symmetric' is false)
+ * 'passphrase' => The passphrase for the symmetric encryption (REQUIRED if
+ * 'symmetric' is true)
+ * </pre>
+ *
+ * @return string The encrypted message.
+ * @throws Horde_Exception
+ */
+ protected function _encryptMessage($text, $params)
+ {
+ /* Create temp files for input. */
+ $input = $this->_createTempFile('horde-pgp');
+ file_put_contents($input, $text);
+
+ /* Build command line. */
+ $cmdline = array(
+ '--armor',
+ '--batch',
+ '--always-trust'
+ );
+ if (empty($params['symmetric'])) {
+ /* Store public key in temporary keyring. */
+ $keyring = $this->_putInKeyring(array_values($params['recips']));
+
+ $cmdline[] = $keyring;
+ $cmdline[] = '--encrypt';
+ foreach (array_keys($params['recips']) as $val) {
+ $cmdline[] = '--recipient ' . $val;
+ }
+ } else {
+ $cmdline[] = '--symmetric';
+ $cmdline[] = '--passphrase-fd 0';
+ }
+ $cmdline[] = $input;
+
+ /* Encrypt the document. */
+ $result = $this->_callGpg($cmdline, 'w', empty($params['symmetric']) ? null : $params['passphrase'], true, true);
+ if (empty($result->output)) {
+ $error = preg_replace('/\n.*/', '', $result->stderr);
+ throw new Horde_Exception(_("Could not PGP encrypt message: ") . $error, 'horde.error');
+ }
+
+ return $result->output;
+ }
+
+ /**
+ * Signs a message in PGP format using a private key.
+ *
+ * @param string $text The text to be signed.
+ * @param array $params The parameters needed for signing.
+ * <pre>
+ * Parameters:
+ * ===========
+ * 'type' => 'signature' (REQUIRED)
+ * 'pubkey' => PGP public key. (REQUIRED)
+ * 'privkey' => PGP private key. (REQUIRED)
+ * 'passphrase' => Passphrase for PGP Key. (REQUIRED)
+ * 'sigtype' => Determine the signature type to use. (Optional)
+ * 'cleartext' -- Make a clear text signature
+ * 'detach' -- Make a detached signature (DEFAULT)
+ * </pre>
+ *
+ * @return string The signed message.
+ * @throws Horde_Exception
+ */
+ protected function _encryptSignature($text, $params)
+ {
+ /* Check for required parameters. */
+ if (!isset($params['pubkey']) ||
+ !isset($params['privkey']) ||
+ !isset($params['passphrase'])) {
+ throw new Horde_Exception(_("A public PGP key, private PGP key, and passphrase are required to sign a message."), 'horde.error');
+ }
+
+ /* Create temp files for input. */
+ $input = $this->_createTempFile('horde-pgp');
+
+ /* Encryption requires both keyrings. */
+ $pub_keyring = $this->_putInKeyring(array($params['pubkey']));
+ $sec_keyring = $this->_putInKeyring(array($params['privkey']), 'private');
+
+ /* Store message in temporary file. */
+ file_put_contents($input, $text);
+
+ /* Determine the signature type to use. */
+ $cmdline = array();
+ if (isset($params['sigtype']) &&
+ $params['sigtype'] == 'cleartext') {
+ $sign_type = '--clearsign';
+ } else {
+ $sign_type = '--detach-sign';
+ }
+
+ /* Additional GPG options. */
+ $cmdline += array(
+ '--armor',
+ '--batch',
+ '--passphrase-fd 0',
+ $sec_keyring,
+ $pub_keyring,
+ $sign_type,
+ $input
+ );
+
+ /* Sign the document. */
+ $result = $this->_callGpg($cmdline, 'w', $params['passphrase'], true, true);
+ if (empty($result->output)) {
+ $error = preg_replace('/\n.*/', '', $result->stderr);
+ throw new Horde_Exception(_("Could not PGP sign message: ") . $error, 'horde.error');
+ }
+
+ return $result->output;
+ }
+
+ /**
+ * Decrypts an PGP encrypted message using a private/public keypair and a
+ * passhprase.
+ *
+ * @param string $text The text to be decrypted.
+ * @param array $params The parameters needed for decryption.
+ * <pre>
+ * Parameters:
+ * ===========
+ * 'type' => 'message' (REQUIRED)
+ * 'pubkey' => PGP public key. (REQUIRED for asymmetric encryption)
+ * 'privkey' => PGP private key. (REQUIRED for asymmetric encryption)
+ * 'passphrase' => Passphrase for PGP Key. (REQUIRED)
+ * </pre>
+ *
+ * @return stdClass An object with the following properties:
+ * <pre>
+ * 'message' - The decrypted message.
+ * 'sig_result' - The result of the signature test.
+ * </pre>
+ * @return stdClass An object with the following properties:
+ * <pre>
+ * 'message' - (string) The signature result text.
+ * 'result' - (boolean) The result of the signature test.
+ * </pre>
+ * @throws Horde_Exception
+ */
+ protected function _decryptMessage($text, $params)
+ {
+ $good_sig_flag = false;
+
+ /* Check for required parameters. */
+ if (!isset($params['passphrase']) && empty($params['no_passphrase'])) {
+ throw new Horde_Exception(_("A passphrase is required to decrypt a message."), 'horde.error');
+ }
+
+ /* Create temp files. */
+ $input = $this->_createTempFile('horde-pgp');
+
+ /* Store message in file. */
+ file_put_contents($input, $text);
+
+ /* Build command line. */
+ $cmdline = array(
+ '--always-trust',
+ '--armor',
+ '--batch'
+ );
+ if (empty($param['no_passphrase'])) {
+ $cmdline[] = '--passphrase-fd 0';
+ }
+ if (!empty($params['pubkey']) && !empty($params['privkey'])) {
+ /* Decryption requires both keyrings. */
+ $pub_keyring = $this->_putInKeyring(array($params['pubkey']));
+ $sec_keyring = $this->_putInKeyring(array($params['privkey']), 'private');
+ $cmdline[] = $sec_keyring;
+ $cmdline[] = $pub_keyring;
+ }
+ $cmdline[] = '--decrypt';
+ $cmdline[] = $input;
+
+ /* Decrypt the document now. */
+ if (empty($params['no_passphrase'])) {
+ $result = $this->_callGpg($cmdline, 'w', $params['passphrase'], true, true);
+ } else {
+ $result = $this->_callGpg($cmdline, 'r', null, true, true);
+ }
+ if (empty($result->output)) {
+ $error = preg_replace('/\n.*/', '', $result->stderr);
+ throw new Horde_Exception(_("Could not decrypt PGP data: ") . $error, 'horde.error');
+ }
+
+ /* Create the return object. */
+ return $this->_checkSignatureResult($result->stderr, $result->output);
+ }
+
+ /**
+ * Decrypts an PGP signed message using a public key.
+ *
+ * @param string $text The text to be verified.
+ * @param array $params The parameters needed for verification.
+ * <pre>
+ * Parameters:
+ * ===========
+ * 'type' => 'signature' or 'detached-signature' (REQUIRED)
+ * 'pubkey' => PGP public key. (REQUIRED)
+ * 'signature' => PGP signature block. (REQUIRED for detached signature)
+ * </pre>
+ *
+ * @return stdClass An object with the following properties:
+ * <pre>
+ * 'message' - (string) The signature result text.
+ * 'result' - (boolean) The result of the signature test.
+ * </pre>
+ * @throws Horde_Exception
+ */
+ protected function _decryptSignature($text, $params)
+ {
+ /* Check for required parameters. */
+ if (!isset($params['pubkey'])) {
+ throw new Horde_Exception(_("A public PGP key is required to verify a signed message."), 'horde.error');
+ }
+ if (($params['type'] === 'detached-signature') &&
+ !isset($params['signature'])) {
+ throw new Horde_Exception(_("The detached PGP signature block is required to verify the signed message."), 'horde.error');
+ }
+
+ $good_sig_flag = 0;
+
+ /* Create temp files for input. */
+ $input = $this->_createTempFile('horde-pgp');
+
+ /* Store public key in temporary keyring. */
+ $keyring = $this->_putInKeyring($params['pubkey']);
+
+ /* Store the message in a temporary file. */
+ file_put_contents($input, $text);
+
+ /* Options for the GPG binary. */
+ $cmdline = array(
+ '--armor',
+ '--always-trust',
+ '--batch',
+ '--charset ' . NLS::getCharset(),
+ $keyring,
+ '--verify'
+ );
+
+ /* Extra stuff to do if we are using a detached signature. */
+ if ($params['type'] === 'detached-signature') {
+ $sigfile = $this->_createTempFile('horde-pgp');
+ $cmdline[] = $sigfile . ' ' . $input;
+ file_put_contents($sigfile, $params['signature']);
+ } else {
+ $cmdline[] = $input;
+ }
+
+ /* Verify the signature. We need to catch standard error output,
+ * since this is where the signature information is sent. */
+ $result = $this->_callGpg($cmdline, 'r', null, true, true, true);
+ return $this->_checkSignatureResult($result->stderr, $result->stderr);
+ }
+
+ /**
+ * Checks signature result from the GnuPG binary.
+ *
+ * @param string $result The signature result.
+ * @param string $message The decrypted message data.
+ *
+ * @return stdClass An object with the following properties:
+ * <pre>
+ * 'message' - (string) The signature result text.
+ * 'result' - (boolean) The result of the signature test.
+ * </pre>
+ * @throws Horde_Exception
+ */
+ protected function _checkSignatureResult($result, $message = null)
+ {
+ /* Good signature:
+ * gpg: Good signature from "blah blah blah (Comment)"
+ * Bad signature:
+ * gpg: BAD signature from "blah blah blah (Comment)" */
+ if (strpos($result, 'gpg: BAD signature') !== false) {
+ throw new Horde_Exception($result, 'horde.error');
+ }
+
+ $ob = new stdClass;
+ $ob->message = $message;
+ $ob->result = (strpos($result, 'gpg: Good signature') !== false);
+
+ return $ob;
+ }
+
+ /**
+ * Signs a MIME part using PGP.
+ *
+ * @param Horde_Mime_Part $mime_part The object to sign.
+ * @param array $params The parameters required for signing.
+ * @see _encryptSignature().
+ *
+ * @return mixed A Horde_Mime_Part object that is signed according to RFC
+ * 3156.
+ * @throws Horde_Exception
+ */
+ public function signMIMEPart($mime_part, $params = array())
+ {
+ $params = array_merge($params, array('type' => 'signature', 'sigtype' => 'detach'));
+
+ /* RFC 3156 Requirements for a PGP signed message:
+ * + Content-Type params 'micalg' & 'protocol' are REQUIRED.
+ * + The digitally signed message MUST be constrained to 7 bits.
+ * + The MIME headers MUST be a part of the signed data. */
+
+ $mime_part->strict7bit(true);
+ $msg_sign = $this->encrypt($mime_part->toCanonicalString(), $params);
+
+ /* Add the PGP signature. */
+ $charset = NLS::getEmailCharset();
+ $pgp_sign = new Horde_Mime_Part();
+ $pgp_sign->setType('application/pgp-signature');
+ $pgp_sign->setCharset($charset);
+ $pgp_sign->setDisposition('inline');
+ $pgp_sign->setDescription(String::convertCharset(_("PGP Digital Signature"), NLS::getCharset(), $charset));
+ $pgp_sign->setContents($msg_sign);
+
+ /* Get the algorithim information from the signature. Since we are
+ * analyzing a signature packet, we need to use the special keyword
+ * '_SIGNATURE' - see Horde_Crypt_Pgp. */
+ $sig_info = $this->pgpPacketSignature($msg_sign, '_SIGNATURE');
+
+ /* Setup the multipart MIME Part. */
+ $part = new Horde_Mime_Part();
+ $part->setType('multipart/signed');
+ $part->setContents('This message is in MIME format and has been PGP signed.' . "\n");
+ $part->addPart($mime_part);
+ $part->addPart($pgp_sign);
+ $part->setContentTypeParameter('protocol', 'application/pgp-signature');
+ $part->setContentTypeParameter('micalg', $sig_info['micalg']);
+
+ return $part;
+ }
+
+ /**
+ * Encrypts a MIME part using PGP.
+ *
+ * @param Horde_Mime_Part $mime_part The object to encrypt.
+ * @param array $params The parameters required for
+ * encryption.
+ * @see _encryptMessage().
+ *
+ * @return mixed A Horde_Mime_Part object that is encrypted according to
+ * RFC 3156.
+ * @throws Horde_Exception
+ */
+ public function encryptMIMEPart($mime_part, $params = array())
+ {
+ $params = array_merge($params, array('type' => 'message'));
+
+ $signenc_body = $mime_part->toCanonicalString();
+ $message_encrypt = $this->encrypt($signenc_body, $params);
+
+ /* Set up MIME Structure according to RFC 3156. */
+ $charset = NLS::getEmailCharset();
+ $part = new Horde_Mime_Part();
+ $part->setType('multipart/encrypted');
+ $part->setCharset($charset);
+ $part->setContentTypeParameter('protocol', 'application/pgp-encrypted');
+ $part->setDescription(String::convertCharset(_("PGP Encrypted Data"), NLS::getCharset(), $charset));
+ $part->setContents('This message is in MIME format and has been PGP encrypted.' . "\n");
+
+ $part1 = new Horde_Mime_Part();
+ $part1->setType('application/pgp-encrypted');
+ $part1->setCharset(null);
+ $part1->setContents("Version: 1\n");
+ $part->addPart($part1);
+
+ $part2 = new Horde_Mime_Part();
+ $part2->setType('application/octet-stream');
+ $part2->setCharset(null);
+ $part2->setContents($message_encrypt);
+ $part2->setDisposition('inline');
+ $part->addPart($part2);
+
+ return $part;
+ }
+
+ /**
+ * Signs and encrypts a MIME part using PGP.
+ *
+ * @param Horde_Mime_Part $mime_part The object to sign and encrypt.
+ * @param array $sign_params The parameters required for
+ * signing. @see _encryptSignature().
+ * @param array $encrypt_params The parameters required for
+ * encryption. @see _encryptMessage().
+ *
+ * @return mixed A Horde_Mime_Part object that is signed and encrypted
+ * according to RFC 3156.
+ * @throws Horde_Exception
+ */
+ public function signAndEncryptMIMEPart($mime_part, $sign_params = array(),
+ $encrypt_params = array())
+ {
+ /* RFC 3156 requires that the entire signed message be encrypted. We
+ * need to explicitly call using Horde_Crypt_Pgp:: because we don't
+ * know whether a subclass has extended these methods. */
+ $part = $this->signMIMEPart($mime_part, $sign_params);
+ $part = $this->encryptMIMEPart($part, $encrypt_params);
+ $part->setContents('This message is in MIME format and has been PGP signed and encrypted.' . "\n");
+
+ $charset = NLS::getEmailCharset();
+ $part->setCharset($charset);
+ $part->setDescription(String::convertCharset(_("PGP Signed/Encrypted Data"), NLS::getCharset(), $charset));
+
+ return $part;
+ }
+
+ /**
+ * Generates a Horde_Mime_Part object, in accordance with RFC 3156, that
+ * contains a public key.
+ *
+ * @param string $key The public key.
+ *
+ * @return Horde_Mime_Part An object that contains the public key.
+ */
+ public function publicKeyMIMEPart($key)
+ {
+ $charset = NLS::getEmailCharset();
+ $part = new Horde_Mime_Part();
+ $part->setType('application/pgp-keys');
+ $part->setCharset($charset);
+ $part->setDescription(String::convertCharset(_("PGP Public Key"), NLS::getCharset(), $charset));
+ $part->setContents($key);
+
+ return $part;
+ }
+
+ /**
+ * Function that handles interfacing with the GnuPG binary.
+ *
+ * @param array $options Options and commands to pass to GnuPG.
+ * @param string $mode 'r' to read from stdout, 'w' to write to
+ * stdin.
+ * @param array $input Input to write to stdin.
+ * @param boolean $output Collect and store output in object returned?
+ * @param boolean $stderr Collect and store stderr in object returned?
+ * @param boolean $verbose Run GnuPG with verbose flag?
+ *
+ * @return stdClass Class with members output, stderr, and stdout.
+ */
+ protected function _callGpg($options, $mode, $input = array(),
+ $output = false, $stderr = false,
+ $verbose = false)
+ {
+ $data = new stdClass;
+ $data->output = null;
+ $data->stderr = null;
+ $data->stdout = null;
+
+ /* Verbose output? */
+ if (!$verbose) {
+ array_unshift($options, '--quiet');
+ }
+
+ /* Create temp files for output. */
+ if ($output) {
+ $output_file = $this->_createTempFile('horde-pgp', false);
+ array_unshift($options, '--output ' . $output_file);
+
+ /* Do we need standard error output? */
+ if ($stderr) {
+ $stderr_file = $this->_createTempFile('horde-pgp', false);
+ $options[] = '2> ' . $stderr_file;
+ }
+ }
+
+ /* Silence errors if not requested. */
+ if (!$output || !$stderr) {
+ $options[] = '2> /dev/null';
+ }
+
+ /* Build the command line string now. */
+ $cmdline = implode(' ', array_merge($this->_gnupg, $options));
+
+ if ($mode == 'w') {
+ $fp = popen($cmdline, 'w');
+ $win32 = !strncasecmp(PHP_OS, 'WIN', 3);
+
+ if (!is_array($input)) {
+ $input = array($input);
+ }
+ foreach ($input as $line) {
+ if ($win32 && (strpos($line, "\x0d\x0a") !== false)) {
+ $chunks = explode("\x0d\x0a", $line);
+ foreach ($chunks as $chunk) {
+ fputs($fp, $chunk . "\n");
+ }
+ } else {
+ fputs($fp, $line . "\n");
+ }
+ }
+ } elseif ($mode == 'r') {
+ $fp = popen($cmdline, 'r');
+ while (!feof($fp)) {
+ $data->stdout .= fgets($fp, 1024);
+ }
+ }
+ pclose($fp);
+
+ if ($output) {
+ $data->output = file_get_contents($output_file);
+ unlink($output_file);
+ if ($stderr) {
+ $data->stderr = file_get_contents($stderr_file);
+ unlink($stderr_file);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Generates a revocation certificate.
+ *
+ * @param string $key The private key.
+ * @param string $email The email to use for the key.
+ * @param string $passphrase The passphrase to use for the key.
+ *
+ * @return string The revocation certificate.
+ * @throws Horde_Exception
+ */
+ public function generateRevocation($key, $email, $passphrase)
+ {
+ $keyring = $this->_putInKeyring($key, 'private');
+
+ /* Prepare the canned answers. */
+ $input = array(
+ 'y', // Really generate a revocation certificate
+ '0', // Refuse to specify a reason
+ '', // Empty comment
+ 'y', // Confirm empty comment
+ );
+ if (!empty($passphrase)) {
+ $input[] = $passphrase;
+ }
+
+ /* Run through gpg binary. */
+ $cmdline = array(
+ $keyring,
+ '--command-fd 0',
+ '--gen-revoke' . ' ' . $email,
+ );
+ $results = $this->_callGpg($cmdline, 'w', $input, true);
+
+ /* If the key is empty, something went wrong. */
+ if (empty($results->output)) {
+ throw new Horde_Exception(_("Revocation key not generated successfully."), 'horde.error');
+ }
+
+ return $results->output;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Horde_Crypt_Smime:: provides a framework for Horde applications to
+ * interact with the OpenSSL library and implement S/MIME.
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @package Horde_Crypt
+ */
+class Horde_Crypt_Smime extends Horde_Crypt
+{
+ /**
+ * Object Identifers to name array.
+ *
+ * @var array
+ */
+ protected $_oids = array(
+ '2.5.4.3' => 'CommonName',
+ '2.5.4.4' => 'Surname',
+ '2.5.4.6' => 'Country',
+ '2.5.4.7' => 'Location',
+ '2.5.4.8' => 'StateOrProvince',
+ '2.5.4.9' => 'StreetAddress',
+ '2.5.4.10' => 'Organisation',
+ '2.5.4.11' => 'OrganisationalUnit',
+ '2.5.4.12' => 'Title',
+ '2.5.4.20' => 'TelephoneNumber',
+ '2.5.4.42' => 'GivenName',
+
+ '2.5.29.14' => 'id-ce-subjectKeyIdentifier',
+
+ '2.5.29.14' => 'id-ce-subjectKeyIdentifier',
+ '2.5.29.15' => 'id-ce-keyUsage',
+ '2.5.29.17' => 'id-ce-subjectAltName',
+ '2.5.29.19' => 'id-ce-basicConstraints',
+ '2.5.29.31' => 'id-ce-CRLDistributionPoints',
+ '2.5.29.32' => 'id-ce-certificatePolicies',
+ '2.5.29.35' => 'id-ce-authorityKeyIdentifier',
+ '2.5.29.37' => 'id-ce-extKeyUsage',
+
+ '1.2.840.113549.1.9.1' => 'Email',
+ '1.2.840.113549.1.1.1' => 'RSAEncryption',
+ '1.2.840.113549.1.1.2' => 'md2WithRSAEncryption',
+ '1.2.840.113549.1.1.4' => 'md5withRSAEncryption',
+ '1.2.840.113549.1.1.5' => 'SHA-1WithRSAEncryption',
+ '1.2.840.10040.4.3' => 'id-dsa-with-sha-1',
+
+ '1.3.6.1.5.5.7.3.2' => 'id_kp_clientAuth',
+
+ '2.16.840.1.113730.1.1' => 'netscape-cert-type',
+ '2.16.840.1.113730.1.2' => 'netscape-base-url',
+ '2.16.840.1.113730.1.3' => 'netscape-revocation-url',
+ '2.16.840.1.113730.1.4' => 'netscape-ca-revocation-url',
+ '2.16.840.1.113730.1.7' => 'netscape-cert-renewal-url',
+ '2.16.840.1.113730.1.8' => 'netscape-ca-policy-url',
+ '2.16.840.1.113730.1.12' => 'netscape-ssl-server-name',
+ '2.16.840.1.113730.1.13' => 'netscape-comment',
+ );
+
+ /**
+ * Constructor.
+ *
+ * @param array $params Parameter array.
+ * 'temp' => Location of temporary directory.
+ */
+ protected function __construct($params)
+ {
+ $this->_tempdir = $params['temp'];
+ }
+
+ /**
+ * Verify a passphrase for a given private key.
+ *
+ * @param string $private_key The user's private key.
+ * @param string $passphrase The user's passphrase.
+ *
+ * @return boolean Returns true on valid passphrase, false on invalid
+ * passphrase.
+ */
+ public function verifyPassphrase($private_key, $passphrase)
+ {
+ $res = is_null($passphrase)
+ ? openssl_pkey_get_private($private_key)
+ : openssl_pkey_get_private($private_key, $passphrase);
+
+ return is_resource($res);
+ }
+
+ /**
+ * Encrypt text using S/MIME.
+ *
+ * @param string $text The text to be encrypted.
+ * @param array $params The parameters needed for encryption.
+ * See the individual _encrypt*() functions for
+ * the parameter requirements.
+ *
+ * @return string The encrypted message.
+ * @throws Horde_Exception
+ */
+ public function encrypt($text, $params = array())
+ {
+ /* Check for availability of OpenSSL PHP extension. */
+ $this->checkForOpenSSL();
+
+ if (isset($params['type'])) {
+ if ($params['type'] === 'message') {
+ return $this->_encryptMessage($text, $params);
+ } elseif ($params['type'] === 'signature') {
+ return $this->_encryptSignature($text, $params);
+ }
+ }
+ }
+
+ /**
+ * Decrypt text via S/MIME.
+ *
+ * @param string $text The text to be smime decrypted.
+ * @param array $params The parameters needed for decryption.
+ * See the individual _decrypt*() functions for
+ * the parameter requirements.
+ *
+ * @return string The decrypted message.
+ * @throws Horde_Exception
+ */
+ public function decrypt($text, $params = array())
+ {
+ /* Check for availability of OpenSSL PHP extension. */
+ $this->checkForOpenSSL();
+
+ if (isset($params['type'])) {
+ if ($params['type'] === 'message') {
+ return $this->_decryptMessage($text, $params);
+ } elseif (($params['type'] === 'signature') ||
+ ($params['type'] === 'detached-signature')) {
+ return $this->_decryptSignature($text, $params);
+ }
+ }
+ }
+
+ /**
+ * Verify a signature using via S/MIME.
+ *
+ * @param string $text The multipart/signed data to be verified.
+ * @param mixed $certs Either a single or array of root certificates.
+ *
+ * @return stdClass Object with the following elements:
+ * 'result' -> Returns true on success.
+ * 'cert' -> The certificate of the signer stored
+ * in the message (in PEM format).
+ * 'email' -> The email of the signing person.
+ * @throws Horde_Exception
+ */
+ public function verify($text, $certs)
+ {
+ /* Check for availability of OpenSSL PHP extension. */
+ $openssl = $this->checkForOpenSSL();
+
+ /* Create temp files for input/output. */
+ $input = $this->_createTempFile('horde-smime');
+ $output = $this->_createTempFile('horde-smime');
+
+ /* Write text to file */
+ file_put_contents($input, $text);
+ unset($text);
+
+ $root_certs = array();
+ if (!is_array($certs)) {
+ $certs = array($certs);
+ }
+ foreach ($certs as $file) {
+ if (file_exists($file)) {
+ $root_certs[] = $file;
+ }
+ }
+
+ $ob = new stdClass;
+
+ if (!empty($root_certs)) {
+ $result = openssl_pkcs7_verify($input, 0, $output, $root_certs);
+ /* Message verified */
+ if ($result === true) {
+ $ob->result = true;
+ $ob->cert = file_get_contents($output);
+ $ob->email = $this->getEmailFromKey($ob->cert);
+ return $ob;
+ }
+ }
+
+ /* Try again without verfying the signer's cert */
+ $result = openssl_pkcs7_verify($input, PKCS7_NOVERIFY, $output);
+
+ if ($result === true) {
+ throw new Horde_Exception(_("Message Verified Successfully but the signer's certificate could not be verified."), 'horde.warning');
+ } elseif ($result == -1) {
+ throw new Horde_Exception(_("Verification failed - an unknown error has occurred."), 'horde.error');
+ } else {
+ throw new Horde_Exception(_("Verification failed - this message may have been tampered with."), 'horde.error');
+ }
+
+ $ob->cert = file_get_contents($output);
+ $ob->email = $this->getEmailFromKey($ob->cert);
+
+ return $ob;
+ }
+
+ /**
+ * Extract the contents from signed S/MIME data.
+ *
+ * @param string $data The signed S/MIME data.
+ * @param string $sslpath The path to the OpenSSL binary.
+ *
+ * @return string The contents embedded in the signed data.
+ * @throws Horde_Exception
+ */
+ public function extractSignedContents($data, $sslpath)
+ {
+ /* Check for availability of OpenSSL PHP extension. */
+ $this->checkForOpenSSL();
+
+ /* Create temp files for input/output. */
+ $input = $this->_createTempFile('horde-smime');
+ $output = $this->_createTempFile('horde-smime');
+
+ /* Write text to file. */
+ file_put_contents($input, $data);
+ unset($data);
+
+ exec($sslpath . ' smime -verify -noverify -nochain -in ' . $input . ' -out ' . $output);
+
+ $ret = file_get_contents($output);
+ if ($ret) {
+ return $ret;
+ }
+
+ throw new Horde_Exception(_("OpenSSL error: Could not extract data from signed S/MIME part."), 'horde.error');
+ }
+
+ /**
+ * Sign a MIME part using S/MIME.
+ *
+ * @param Horde_Mime_Part $mime_part The object to sign.
+ * @param array $params The parameters required for signing.
+ *
+ * @return mixed A Horde_Mime_Part object that is signed.
+ * @throws Horde_Exception
+ */
+ public function signMIMEPart($mime_part, $params)
+ {
+ /* Sign the part as a message */
+ $message = $this->encrypt($mime_part->toCanonicalString(), $params);
+
+ /* Break the result into its components */
+ $mime_message = Horde_Mime_Part::parseMessage($message);
+
+ $smime_sign = $mime_message->getPart('2');
+ $smime_sign->setDescription(_("S/MIME Cryptographic Signature"));
+ $smime_sign->transferDecodeContents();
+ $smime_sign->setTransferEncoding('base64');
+
+ $smime_part = new Horde_Mime_Part();
+ $smime_part->setType('multipart/signed');
+ $smime_part->setContents('This is a cryptographically signed message in MIME format.' . "\n");
+ $smime_part->setContentTypeParameter('protocol', 'application/pkcs7-signature');
+ $smime_part->setContentTypeParameter('micalg', 'sha1');
+ $smime_part->addPart($mime_part);
+ $smime_part->addPart($smime_sign);
+
+ return $smime_part;
+ }
+
+ /**
+ * Encrypt a MIME part using S/MIME.
+ *
+ * @param Horde_Mime_Part $mime_part The object to encrypt.
+ * @param array $params The parameters required for
+ * encryption.
+ *
+ * @return mixed A Horde_Mime_Part object that is encrypted.
+ * @throws Horde_Exception
+ */
+ public function encryptMIMEPart($mime_part, $params = array())
+ {
+ /* Sign the part as a message */
+ $message = $this->encrypt($mime_part->toCanonicalString(), $params);
+
+ /* Get charset for mime part description. */
+ $charset = NLS::getEmailCharset();
+
+ $msg = new Horde_Mime_Part();
+ $msg->setCharset($charset);
+ $msg->setDescription(String::convertCharset(_("S/MIME Encrypted Message"), NLS::getCharset(), $charset));
+ $msg->setDisposition('inline');
+ $msg->setType('application/pkcs7-mime');
+ $msg->setContentTypeParameter('smime-type', 'enveloped-data');
+ $msg->setContents(substr($message, strpos($message, "\n\n") + 2));
+
+ return $msg;
+ }
+
+ /**
+ * Encrypt a message in S/MIME format using a public key.
+ *
+ * @param string $text The text to be encrypted.
+ * @param array $params The parameters needed for encryption.
+ * <pre>
+ * Parameters:
+ * ===========
+ * 'type' => 'message' (REQUIRED)
+ * 'pubkey' => public key (REQUIRED)
+ * </pre>
+ *
+ * @return string The encrypted message.
+ * @throws Horde_Exception
+ */
+ protected function _encryptMessage($text, $params)
+ {
+ /* Check for required parameters. */
+ if (!isset($params['pubkey'])) {
+ throw new Horde_Exception(_("A public S/MIME key is required to encrypt a message."), 'horde.error');
+ }
+
+ /* Create temp files for input/output. */
+ $input = $this->_createTempFile('horde-smime');
+ $output = $this->_createTempFile('horde-smime');
+
+ /* Store message in file. */
+ file_put_contents($input, $text);
+ unset($text);
+
+ /* Encrypt the document. */
+ if (openssl_pkcs7_encrypt($input, $output, $params['pubkey'], array())) {
+ $result = file_get_contents($output);
+ if (!empty($result)) {
+ return $this->_fixContentType($result, 'encrypt');
+ }
+ }
+
+ throw new Horde_Exception(_("Could not S/MIME encrypt message."), 'horde.error');
+ }
+
+ /**
+ * Sign a message in S/MIME format using a private key.
+ *
+ * @param string $text The text to be signed.
+ * @param array $params The parameters needed for signing.
+ * <pre>
+ * Parameters:
+ * ===========
+ * 'certs' => Additional signing certs (Optional)
+ * 'passphrase' => Passphrase for key (REQUIRED)
+ * 'privkey' => Private key (REQUIRED)
+ * 'pubkey' => Public key (REQUIRED)
+ * 'sigtype' => Determine the signature type to use. (Optional)
+ * 'cleartext' -- Make a clear text signature
+ * 'detach' -- Make a detached signature (DEFAULT)
+ * 'type' => 'signature' (REQUIRED)
+ * </pre>
+ *
+ * @return string The signed message.
+ * @throws Horde_Exception
+ */
+ protected function _encryptSignature($text, $params)
+ {
+ /* Check for required parameters. */
+ if (!isset($params['pubkey']) ||
+ !isset($params['privkey']) ||
+ !array_key_exists('passphrase', $params)) {
+ throw new Horde_Exception(_("A public S/MIME key, private S/MIME key, and passphrase are required to sign a message."), 'horde.error');
+ }
+
+ /* Create temp files for input/output/certificates. */
+ $input = $this->_createTempFile('horde-smime');
+ $output = $this->_createTempFile('horde-smime');
+ $certs = $this->_createTempFile('horde-smime');
+
+ /* Store message in temporary file. */
+ file_put_contents($input, $text);
+ unset($text);
+
+ /* Store additional certs in temporary file. */
+ if (!empty($params['certs'])) {
+ file_put_contents($certs, $params['certs']);
+ }
+
+ /* Determine the signature type to use. */
+ $flags = (isset($params['sigtype']) && ($params['sigtype'] == 'cleartext'))
+ ? PKCS7_TEXT
+ : PKCS7_DETACHED;
+
+ $privkey = (is_null($params['passphrase'])) ? $params['privkey'] : array($params['privkey'], $params['passphrase']);
+
+ if (empty($params['certs'])) {
+ $res = openssl_pkcs7_sign($input, $output, $params['pubkey'], $privkey, array(), $flags);
+ } else {
+ $res = openssl_pkcs7_sign($input, $output, $params['pubkey'], $privkey, array(), $flags, $certs);
+ }
+
+ if (!$res) {
+ throw new Horde_Exception(_("Could not S/MIME sign message."), 'horde.error');
+ }
+
+ $data = file_get_contents($output);
+ return $this->_fixContentType($data, 'signature');
+ }
+
+ /**
+ * Decrypt an S/MIME encrypted message using a private/public keypair
+ * and a passhprase.
+ *
+ * @param string $text The text to be decrypted.
+ * @param array $params The parameters needed for decryption.
+ * <pre>
+ * Parameters:
+ * ===========
+ * 'type' => 'message' (REQUIRED)
+ * 'pubkey' => public key. (REQUIRED)
+ * 'privkey' => private key. (REQUIRED)
+ * 'passphrase' => Passphrase for Key. (REQUIRED)
+ * </pre>
+ *
+ * @return string The decrypted message.
+ * @throws Horde_Exception
+ */
+ protected function _decryptMessage($text, $params)
+ {
+ /* Check for required parameters. */
+ if (!isset($params['pubkey']) ||
+ !isset($params['privkey']) ||
+ !array_key_exists('passphrase', $params)) {
+ throw new Horde_Exception(_("A public S/MIME key, private S/MIME key, and passphrase are required to decrypt a message."), 'horde.error');
+ }
+
+ /* Create temp files for input/output. */
+ $input = $this->_createTempFile('horde-smime');
+ $output = $this->_createTempFile('horde-smime');
+
+ /* Store message in file. */
+ file_put_contents($input, $text);
+ unset($text);
+
+ $privkey = (is_null($params['passphrase'])) ? $params['privkey'] : array($params['privkey'], $params['passphrase']);
+ if (openssl_pkcs7_decrypt($input, $output, $params['pubkey'], $privkey)) {
+ return file_get_contents($output);
+ }
+
+ throw new Horde_Exception(_("Could not decrypt S/MIME data."), 'horde.error');
+ }
+
+ /**
+ * Sign and Encrypt a MIME part using S/MIME.
+ *
+ * @param Horde_Mime_Part $mime_part The object to sign and encrypt.
+ * @param array $sign_params The parameters required for
+ * signing. @see _encryptSignature().
+ * @param array $encrypt_params The parameters required for
+ * encryption.
+ * @see _encryptMessage().
+ *
+ * @return mixed A Horde_Mime_Part object that is signed and encrypted.
+ * @throws Horde_Exception
+ */
+ public function signAndEncryptMIMEPart($mime_part, $sign_params = array(),
+ $encrypt_params = array())
+ {
+ $part = $this->signMIMEPart($mime_part, $sign_params);
+ return $this->encryptMIMEPart($part, $encrypt_params);
+ }
+
+ /**
+ * Convert a PEM format certificate to readable HTML version
+ *
+ * @param string $cert PEM format certificate
+ *
+ * @return string HTML detailing the certificate.
+ */
+ public function certToHTML($cert)
+ {
+ /* Common Fields */
+ $fieldnames = array(
+ 'Email' => _("Email Address"),
+ 'CommonName' => _("Common Name"),
+ 'Organisation' => _("Organisation"),
+ 'OrganisationalUnit' => _("Organisational Unit"),
+ 'Country' => _("Country"),
+ 'StateOrProvince' => _("State or Province"),
+ 'Location' => _("Location"),
+ 'StreetAddress' => _("Street Address"),
+ 'TelephoneNumber' => _("Telephone Number"),
+ 'Surname' => _("Surname"),
+ 'GivenName' => _("Given Name")
+ );
+
+ /* Netscape Extensions */
+ $fieldnames += array(
+ 'netscape-cert-type' => _("Netscape certificate type"),
+ 'netscape-base-url' => _("Netscape Base URL"),
+ 'netscape-revocation-url' => _("Netscape Revocation URL"),
+ 'netscape-ca-revocation-url' => _("Netscape CA Revocation URL"),
+ 'netscape-cert-renewal-url' => _("Netscape Renewal URL"),
+ 'netscape-ca-policy-url' => _("Netscape CA policy URL"),
+ 'netscape-ssl-server-name' => _("Netscape SSL server name"),
+ 'netscape-comment' => _("Netscape certificate comment")
+ );
+
+ /* X590v3 Extensions */
+ $fieldnames += array(
+ 'id-ce-extKeyUsage' => _("X509v3 Extended Key Usage"),
+ 'id-ce-basicConstraints' => _("X509v3 Basic Constraints"),
+ 'id-ce-subjectAltName' => _("X509v3 Subject Alternative Name"),
+ 'id-ce-subjectKeyIdentifier' => _("X509v3 Subject Key Identifier"),
+ 'id-ce-certificatePolicies' => _("Certificate Policies"),
+ 'id-ce-CRLDistributionPoints' => _("CRL Distribution Points"),
+ 'id-ce-keyUsage' => _("Key Usage")
+ );
+
+ $cert_details = $this->parseCert($cert);
+ if (!is_array($cert_details)) {
+ return '<pre class="fixed">' . _("Unable to extract certificate details") . '</pre>';
+ }
+ $certificate = $cert_details['certificate'];
+
+ $text = '<pre class="fixed">';
+
+ /* Subject (a/k/a Certificate Owner) */
+ if (isset($certificate['subject'])) {
+ $text .= "<strong>" . _("Certificate Owner") . ":</strong>\n";
+
+ foreach ($certificate['subject'] as $key => $value) {
+ if (isset($fieldnames[$key])) {
+ $text .= sprintf(" %s: %s\n", $fieldnames[$key], $value);
+ } else {
+ $text .= sprintf(" *%s: %s\n", $key, $value);
+ }
+ }
+ $text .= "\n";
+ }
+
+ /* Issuer */
+ if (isset($certificate['issuer'])) {
+ $text .= "<strong>" . _("Issuer") . ":</strong>\n";
+
+ foreach ($certificate['issuer'] as $key => $value) {
+ if (isset($fieldnames[$key])) {
+ $text .= sprintf(" %s: %s\n", $fieldnames[$key], $value);
+ } else {
+ $text .= sprintf(" *%s: %s\n", $key, $value);
+ }
+ }
+ $text .= "\n";
+ }
+
+ /* Dates */
+ $text .= "<strong>" . _("Validity") . ":</strong>\n";
+ $text .= sprintf(" %s: %s\n", _("Not Before"), strftime("%x %X", $certificate['validity']['notbefore']));
+ $text .= sprintf(" %s: %s\n", _("Not After"), strftime("%x %X", $certificate['validity']['notafter']));
+ $text .= "\n";
+
+ /* Certificate Owner - Public Key Info */
+ $text .= "<strong>" . _("Public Key Info") . ":</strong>\n";
+ $text .= sprintf(" %s: %s\n", _("Public Key Algorithm"), $certificate['subjectPublicKeyInfo']['algorithm']);
+ if ($certificate['subjectPublicKeyInfo']['algorithm'] == 'rsaEncryption') {
+ if (Util::extensionExists('bcmath')) {
+ $modulus = $certificate['subjectPublicKeyInfo']['subjectPublicKey']['modulus'];
+ $modulus_hex = '';
+ while ($modulus != '0') {
+ $modulus_hex = dechex(bcmod($modulus, '16')) . $modulus_hex;
+ $modulus = bcdiv($modulus, '16', 0);
+ }
+
+ if ((strlen($modulus_hex) > 64) &&
+ (strlen($modulus_hex) < 128)) {
+ str_pad($modulus_hex, 128, '0', STR_PAD_RIGHT);
+ } elseif ((strlen($modulus_hex) > 128) &&
+ (strlen($modulus_hex) < 256)) {
+ str_pad($modulus_hex, 256, '0', STR_PAD_RIGHT);
+ }
+
+ $text .= " " . sprintf(_("RSA Public Key (%d bit)"), strlen($modulus_hex) * 4) . ":\n";
+
+ $modulus_str = '';
+ for ($i = 0; $i < strlen($modulus_hex); $i += 2) {
+ if (($i % 32) == 0) {
+ $modulus_str .= "\n ";
+ }
+ $modulus_str .= substr($modulus_hex, $i, 2) . ':';
+ }
+
+ $text .= sprintf(" %s: %s\n", _("Modulus"), $modulus_str);
+ }
+
+ $text .= sprintf(" %s: %s\n", _("Exponent"), $certificate['subjectPublicKeyInfo']['subjectPublicKey']['publicExponent']);
+ }
+ $text .= "\n";
+
+ /* X509v3 extensions */
+ if (isset($certificate['extensions'])) {
+ $text .= "<strong>" . _("X509v3 extensions") . ":</strong>\n";
+
+ foreach ($certificate['extensions'] as $key => $value) {
+ if (is_array($value)) {
+ $value = _("Unsupported Extension");
+ }
+ if (isset($fieldnames[$key])) {
+ $text .= sprintf(" %s:\n %s\n", $fieldnames[$key], wordwrap($value, 40, "\n "));
+ } else {
+ $text .= sprintf(" %s:\n %s\n", $key, wordwrap($value, 60, "\n "));
+ }
+ }
+
+ $text .= "\n";
+ }
+
+ /* Certificate Details */
+ $text .= "<strong>" . _("Certificate Details") . ":</strong>\n";
+ $text .= sprintf(" %s: %d\n", _("Version"), $certificate['version']);
+ $text .= sprintf(" %s: %d\n", _("Serial Number"), $certificate['serialNumber']);
+
+ foreach ($cert_details['fingerprints'] as $hash => $fingerprint) {
+ $label = sprintf(_("%s Fingerprint"), String::upper($hash));
+ $fingerprint_str = '';
+ for ($i = 0; $i < strlen($fingerprint); $i += 2) {
+ $fingerprint_str .= substr($fingerprint, $i, 2) . ':';
+ }
+ $text .= sprintf(" %s:\n %s\n", $label, $fingerprint_str);
+ }
+ $text .= sprintf(" %s: %s\n", _("Signature Algorithm"), $cert_details['signatureAlgorithm']);
+ $text .= sprintf(" %s:", _("Signature"));
+
+ $sig_str = '';
+ for ($i = 0; $i < strlen($cert_details['signature']); $i++) {
+ if (($i % 16) == 0) {
+ $sig_str .= "\n ";
+ }
+ $sig_str .= sprintf("%02x:", ord($cert_details['signature'][$i]));
+ }
+
+ return $text . $sig_str . "\n</pre>";
+ }
+
+ /**
+ * Extract the contents of a PEM format certificate to an array.
+ *
+ * @param string $cert PEM format certificate
+ *
+ * @return array Array containing all extractable information about
+ * the certificate.
+ */
+ public function parseCert($cert)
+ {
+ $cert_split = preg_split('/(-----((BEGIN)|(END)) CERTIFICATE-----)/', $cert);
+ if (!isset($cert_split[1])) {
+ $raw_cert = base64_decode($cert);
+ } else {
+ $raw_cert = base64_decode($cert_split[1]);
+ }
+
+ $cert_data = $this->_parseASN($raw_cert);
+ if (!is_array($cert_data) || ($cert_data[0] == 'UNKNOWN')) {
+ return false;
+ }
+
+ $cert_details = array();
+ $cert_details['fingerprints']['md5'] = hash('md5', $raw_cert);
+ $cert_details['fingerprints']['sha1'] = hash('sha1', $raw_cert);
+
+ $cert_details['certificate']['extensions'] = array();
+ $cert_details['certificate']['version'] = $cert_data[1][0][1][0][1] + 1;
+ $cert_details['certificate']['serialNumber'] = $cert_data[1][0][1][1][1];
+ $cert_details['certificate']['signature'] = $cert_data[1][0][1][2][1][0][1];
+ $cert_details['certificate']['issuer'] = $cert_data[1][0][1][3][1];
+ $cert_details['certificate']['validity'] = $cert_data[1][0][1][4][1];
+ $cert_details['certificate']['subject'] = @$cert_data[1][0][1][5][1];
+ $cert_details['certificate']['subjectPublicKeyInfo'] = $cert_data[1][0][1][6][1];
+
+ $cert_details['signatureAlgorithm'] = $cert_data[1][1][1][0][1];
+ $cert_details['signature'] = $cert_data[1][2][1];
+
+ // issuer
+ $issuer = array();
+ foreach ($cert_details['certificate']['issuer'] as $value) {
+ $issuer[$value[1][1][0][1]] = $value[1][1][1][1];
+ }
+ $cert_details['certificate']['issuer'] = $issuer;
+
+ // subject
+ $subject = array();
+ foreach ($cert_details['certificate']['subject'] as $value) {
+ $subject[$value[1][1][0][1]] = $value[1][1][1][1];
+ }
+ $cert_details['certificate']['subject'] = $subject;
+
+ // validity
+ $vals = $cert_details['certificate']['validity'];
+ $cert_details['certificate']['validity'] = array();
+ $cert_details['certificate']['validity']['notbefore'] = $vals[0][1];
+ $cert_details['certificate']['validity']['notafter'] = $vals[1][1];
+ foreach ($cert_details['certificate']['validity'] as $key => $val) {
+ $year = substr($val, 0, 2);
+ $month = substr($val, 2, 2);
+ $day = substr($val, 4, 2);
+ $hour = substr($val, 6, 2);
+ $minute = substr($val, 8, 2);
+ if (($val[11] == '-') || ($val[9] == '+')) {
+ // handle time zone offset here
+ $seconds = 0;
+ } elseif (String::upper($val[11]) == 'Z') {
+ $seconds = 0;
+ } else {
+ $seconds = substr($val, 10, 2);
+ if (($val[11] == '-') || ($val[9] == '+')) {
+ // handle time zone offset here
+ }
+ }
+ $cert_details['certificate']['validity'][$key] = mktime ($hour, $minute, $seconds, $month, $day, $year);
+ }
+
+ // Split the Public Key into components.
+ $subjectPublicKeyInfo = array();
+ $subjectPublicKeyInfo['algorithm'] = $cert_details['certificate']['subjectPublicKeyInfo'][0][1][0][1];
+ if ($subjectPublicKeyInfo['algorithm'] == 'rsaEncryption') {
+ $subjectPublicKey = $this->_parseASN($cert_details['certificate']['subjectPublicKeyInfo'][1][1]);
+ $subjectPublicKeyInfo['subjectPublicKey']['modulus'] = $subjectPublicKey[1][0][1];
+ $subjectPublicKeyInfo['subjectPublicKey']['publicExponent'] = $subjectPublicKey[1][1][1];
+ }
+ $cert_details['certificate']['subjectPublicKeyInfo'] = $subjectPublicKeyInfo;
+
+ if (isset($cert_data[1][0][1][7]) &&
+ is_array($cert_data[1][0][1][7][1])) {
+ foreach ($cert_data[1][0][1][7][1] as $ext) {
+ $oid = $ext[1][0][1];
+ $cert_details['certificate']['extensions'][$oid] = $ext[1][1];
+ }
+ }
+
+ $i = 9;
+
+ while (isset($cert_data[1][0][1][$i]) &&
+ is_array($cert_data[1][0][1][$i][1])) {
+ $oid = $cert_data[1][0][1][$i][1][0][1];
+ $cert_details['certificate']['extensions'][$oid] = $cert_data[1][0][1][$i][1][1];
+ ++$i;
+ }
+
+ foreach ($cert_details['certificate']['extensions'] as $oid => $val) {
+ switch ($oid) {
+ case 'netscape-base-url':
+ case 'netscape-revocation-url':
+ case 'netscape-ca-revocation-url':
+ case 'netscape-cert-renewal-url':
+ case 'netscape-ca-policy-url':
+ case 'netscape-ssl-server-name':
+ case 'netscape-comment':
+ $val = $this->_parseASN($val[1]);
+ $cert_details['certificate']['extensions'][$oid] = $val[1];
+ break;
+
+ case 'id-ce-subjectAltName':
+ $val = $this->_parseASN($val[1]);
+ $cert_details['certificate']['extensions'][$oid] = '';
+ foreach ($val[1] as $name) {
+ if (!empty($cert_details['certificate']['extensions'][$oid])) {
+ $cert_details['certificate']['extensions'][$oid] .= ', ';
+ }
+ $cert_details['certificate']['extensions'][$oid] .= $name[1];
+ }
+ break;
+
+ case 'netscape-cert-type':
+ $val = $this->_parseASN($val[1]);
+ $val = ord($val[1]);
+ $newVal = '';
+
+ if ($val & 0x80) {
+ $newVal .= empty($newVal) ? 'SSL client' : ', SSL client';
+ }
+ if ($val & 0x40) {
+ $newVal .= empty($newVal) ? 'SSL server' : ', SSL server';
+ }
+ if ($val & 0x20) {
+ $newVal .= empty($newVal) ? 'S/MIME' : ', S/MIME';
+ }
+ if ($val & 0x10) {
+ $newVal .= empty($newVal) ? 'Object Signing' : ', Object Signing';
+ }
+ if ($val & 0x04) {
+ $newVal .= empty($newVal) ? 'SSL CA' : ', SSL CA';
+ }
+ if ($val & 0x02) {
+ $newVal .= empty($newVal) ? 'S/MIME CA' : ', S/MIME CA';
+ }
+ if ($val & 0x01) {
+ $newVal .= empty($newVal) ? 'Object Signing CA' : ', Object Signing CA';
+ }
+
+ $cert_details['certificate']['extensions'][$oid] = $newVal;
+ break;
+
+ case 'id-ce-extKeyUsage':
+ $val = $this->_parseASN($val[1]);
+ $val = $val[1];
+
+ $newVal = '';
+ if ($val[0][1] != 'sequence') {
+ $val = array($val);
+ } else {
+ $val = $val[1][1];
+ }
+ foreach ($val as $usage) {
+ if ($usage[1] == 'id_kp_clientAuth') {
+ $newVal .= empty($newVal) ? 'TLS Web Client Authentication' : ', TLS Web Client Authentication';
+ } else {
+ $newVal .= empty($newVal) ? $usage[1] : ', ' . $usage[1];
+ }
+ }
+ $cert_details['certificate']['extensions'][$oid] = $newVal;
+ break;
+
+ case 'id-ce-subjectKeyIdentifier':
+ $val = $this->_parseASN($val[1]);
+ $val = $val[1];
+
+ $newVal = '';
+
+ for ($i = 0; $i < strlen($val); $i++) {
+ $newVal .= sprintf("%02x:", ord($val[$i]));
+ }
+ $cert_details['certificate']['extensions'][$oid] = $newVal;
+ break;
+
+ case 'id-ce-authorityKeyIdentifier':
+ $val = $this->_parseASN($val[1]);
+ if ($val[0] == 'string') {
+ $val = $val[1];
+
+ $newVal = '';
+ for ($i = 0; $i < strlen($val); $i++) {
+ $newVal .= sprintf("%02x:", ord($val[$i]));
+ }
+ $cert_details['certificate']['extensions'][$oid] = $newVal;
+ } else {
+ $cert_details['certificate']['extensions'][$oid] = _("Unsupported Extension");
+ }
+ break;
+
+ case 'id-ce-basicConstraints':
+ case 'default':
+ $cert_details['certificate']['extensions'][$oid] = _("Unsupported Extension");
+ break;
+ }
+ }
+
+ return $cert_details;
+ }
+
+ /**
+ * Attempt to parse ASN.1 formated data.
+ *
+ * @param string $data ASN.1 formated data
+ *
+ * @return array Array contained the extracted values.
+ */
+ protected function _parseASN($data)
+ {
+ $result = array();
+
+ while (strlen($data) > 1) {
+ $class = ord($data[0]);
+ switch ($class) {
+ case 0x30:
+ // Sequence
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+ $sequence_data = substr($data, 2 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+
+ $values = $this->_parseASN($sequence_data);
+ if (!is_array($values) || is_string($values[0])) {
+ $values = array($values);
+ }
+ $sequence_values = array();
+ $i = 0;
+ foreach ($values as $val) {
+ if ($val[0] == 'extension') {
+ $sequence_values['extensions'][] = $val;
+ } else {
+ $sequence_values[$i++] = $val;
+ }
+ }
+ $result[] = array('sequence', $sequence_values);
+ break;
+
+ case 0x31:
+ // Set of
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+ $sequence_data = substr($data, 2 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+ $result[] = array('set', $this->_parseASN($sequence_data));
+ break;
+
+ case 0x01:
+ // Boolean type
+ $boolean_value = (ord($data[2]) == 0xff);
+ $data = substr($data, 3);
+ $result[] = array('boolean', $boolean_value);
+ break;
+
+ case 0x02:
+ // Integer type
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+
+ $integer_data = substr($data, 2 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+
+ $value = 0;
+ if ($len <= 4) {
+ /* Method works fine for small integers */
+ for ($i = 0; $i < strlen($integer_data); $i++) {
+ $value = ($value << 8) | ord($integer_data[$i]);
+ }
+ } else {
+ /* Method works for arbitrary length integers */
+ if (Util::extensionExists('bcmath')) {
+ for ($i = 0; $i < strlen($integer_data); $i++) {
+ $value = bcadd(bcmul($value, 256), ord($integer_data[$i]));
+ }
+ } else {
+ $value = -1;
+ }
+ }
+ $result[] = array('integer(' . $len . ')', $value);
+ break;
+
+ case 0x03:
+ // Bitstring type
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+ $bitstring_data = substr($data, 3 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+ $result[] = array('bit string', $bitstring_data);
+ break;
+
+ case 0x04:
+ // Octetstring type
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+ $octectstring_data = substr($data, 2 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+ $result[] = array('octet string', $octectstring_data);
+ break;
+
+ case 0x05:
+ // Null type
+ $data = substr($data, 2);
+ $result[] = array('null', null);
+ break;
+
+ case 0x06:
+ // Object identifier type
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+ $oid_data = substr($data, 2 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+
+ // Unpack the OID
+ $plain = floor(ord($oid_data[0]) / 40);
+ $plain .= '.' . ord($oid_data[0]) % 40;
+
+ $value = 0;
+ $i = 1;
+ while ($i < strlen($oid_data)) {
+ $value = $value << 7;
+ $value = $value | (ord($oid_data[$i]) & 0x7f);
+
+ if (!(ord($oid_data[$i]) & 0x80)) {
+ $plain .= '.' . $value;
+ $value = 0;
+ }
+ $i++;
+ }
+
+ if (isset($this->_oids[$plain])) {
+ $result[] = array('oid', $this->_oids[$plain]);
+ } else {
+ $result[] = array('oid', $plain);
+ }
+
+ break;
+
+ case 0x12:
+ case 0x13:
+ case 0x14:
+ case 0x15:
+ case 0x16:
+ case 0x81:
+ case 0x80:
+ // Character string type
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+ $string_data = substr($data, 2 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+ $result[] = array('string', $string_data);
+ break;
+
+ case 0x17:
+ // Time types
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+ $time_data = substr($data, 2 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+ $result[] = array('utctime', $time_data);
+ break;
+
+ case 0x82:
+ // X509v3 extensions?
+ $len = ord($data[1]);
+ $bytes = 0;
+ if ($len & 0x80) {
+ $bytes = $len & 0x0f;
+ $len = 0;
+ for ($i = 0; $i < $bytes; $i++) {
+ $len = ($len << 8) | ord($data[$i + 2]);
+ }
+ }
+ $sequence_data = substr($data, 2 + $bytes, $len);
+ $data = substr($data, 2 + $bytes + $len);
+ $result[] = array('extension', 'X509v3 extensions');
+ $result[] = $this->_parseASN($sequence_data);
+ break;
+
+ case 0xa0:
+ case 0xa3:
+ // Extensions
+ $extension_data = substr($data, 0, 2);
+ $data = substr($data, 2);
+ $result[] = array('extension', dechex($extension_data));
+ break;
+
+ case 0xe6:
+ $extension_data = substr($data, 0, 1);
+ $data = substr($data, 1);
+ $result[] = array('extension', dechex($extension_data));
+ break;
+
+ case 0xa1:
+ $extension_data = substr($data, 0, 1);
+ $data = substr($data, 6);
+ $result[] = array('extension', dechex($extension_data));
+ break;
+
+ default:
+ // Unknown
+ $result[] = array('UNKNOWN', dechex($data));
+ $data = '';
+ break;
+ }
+ }
+
+ return (count($result) > 1) ? $result : array_pop($result);
+ }
+
+ /**
+ * Decrypt an S/MIME signed message using a public key.
+ *
+ * @param string $text The text to be verified.
+ * @param array $params The parameters needed for verification.
+ *
+ * @return string The verification message.
+ * @throws Horde_Exception
+ */
+ protected function _decryptSignature($text, $params)
+ {
+ throw new Horde_Exception('_decryptSignature() ' . _("not yet implemented"));
+ }
+
+ /**
+ * Check for the presence of the OpenSSL extension to PHP.
+ *
+ * @throws Horde_Exception
+ */
+ public function checkForOpenSSL()
+ {
+ if (!Util::extensionExists('openssl')) {
+ throw new Horde_Exception(_("The openssl module is required for the Horde_Crypt_Smime:: class."));
+ }
+ }
+
+ /**
+ * Extract the email address from a public key.
+ *
+ * @param string $key The public key.
+ *
+ * @return mixed Returns the first email address found, or null if
+ * there are none.
+ */
+ public function getEmailFromKey($key)
+ {
+ $key_info = openssl_x509_parse($key);
+ if (!is_array($key_info)) {
+ return null;
+ }
+
+ if (isset($key_info['subject'])) {
+ if (isset($key_info['subject']['Email'])) {
+ return $key_info['subject']['Email'];
+ } elseif (isset($key_info['subject']['emailAddress'])) {
+ return $key_info['subject']['emailAddress'];
+ }
+ }
+
+ // Check subjectAltName per http://www.ietf.org/rfc/rfc3850.txt
+ if (isset($key_info['extensions']['subjectAltName'])) {
+ $names = preg_split('/\s*,\s*/', $key_info['extensions']['subjectAltName'], -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($names as $name) {
+ if (strpos($name, ':') === false) {
+ continue;
+ }
+ list($kind, $value) = explode(':', $name, 2);
+ if (String::lower($kind) == 'email') {
+ return $value;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Convert a PKCS 12 encrypted certificate package into a private key,
+ * public key, and any additional keys.
+ *
+ * @param string $text The PKCS 12 data.
+ * @param array $params The parameters needed for parsing.
+ * <pre>
+ * Parameters:
+ * ===========
+ * 'sslpath' => The path to the OpenSSL binary. (REQUIRED)
+ * 'password' => The password to use to decrypt the data. (Optional)
+ * 'newpassword' => The password to use to encrypt the private key.
+ * (Optional)
+ * </pre>
+ *
+ * @return stdClass An object.
+ * 'private' - The private key in PEM format.
+ * 'public' - The public key in PEM format.
+ * 'certs' - An array of additional certs.
+ * @throws Horde_Exception
+ */
+ public function parsePKCS12Data($pkcs12, $params)
+ {
+ /* Check for availability of OpenSSL PHP extension. */
+ $this->checkForOpenSSL();
+
+ if (!isset($params['sslpath'])) {
+ throw new Horde_Exception(_("No path to the OpenSSL binary provided. The OpenSSL binary is necessary to work with PKCS 12 data."), 'horde.error');
+ }
+ $sslpath = escapeshellcmd($params['sslpath']);
+
+ /* Create temp files for input/output. */
+ $input = $this->_createTempFile('horde-smime');
+ $output = $this->_createTempFile('horde-smime');
+
+ $ob = new stdClass;
+
+ /* Write text to file */
+ file_put_contents($input, $pkcs12);
+ unset($pkcs12);
+
+ /* Extract the private key from the file first. */
+ $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nocerts';
+ if (isset($params['password'])) {
+ $cmdline .= ' -passin stdin';
+ if (!empty($params['newpassword'])) {
+ $cmdline .= ' -passout stdin';
+ } else {
+ $cmdline .= ' -nodes';
+ }
+ $fd = popen($cmdline, 'w');
+ fwrite($fd, $params['password'] . "\n");
+ if (!empty($params['newpassword'])) {
+ fwrite($fd, $params['newpassword'] . "\n");
+ }
+ pclose($fd);
+ } else {
+ $cmdline .= ' -nodes';
+ exec($cmdline);
+ }
+ $ob->private = trim(file_get_contents($output));
+ if (empty($ob->private)) {
+ throw new Horde_Exception(_("Password incorrect"), 'horde.error');
+ }
+
+ /* Extract the client public key next. */
+ $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nokeys -clcerts';
+ if (isset($params['password'])) {
+ $cmdline .= ' -passin stdin';
+ $fd = popen($cmdline, 'w');
+ fwrite($fd, $params['password'] . "\n");
+ pclose($fd);
+ } else {
+ exec($cmdline);
+ }
+ $ob->public = trim(file_get_contents($output));
+
+ /* Extract the CA public key next. */
+ $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nokeys -cacerts';
+ if (isset($params['password'])) {
+ $cmdline .= ' -passin stdin';
+ $fd = popen($cmdline, 'w');
+ fwrite($fd, $params['password'] . "\n");
+ pclose($fd);
+ } else {
+ exec($cmdline);
+ }
+ $ob->certs = trim(file_get_contents($output));
+
+ return $ob;
+ }
+
+ /**
+ * The Content-Type parameters PHP's openssl_pkcs7_* functions return are
+ * deprecated. Fix these headers to the correct ones (see RFC 2311).
+ *
+ * @param string $text The PKCS7 data.
+ * @param string $type Is this 'message' or 'signature' data?
+ *
+ * @return string The PKCS7 data with the correct Content-Type parameter.
+ */
+ protected function _fixContentType($text, $type)
+ {
+ if ($type == 'message') {
+ $from = 'application/x-pkcs7-mime';
+ $to = 'application/pkcs7-mime';
+ } else {
+ $from = 'application/x-pkcs7-signature';
+ $to = 'application/pkcs7-signature';
+ }
+ return str_replace('Content-Type: ' . $from, 'Content-Type: ' . $to, $text);
+ }
+
+}
+++ /dev/null
-<?php
-/**
- * Horde_Crypt_pgp:: provides a framework for Horde applications to interact
- * with the GNU Privacy Guard program ("GnuPG"). GnuPG implements the OpenPGP
- * standard (RFC 2440).
- *
- * GnuPG Website: http://www.gnupg.org/
- *
- * This class has been developed with, and is only guaranteed to work with,
- * Version 1.21 or above of GnuPG.
- *
- * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Michael Slusarz <slusarz@horde.org>
- * @package Horde_Crypt
- */
-class Horde_Crypt_pgp extends Horde_Crypt
-{
- /**
- * Armor Header Lines - From RFC 2440:
- *
- * An Armor Header Line consists of the appropriate header line text
- * surrounded by five (5) dashes ('-', 0x2D) on either side of the header
- * line text. The header line text is chosen based upon the type of data
- * that is being encoded in Armor, and how it is being encoded.
- *
- * All Armor Header Lines are prefixed with 'PGP'.
- *
- * The Armor Tail Line is composed in the same manner as the Armor Header
- * Line, except the string "BEGIN" is replaced by the string "END."
- */
-
- /* Used for signed, encrypted, or compressed files. */
- const ARMOR_MESSAGE = 1;
-
- /* Used for signed files. */
- const ARMOR_SIGNED_MESSAGE = 2;
-
- /* Used for armoring public keys. */
- const ARMOR_PUBLIC_KEY = 3;
-
- /* Used for armoring private keys. */
- const ARMOR_PRIVATE_KEY = 4;
-
- /* Used for detached signatures, PGP/MIME signatures, and natures
- * following clearsigned messages. */
- const ARMOR_SIGNATURE = 5;
-
- /* Regular text contained in an PGP message. */
- const ARMOR_TEXT = 6;
-
- /**
- * Strings in armor header lines used to distinguish between the different
- * types of PGP decryption/encryption.
- *
- * @var array
- */
- protected $_armor = array(
- 'MESSAGE' => self::ARMOR_MESSAGE,
- 'SIGNED MESSAGE' => self::ARMOR_SIGNED_MESSAGE,
- 'PUBLIC KEY BLOCK' => self::ARMOR_PUBLIC_KEY,
- 'PRIVATE KEY BLOCK' => self::ARMOR_PRIVATE_KEY,
- 'SIGNATURE' => self::ARMOR_SIGNATURE
- );
-
- /* The default public PGP keyserver to use. */
- const KEYSERVER_PUBLIC = 'pgp.mit.edu';
-
- /* The number of times the keyserver refuses connection before an error is
- * returned. */
- const KEYSERVER_REFUSE = 3;
-
- /* The number of seconds that PHP will attempt to connect to the keyserver
- * before it will stop processing the request. */
- const KEYSERVER_TIMEOUT = 10;
-
- /**
- * The list of PGP hash algorithms (from RFC 3156).
- *
- * @var array
- */
- protected $_hashAlg = array(
- 1 => 'pgp-md5',
- 2 => 'pgp-sha1',
- 3 => 'pgp-ripemd160',
- 5 => 'pgp-md2',
- 6 => 'pgp-tiger192',
- 7 => 'pgp-haval-5-160',
- 8 => 'pgp-sha256',
- 9 => 'pgp-sha384',
- 10 => 'pgp-sha512',
- 11 => 'pgp-sha224',
- );
-
- /**
- * GnuPG program location/common options.
- *
- * @var array
- */
- protected $_gnupg;
-
- /**
- * Filename of the temporary public keyring.
- *
- * @var string
- */
- protected $_publicKeyring;
-
- /**
- * Filename of the temporary private keyring.
- *
- * @var string
- */
- protected $_privateKeyring;
-
- /**
- * Constructor.
- *
- * @param array $params Parameter array containing the path to the GnuPG
- * binary (key = 'program') and to a temporary
- * directory.
- */
- public function __construct($params = array())
- {
- $this->_tempdir = Util::createTempDir(true, $params['temp']);
-
- if (empty($params['program'])) {
- Horde::fatal(PEAR::raiseError('The location of the GnuPG binary must be given to the Horde_Crypt_pgp:: class.'), __FILE__, __LINE__);
- }
-
- /* Store the location of GnuPG and set common options. */
- $this->_gnupg = array(
- $params['program'],
- '--no-tty',
- '--no-secmem-warning',
- '--no-options',
- '--no-default-keyring',
- '--yes',
- '--homedir ' . $this->_tempdir
- );
-
- if (strncasecmp(PHP_OS, 'WIN', 3)) {
- array_unshift($this->_gnupg, 'LANG= ;');
- }
- }
-
- /**
- * Generates a personal Public/Private keypair combination.
- *
- * @param string $realname The name to use for the key.
- * @param string $email The email to use for the key.
- * @param string $passphrase The passphrase to use for the key.
- * @param string $comment The comment to use for the key.
- * @param integer $keylength The keylength to use for the key.
- *
- * @return array An array consisting of the public key and the private
- * key, or PEAR_Error on error.
- * <pre>
- * Return array:
- * Key Value
- * --------------------------
- * 'public' => Public Key
- * 'private' => Private Key
- * </pre>
- */
- public function generateKey($realname, $email, $passphrase, $comment = '',
- $keylength = 1024)
- {
- /* Create temp files to hold the generated keys. */
- $pub_file = $this->_createTempFile('horde-pgp');
- $secret_file = $this->_createTempFile('horde-pgp');
-
- /* Create the config file necessary for GnuPG to run in batch mode. */
- /* TODO: Sanitize input, More user customizable? */
- $input = array();
- $input[] = '%pubring ' . $pub_file;
- $input[] = '%secring ' . $secret_file;
- $input[] = 'Key-Type: DSA';
- $input[] = 'Key-Length: 1024';
- $input[] = 'Subkey-Type: ELG-E';
- $input[] = 'Subkey-Length: ' . $keylength;
- $input[] = 'Name-Real: ' . $realname;
- if (!empty($comment)) {
- $input[] = 'Name-Comment: ' . $comment;
- }
- $input[] = 'Name-Email: ' . $email;
- $input[] = 'Expire-Date: 0';
- $input[] = 'Passphrase: ' . $passphrase;
- $input[] = '%commit';
-
- /* Run through gpg binary. */
- $cmdline = array(
- '--gen-key',
- '--batch',
- '--armor'
- );
- $result = $this->_callGpg($cmdline, 'w', $input, true, true);
-
- /* Get the keys from the temp files. */
- $public_key = file($pub_file);
- $secret_key = file($secret_file);
-
- /* If either key is empty, something went wrong. */
- if (empty($public_key) || empty($secret_key)) {
- $msg = _("Public/Private keypair not generated successfully.");
- if (!empty($result->stderr)) {
- $msg .= ' ' . _("Returned error message:") . ' ' . $result->stderr;
- }
- return PEAR::raiseError($msg, 'horde.error');
- }
-
- return array('public' => $public_key, 'private' => $secret_key);
- }
-
- /**
- * Returns information on a PGP data block.
- *
- * @param string $pgpdata The PGP data block.
- *
- * @return array An array with information on the PGP data block. If an
- * element is not present in the data block, it will
- * likewise not be set in the array.
- * <pre>
- * Array Format:
- * -------------
- * [public_key]/[secret_key] => Array
- * (
- * [created] => Key creation - UNIX timestamp
- * [expires] => Key expiration - UNIX timestamp (0 = never expires)
- * [size] => Size of the key in bits
- * )
- *
- * [keyid] => Key ID of the PGP data (if available)
- * 16-bit hex value (as of Horde 3.2)
- *
- * [signature] => Array (
- * [id{n}/'_SIGNATURE'] => Array (
- * [name] => Full Name
- * [comment] => Comment
- * [email] => E-mail Address
- * [keyid] => 16-bit hex value (as of Horde 3.2)
- * [created] => Signature creation - UNIX timestamp
- * [expires] => Signature expiration - UNIX timestamp
- * [micalg] => The hash used to create the signature
- * [sig_{hex}] => Array [details of a sig verifying the ID] (
- * [created] => Signature creation - UNIX timestamp
- * [expires] => Signature expiration - UNIX timestamp
- * [keyid] => 16-bit hex value (as of Horde 3.2)
- * [micalg] => The hash used to create the signature
- * )
- * )
- * )
- * </pre>
- *
- * Each user ID will be stored in the array 'signature' and have data
- * associated with it, including an array for information on each
- * signature that has signed that UID. Signatures not associated with a
- * UID (e.g. revocation signatures and sub keys) will be stored under the
- * special keyword '_SIGNATURE'.
- */
- public function pgpPacketInformation($pgpdata)
- {
- $data_array = array();
- $keyid = '';
- $header = null;
- $input = $this->_createTempFile('horde-pgp');
- $sig_id = $uid_idx = 0;
-
- /* Store message in temporary file. */
- $fp = fopen($input, 'w+');
- fputs($fp, $pgpdata);
- fclose($fp);
-
- $cmdline = array(
- '--list-packets',
- $input
- );
- $result = $this->_callGpg($cmdline, 'r');
-
- foreach (explode("\n", $result->stdout) as $line) {
- /* Headers are prefaced with a ':' as the first character on the
- line. */
- if (strpos($line, ':') === 0) {
- $lowerLine = String::lower($line);
-
- /* If we have a key (rather than a signature block), get the
- key's ID */
- if (strpos($lowerLine, ':public key packet:') !== false ||
- strpos($lowerLine, ':secret key packet:') !== false) {
- $cmdline = array(
- '--with-colons',
- $input
- );
- $data = $this->_callGpg($cmdline, 'r');
- if (preg_match("/(sec|pub):.*:.*:.*:([A-F0-9]{16}):/", $data->stdout, $matches)) {
- $keyid = $matches[2];
- }
- }
-
- if (strpos($lowerLine, ':public key packet:') !== false) {
- $header = 'public_key';
- } elseif (strpos($lowerLine, ':secret key packet:') !== false) {
- $header = 'secret_key';
- } elseif (strpos($lowerLine, ':user id packet:') !== false) {
- $uid_idx++;
- $line = preg_replace_callback('/\\\\x([0-9a-f]{2})/', array($this, '_pgpPacketInformationHelper'), $line);
- if (preg_match("/\"([^\<]+)\<([^\>]+)\>\"/", $line, $matches)) {
- $header = 'id' . $uid_idx;
- if (preg_match('/([^\(]+)\((.+)\)$/', trim($matches[1]), $comment_matches)) {
- $data_array['signature'][$header]['name'] = trim($comment_matches[1]);
- $data_array['signature'][$header]['comment'] = $comment_matches[2];
- } else {
- $data_array['signature'][$header]['name'] = trim($matches[1]);
- $data_array['signature'][$header]['comment'] = '';
- }
- $data_array['signature'][$header]['email'] = $matches[2];
- $data_array['signature'][$header]['keyid'] = $keyid;
- }
- } elseif (strpos($lowerLine, ':signature packet:') !== false) {
- if (empty($header) || empty($uid_idx)) {
- $header = '_SIGNATURE';
- }
- if (preg_match("/keyid\s+([0-9A-F]+)/i", $line, $matches)) {
- $sig_id = $matches[1];
- $data_array['signature'][$header]['sig_' . $sig_id]['keyid'] = $matches[1];
- $data_array['keyid'] = $matches[1];
- }
- } elseif (strpos($lowerLine, ':literal data packet:') !== false) {
- $header = 'literal';
- } elseif (strpos($lowerLine, ':encrypted data packet:') !== false) {
- $header = 'encrypted';
- } else {
- $header = null;
- }
- } else {
- if ($header == 'secret_key' || $header == 'public_key') {
- if (preg_match("/created\s+(\d+),\s+expires\s+(\d+)/i", $line, $matches)) {
- $data_array[$header]['created'] = $matches[1];
- $data_array[$header]['expires'] = $matches[2];
- } elseif (preg_match("/\s+[sp]key\[0\]:\s+\[(\d+)/i", $line, $matches)) {
- $data_array[$header]['size'] = $matches[1];
- }
- } elseif ($header == 'literal' || $header == 'encrypted') {
- $data_array[$header] = true;
- } elseif ($header) {
- if (preg_match("/version\s+\d+,\s+created\s+(\d+)/i", $line, $matches)) {
- $data_array['signature'][$header]['sig_' . $sig_id]['created'] = $matches[1];
- } elseif (isset($data_array['signature'][$header]['sig_' . $sig_id]['created']) &&
- preg_match('/expires after (\d+y\d+d\d+h\d+m)\)$/', $line, $matches)) {
- $expires = $matches[1];
- preg_match('/^(\d+)y(\d+)d(\d+)h(\d+)m$/', $expires, $matches);
- list(, $years, $days, $hours, $minutes) = $matches;
- $data_array['signature'][$header]['sig_' . $sig_id]['expires'] =
- strtotime('+ ' . $years . ' years + ' . $days . ' days + ' . $hours . ' hours + ' . $minutes . ' minutes', $data_array['signature'][$header]['sig_' . $sig_id]['created']);
- } elseif (preg_match("/digest algo\s+(\d{1})/", $line, $matches)) {
- $micalg = $this->_hashAlg[$matches[1]];
- $data_array['signature'][$header]['sig_' . $sig_id]['micalg'] = $micalg;
- if ($header == '_SIGNATURE') {
- /* Likely a signature block, not a key. */
- $data_array['signature']['_SIGNATURE']['micalg'] = $micalg;
- }
- if ($sig_id == $keyid) {
- /* Self signing signature - we can assume
- * the micalg value from this signature is
- * that for the key */
- $data_array['signature']['_SIGNATURE']['micalg'] = $micalg;
- $data_array['signature'][$header]['micalg'] = $micalg;
- }
- }
- }
- }
- }
-
- $keyid && $data_array['keyid'] = $keyid;
-
- return $data_array;
- }
-
- protected function _pgpPacketInformationHelper($a)
- {
- return chr(hexdec($a[1]));
- }
-
- /**
- * Returns human readable information on a PGP key.
- *
- * @param string $pgpdata The PGP data block.
- *
- * @return string Tabular information on the PGP key.
- */
- public function pgpPrettyKey($pgpdata)
- {
- $msg = '';
- $packet_info = $this->pgpPacketInformation($pgpdata);
- $fingerprints = $this->getFingerprintsFromKey($pgpdata);
-
- if (!empty($packet_info['signature'])) {
- /* Making the property names the same width for all
- * localizations .*/
- $leftrow = array(_("Name"), _("Key Type"), _("Key Creation"),
- _("Expiration Date"), _("Key Length"),
- _("Comment"), _("E-Mail"), _("Hash-Algorithm"),
- _("Key ID"), _("Key Fingerprint"));
- $leftwidth = array_map('strlen', $leftrow);
- $maxwidth = max($leftwidth) + 2;
- array_walk($leftrow, array($this, '_pgpPrettyKeyFormatter'), $maxwidth);
-
- foreach (array_keys($packet_info['signature']) as $uid_idx) {
- if ($uid_idx == '_SIGNATURE') {
- continue;
- }
- $key_info = $this->pgpPacketSignatureByUidIndex($pgpdata, $uid_idx);
-
- if (!empty($key_info['keyid'])) {
- $key_info['keyid'] = $this->_getKeyIDString($key_info['keyid']);
- } else {
- $key_info['keyid'] = null;
- }
-
- $fingerprint = isset($fingerprints[$key_info['keyid']]) ? $fingerprints[$key_info['keyid']] : null;
-
- $msg .= $leftrow[0] . (isset($key_info['name']) ? stripcslashes($key_info['name']) : '') . "\n"
- . $leftrow[1] . (($key_info['key_type'] == 'public_key') ? _("Public Key") : _("Private Key")) . "\n"
- . $leftrow[2] . strftime("%D", $key_info['key_created']) . "\n"
- . $leftrow[3] . (empty($key_info['key_expires']) ? '[' . _("Never") . ']' : strftime("%D", $key_info['key_expires'])) . "\n"
- . $leftrow[4] . $key_info['key_size'] . " Bytes\n"
- . $leftrow[5] . (empty($key_info['comment']) ? '[' . _("None") . ']' : $key_info['comment']) . "\n"
- . $leftrow[6] . (empty($key_info['email']) ? '[' . _("None") . ']' : $key_info['email']) . "\n"
- . $leftrow[7] . (empty($key_info['micalg']) ? '[' . _("Unknown") . ']' : $key_info['micalg']) . "\n"
- . $leftrow[8] . (empty($key_info['keyid']) ? '[' . _("Unknown") . ']' : $key_info['keyid']) . "\n"
- . $leftrow[9] . (empty($fingerprint) ? '[' . _("Unknown") . ']' : $fingerprint) . "\n\n";
- }
- }
-
- return $msg;
- }
-
- protected function _pgpPrettyKeyFormatter(&$s, $k, $m)
- {
- $s .= ':' . str_repeat(' ', $m - String::length($s));
- }
-
- protected function _getKeyIDString($keyid)
- {
- /* Get the 8 character key ID string. */
- if (strpos($keyid, '0x') === 0) {
- $keyid = substr($keyid, 2);
- }
- if (strlen($keyid) > 8) {
- $keyid = substr($keyid, -8);
- }
- return '0x' . $keyid;
- }
-
- /**
- * Returns only information on the first ID that matches the email address
- * input.
- *
- * @param string $pgpdata The PGP data block.
- * @param string $email An e-mail address.
- *
- * @return array An array with information on the PGP data block. If an
- * element is not present in the data block, it will
- * likewise not be set in the array.
- * <pre>
- * Array Fields:
- * -------------
- * key_created => Key creation - UNIX timestamp
- * key_expires => Key expiration - UNIX timestamp (0 = never expires)
- * key_size => Size of the key in bits
- * key_type => The key type (public_key or secret_key)
- * name => Full Name
- * comment => Comment
- * email => E-mail Address
- * keyid => 16-bit hex value
- * created => Signature creation - UNIX timestamp
- * micalg => The hash used to create the signature
- * </pre>
- */
- public function pgpPacketSignature($pgpdata, $email)
- {
- $data = $this->pgpPacketInformation($pgpdata);
- $key_type = null;
- $return_array = array();
-
- /* Check that [signature] key exists. */
- if (!isset($data['signature'])) {
- return $return_array;
- }
-
- /* Store the signature information now. */
- if (($email == '_SIGNATURE') &&
- isset($data['signature']['_SIGNATURE'])) {
- foreach ($data['signature'][$email] as $key => $value) {
- $return_array[$key] = $value;
- }
- } else {
- $uid_idx = 1;
-
- while (isset($data['signature']['id' . $uid_idx])) {
- if ($data['signature']['id' . $uid_idx]['email'] == $email) {
- foreach ($data['signature']['id' . $uid_idx] as $key => $val) {
- $return_array[$key] = $val;
- }
- break;
- }
- $uid_idx++;
- }
- }
-
- return $this->_pgpPacketSignature($data, $return_array);
- }
-
- /**
- * Returns information on a PGP signature embedded in PGP data. Similar
- * to pgpPacketSignature(), but returns information by unique User ID
- * Index (format id{n} where n is an integer of 1 or greater).
- *
- * @param string $pgpdata See pgpPacketSignature().
- * @param string $uid_idx The UID index.
- *
- * @return array See pgpPacketSignature().
- */
- public function pgpPacketSignatureByUidIndex($pgpdata, $uid_idx)
- {
- $data = $this->pgpPacketInformation($pgpdata);
- $key_type = null;
- $return_array = array();
-
- /* Search for the UID index. */
- if (!isset($data['signature']) ||
- !isset($data['signature'][$uid_idx])) {
- return $return_array;
- }
-
- /* Store the signature information now. */
- foreach ($data['signature'][$uid_idx] as $key => $value) {
- $return_array[$key] = $value;
- }
-
- return $this->_pgpPacketSignature($data, $return_array);
- }
-
- /**
- * Adds some data to the pgpPacketSignature*() function array.
- *
- * @param array $data See pgpPacketSignature().
- * @param array $retarray The return array.
- *
- * @return array The return array.
- */
- protected function _pgpPacketSignature($data, $retarray)
- {
- /* If empty, return now. */
- if (empty($retarray)) {
- return $retarray;
- }
-
- $key_type = null;
-
- /* Store any public/private key information. */
- if (isset($data['public_key'])) {
- $key_type = 'public_key';
- } elseif (isset($data['secret_key'])) {
- $key_type = 'secret_key';
- }
-
- if ($key_type) {
- $retarray['key_type'] = $key_type;
- if (isset($data[$key_type]['created'])) {
- $retarray['key_created'] = $data[$key_type]['created'];
- }
- if (isset($data[$key_type]['expires'])) {
- $retarray['key_expires'] = $data[$key_type]['expires'];
- }
- if (isset($data[$key_type]['size'])) {
- $retarray['key_size'] = $data[$key_type]['size'];
- }
- }
-
- return $retarray;
- }
-
- /**
- * Returns the key ID of the key used to sign a block of PGP data.
- *
- * @param string $text The PGP signed text block.
- *
- * @return string The key ID of the key used to sign $text.
- */
- public function getSignersKeyID($text)
- {
- $keyid = null;
-
- $input = $this->_createTempFile('horde-pgp');
-
- $fp = fopen($input, 'w+');
- fputs($fp, $text);
- fclose($fp);
-
- $cmdline = array(
- '--verify',
- $input
- );
- $result = $this->_callGpg($cmdline, 'r', null, true, true);
- if (preg_match('/gpg:\sSignature\smade.*ID\s+([A-F0-9]{8})\s+/', $result->stderr, $matches)) {
- $keyid = $matches[1];
- }
-
- return $keyid;
- }
-
- /**
- * Verify a passphrase for a given public/private keypair.
- *
- * @param string $public_key The user's PGP public key.
- * @param string $private_key The user's PGP private key.
- * @param string $passphrase The user's passphrase.
- *
- * @return boolean Returns true on valid passphrase, false on invalid
- * passphrase, and PEAR_Error on error.
- */
- public function verifyPassphrase($public_key, $private_key, $passphrase)
- {
- /* Encrypt a test message. */
- $result = $this->encrypt('Test', array('type' => 'message', 'pubkey' => $public_key));
- if (is_a($result, 'PEAR_Error')) {
- return false;
- }
-
- /* Try to decrypt the message. */
- $result = $this->decrypt($result, array('type' => 'message', 'pubkey' => $public_key, 'privkey' => $private_key, 'passphrase' => $passphrase));
- if (is_a($result, 'PEAR_Error')) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Parses a message into text and PGP components.
- *
- * @param string $text The text to parse.
- *
- * @return array An array with the parsed text, returned in blocks of
- * text corresponding to their actual order. Keys:
- * <pre>
- * 'type' - (integer) The type of data contained in block.
- * Valid types are defined at the top of this class
- * (the ARMOR_* constants).
- * 'data' - (array) The data for each section. Each line has been stripped
- * of EOL characters.
- * </pre>
- */
- public function parsePGPData($text)
- {
- $data = array();
- $temp = array(
- 'type' => self::ARMOR_TEXT
- );
-
- $buffer = explode("\n", $text);
- while (list(,$val) = each($buffer)) {
- $val = rtrim($val, "\r");
- if (preg_match('/^-----(BEGIN|END) PGP ([^-]+)-----\s*$/', $val, $matches)) {
- if (isset($temp['data'])) {
- $data[] = $temp;
- }
- $temp= array();
-
- if ($matches[1] == 'BEGIN') {
- $temp['type'] = $this->_armor[$matches[2]];
- $temp['data'][] = $val;
- } elseif ($matches[1] == 'END') {
- $temp['type'] = self::ARMOR_TEXT;
- $data[count($data) - 1]['data'][] = $val;
- }
- } else {
- $temp['data'][] = $val;
- }
- }
-
- if (isset($temp['data']) &&
- ((count($temp['data']) > 1) || !empty($temp['data'][0]))) {
- $data[] = $temp;
- }
-
- return $data;
- }
-
- /**
- * Returns a PGP public key from a public keyserver.
- *
- * @param string $keyid The key ID of the PGP key.
- * @param string $server The keyserver to use.
- * @param float $timeout The keyserver timeout.
- * @param string $address The email address of the PGP key.
- *
- * @return string The PGP public key, or PEAR_Error on error.
- */
- public function getPublicKeyserver($keyid,
- $server = self::KEYSERVER_PUBLIC,
- $timeout = self::KEYSERVER_TIMEOUT,
- $address = null)
- {
- if (empty($keyid) && !empty($address)) {
- $keyid = $this->getKeyID($address, $server, $timeout);
- if (is_a($keyid, 'PEAR_Error')) {
- return $keyid;
- }
- }
-
- /* Connect to the public keyserver. */
- $uri = '/pks/lookup?op=get&search=' . $this->_getKeyIDString($keyid);
- $output = $this->_connectKeyserver('GET', $server, $uri, '', $timeout);
- if (is_a($output, 'PEAR_Error')) {
- return $output;
- }
-
- /* Strip HTML Tags from output. */
- if (($start = strstr($output, '-----BEGIN'))) {
- $length = strpos($start, '-----END') + 34;
- return substr($start, 0, $length);
- } else {
- return PEAR::raiseError(_("Could not obtain public key from the keyserver."), 'horde.error');
- }
- }
-
- /**
- * Sends a PGP public key to a public keyserver.
- *
- * @param string $pubkey The PGP public key
- * @param string $server The keyserver to use.
- * @param float $timeout The keyserver timeout.
- *
- * @return PEAR_Error PEAR_Error on error/failure.
- */
- public function putPublicKeyserver($pubkey,
- $server = self::KEYSERVER_PUBLIC,
- $timeout = self::KEYSERVER_TIMEOUT)
- {
- /* Get the key ID of the public key. */
- $info = $this->pgpPacketInformation($pubkey);
-
- /* See if the public key already exists on the keyserver. */
- if (!is_a($this->getPublicKeyserver($info['keyid'], $server, $timeout), 'PEAR_Error')) {
- return PEAR::raiseError(_("Key already exists on the public keyserver."), 'horde.warning');
- }
-
- /* Connect to the public keyserver. _connectKeyserver()
- * returns a PEAR_Error object on error and the output text on
- * success. */
- $pubkey = 'keytext=' . urlencode(rtrim($pubkey));
- $cmd = array(
- 'Host: ' . $server . ':11371',
- 'User-Agent: Horde Application Framework 3.2',
- 'Content-Type: application/x-www-form-urlencoded',
- 'Content-Length: ' . strlen($pubkey),
- 'Connection: close',
- '',
- $pubkey
- );
-
- $result = $this->_connectKeyserver('POST', $server, '/pks/add', implode("\r\n", $cmd), $timeout);
- if (is_a($result, 'PEAR_Error')) {
- return $result;
- }
- }
-
- /**
- * Returns the first matching key ID for an email address from a
- * public keyserver.
- *
- * @param string $address The email address of the PGP key.
- * @param string $server The keyserver to use.
- * @param float $timeout The keyserver timeout.
- *
- * @return string The PGP key ID, or PEAR_Error on error.
- */
- public function getKeyID($address, $server = self::KEYSERVER_PUBLIC,
- $timeout = self::KEYSERVER_TIMEOUT)
- {
- /* Connect to the public keyserver. */
- $uri = '/pks/lookup?op=index&options=mr&search=' . urlencode($address);
- $output = $this->_connectKeyserver('GET', $server, $uri, '', $timeout);
- if (is_a($output, 'PEAR_Error')) {
- return $output;
- }
-
- if (($start = strstr($output, '-----BEGIN PGP PUBLIC KEY BLOCK'))) {
- /* The server returned the matching key immediately. */
- $length = strpos($start, '-----END PGP PUBLIC KEY BLOCK') + 34;
- $info = $this->pgpPacketInformation(substr($start, 0, $length));
- if (!empty($info['keyid']) &&
- (empty($info['public_key']['expires']) ||
- $info['public_key']['expires'] > time())) {
- return $info['keyid'];
- }
- } elseif (strpos($output, 'pub:') !== false) {
- $output = explode("\n", $output);
- $keyids = array();
- foreach ($output as $line) {
- if (substr($line, 0, 4) == 'pub:') {
- $line = explode(':', $line);
- /* Ignore invalid lines and expired keys. */
- if (count($line) != 7 ||
- (!empty($line[5]) && $line[5] <= time())) {
- continue;
- }
- $keyids[$line[4]] = $line[1];
- }
- }
- /* Sort by timestamp to use the newest key. */
- if (count($keyids)) {
- ksort($keyids);
- return array_pop($keyids);
- }
- }
-
- return PEAR::raiseError(_("Could not obtain public key from the keyserver."));
- }
-
- /**
- * Get the fingerprints from a key block.
- *
- * @param string $pgpdata The PGP data block.
- *
- * @return array The fingerprints in $pgpdata indexed by key id.
- */
- public function getFingerprintsFromKey($pgpdata)
- {
- $fingerprints = array();
-
- /* Store the key in a temporary keyring. */
- $keyring = $this->_putInKeyring($pgpdata);
-
- /* Options for the GPG binary. */
- $cmdline = array(
- '--fingerprint',
- $keyring,
- );
-
- $result = $this->_callGpg($cmdline, 'r');
- if (!$result || !$result->stdout) {
- return $fingerprints;
- }
-
- /* Parse fingerprints and key ids from output. */
- $lines = explode("\n", $result->stdout);
- $keyid = null;
- foreach ($lines as $line) {
- if (preg_match('/pub\s+\w+\/(\w{8})/', $line, $matches)) {
- $keyid = '0x' . $matches[1];
- } elseif ($keyid && preg_match('/^\s+[\s\w]+=\s*([\w\s]+)$/m', $line, $matches)) {
- $fingerprints[$keyid] = trim($matches[1]);
- $keyid = null;
- }
- }
-
- return $fingerprints;
- }
-
- /**
- * Connects to a public key server via HKP (Horrowitz Keyserver Protocol).
- *
- * @param string $method POST, GET, etc.
- * @param string $server The keyserver to use.
- * @param string $uri The URI to access (relative to the server).
- * @param string $command The PGP command to run.
- * @param float $timeout The timeout value.
- *
- * @return string The text from standard output on success, or PEAR_Error
- * on error/failure.
- */
- protected function _connectKeyserver($method, $server, $resource,
- $command, $timeout)
- {
- $connRefuse = 0;
- $output = '';
-
- $port = '11371';
- if (!empty($GLOBALS['conf']['http']['proxy']['proxy_host'])) {
- $resource = 'http://' . $server . ':' . $port . $resource;
-
- $server = $GLOBALS['conf']['http']['proxy']['proxy_host'];
- if (!empty($GLOBALS['conf']['http']['proxy']['proxy_port'])) {
- $port = $GLOBALS['conf']['http']['proxy']['proxy_port'];
- } else {
- $port = 80;
- }
- }
-
- $command = $method . ' ' . $resource . ' HTTP/1.0' . ($command ? "\r\n" . $command : '');
-
- /* Attempt to get the key from the keyserver. */
- do {
- $connError = false;
- $errno = $errstr = null;
-
- /* The HKP server is located on port 11371. */
- $fp = @fsockopen($server, $port, $errno, $errstr, $timeout);
- if (!$fp) {
- $connError = true;
- } else {
- fputs($fp, $command . "\n\n");
- while (!feof($fp)) {
- $output .= fgets($fp, 1024);
- }
- fclose($fp);
- }
-
- if ($connError) {
- if (++$connRefuse === self::KEYSERVER_REFUSE) {
- if ($errno == 0) {
- $output = PEAR::raiseError(_("Connection refused to the public keyserver."), 'horde.error');
- } else {
- $output = PEAR::raiseError(sprintf(_("Connection refused to the public keyserver. Reason: %s (%s)"), String::convertCharset($errstr, NLS::getExternalCharset()), $errno), 'horde.error');
- }
- break;
- }
- }
- } while ($connError);
-
- return $output;
- }
-
- /**
- * Encrypts text using PGP.
- *
- * @param string $text The text to be PGP encrypted.
- * @param array $params The parameters needed for encryption.
- * See the individual _encrypt*() functions for the
- * parameter requirements.
- *
- * @return string The encrypted message, or PEAR_Error on error.
- */
- public function encrypt($text, $params = array())
- {
- if (isset($params['type'])) {
- if ($params['type'] === 'message') {
- return $this->_encryptMessage($text, $params);
- } elseif ($params['type'] === 'signature') {
- return $this->_encryptSignature($text, $params);
- }
- }
- }
-
- /**
- * Decrypts text using PGP.
- *
- * @param string $text The text to be PGP decrypted.
- * @param array $params The parameters needed for decryption.
- * See the individual _decrypt*() functions for the
- * parameter requirements.
- *
- * @return string The decrypted message, or PEAR_Error on error.
- */
- public function decrypt($text, $params = array())
- {
- if (isset($params['type'])) {
- if ($params['type'] === 'message') {
- return $this->_decryptMessage($text, $params);
- } elseif (($params['type'] === 'signature') ||
- ($params['type'] === 'detached-signature')) {
- return $this->_decryptSignature($text, $params);
- }
- }
- }
-
- /**
- * Returns whether a text has been encrypted symmetrically.
- *
- * @param string $text The PGP encrypted text.
- *
- * @return boolean True if the text is symmetricallly encrypted.
- */
- public function encryptedSymmetrically($text)
- {
- $cmdline = array(
- '--decrypt',
- '--batch'
- );
- $result = $this->_callGpg($cmdline, 'w', $text, true, true, true);
- return strpos($result->stderr, 'gpg: encrypted with 1 passphrase') !== false;
- }
-
- /**
- * Creates a temporary gpg keyring.
- *
- * @param string $type The type of key to analyze. Either 'public'
- * (Default) or 'private'
- *
- * @return string Command line keystring option to use with gpg program.
- */
- protected function _createKeyring($type = 'public')
- {
- $type = String::lower($type);
-
- if ($type === 'public') {
- if (empty($this->_publicKeyring)) {
- $this->_publicKeyring = $this->_createTempFile('horde-pgp');
- }
- return '--keyring ' . $this->_publicKeyring;
- } elseif ($type === 'private') {
- if (empty($this->_privateKeyring)) {
- $this->_privateKeyring = $this->_createTempFile('horde-pgp');
- }
- return '--secret-keyring ' . $this->_privateKeyring;
- }
- }
-
- /**
- * Adds PGP keys to the keyring.
- *
- * @param mixed $keys A single key or an array of key(s) to add to the
- * keyring.
- * @param string $type The type of key(s) to add. Either 'public'
- * (Default) or 'private'
- *
- * @return string Command line keystring option to use with gpg program.
- */
- protected function _putInKeyring($keys = array(), $type = 'public')
- {
- $type = String::lower($type);
-
- if (!is_array($keys)) {
- $keys = array($keys);
- }
-
- /* Create the keyrings if they don't already exist. */
- $keyring = $this->_createKeyring($type);
-
- /* Store the key(s) in the keyring. */
- $cmdline = array(
- '--allow-secret-key-import',
- '--fast-import',
- $keyring
- );
- $this->_callGpg($cmdline, 'w', array_values($keys));
-
- return $keyring;
- }
-
- /**
- * Encrypts a message in PGP format using a public key.
- *
- * @param string $text The text to be encrypted.
- * @param array $params The parameters needed for encryption.
- * <pre>
- * Parameters:
- * ===========
- * 'type' => 'message' (REQUIRED)
- * 'symmetric' => Whether to use symmetric instead of asymmetric
- * encryption (defaults to false)
- * 'recips' => An array with the e-mail address of the recipient as
- * the key and that person's public key as the value.
- * (REQUIRED if 'symmetric' is false)
- * 'passphrase' => The passphrase for the symmetric encryption (REQUIRED if
- * 'symmetric' is true)
- * </pre>
- *
- * @return string The encrypted message, or PEAR_Error on error.
- */
- protected function _encryptMessage($text, $params)
- {
- /* Create temp files for input. */
- $input = $this->_createTempFile('horde-pgp');
- $fp = fopen($input, 'w+');
- fputs($fp, $text);
- fclose($fp);
-
- /* Build command line. */
- $cmdline = array(
- '--armor',
- '--batch',
- '--always-trust'
- );
- if (empty($params['symmetric'])) {
- /* Store public key in temporary keyring. */
- $keyring = $this->_putInKeyring(array_values($params['recips']));
-
- $cmdline[] = $keyring;
- $cmdline[] = '--encrypt';
- foreach (array_keys($params['recips']) as $val) {
- $cmdline[] = '--recipient ' . $val;
- }
- } else {
- $cmdline[] = '--symmetric';
- $cmdline[] = '--passphrase-fd 0';
- }
- $cmdline[] = $input;
-
- /* Encrypt the document. */
- $result = $this->_callGpg($cmdline, 'w', empty($params['symmetric']) ? null : $params['passphrase'], true, true);
- if (empty($result->output)) {
- $error = preg_replace('/\n.*/', '', $result->stderr);
- return PEAR::raiseError(_("Could not PGP encrypt message: ") . $error, 'horde.error');
- }
-
- return $result->output;
- }
-
- /**
- * Signs a message in PGP format using a private key.
- *
- * @param string $text The text to be signed.
- * @param array $params The parameters needed for signing.
- * <pre>
- * Parameters:
- * ===========
- * 'type' => 'signature' (REQUIRED)
- * 'pubkey' => PGP public key. (REQUIRED)
- * 'privkey' => PGP private key. (REQUIRED)
- * 'passphrase' => Passphrase for PGP Key. (REQUIRED)
- * 'sigtype' => Determine the signature type to use. (Optional)
- * 'cleartext' -- Make a clear text signature
- * 'detach' -- Make a detached signature (DEFAULT)
- * </pre>
- *
- * @return string The signed message, or PEAR_Error on error.
- */
- protected function _encryptSignature($text, $params)
- {
- /* Check for required parameters. */
- if (!isset($params['pubkey']) ||
- !isset($params['privkey']) ||
- !isset($params['passphrase'])) {
- return PEAR::raiseError(_("A public PGP key, private PGP key, and passphrase are required to sign a message."), 'horde.error');
- }
-
- /* Create temp files for input. */
- $input = $this->_createTempFile('horde-pgp');
-
- /* Encryption requires both keyrings. */
- $pub_keyring = $this->_putInKeyring(array($params['pubkey']));
- $sec_keyring = $this->_putInKeyring(array($params['privkey']), 'private');
-
- /* Store message in temporary file. */
- $fp = fopen($input, 'w+');
- fputs($fp, $text);
- fclose($fp);
-
- /* Determine the signature type to use. */
- $cmdline = array();
- if (isset($params['sigtype']) &&
- $params['sigtype'] == 'cleartext') {
- $sign_type = '--clearsign';
- } else {
- $sign_type = '--detach-sign';
- }
-
- /* Additional GPG options. */
- $cmdline += array(
- '--armor',
- '--batch',
- '--passphrase-fd 0',
- $sec_keyring,
- $pub_keyring,
- $sign_type,
- $input
- );
-
- /* Sign the document. */
- $result = $this->_callGpg($cmdline, 'w', $params['passphrase'], true, true);
- if (empty($result->output)) {
- $error = preg_replace('/\n.*/', '', $result->stderr);
- return PEAR::raiseError(_("Could not PGP sign message: ") . $error, 'horde.error');
- } else {
- return $result->output;
- }
- }
-
- /**
- * Decrypts an PGP encrypted message using a private/public keypair and a
- * passhprase.
- *
- * @param string $text The text to be decrypted.
- * @param array $params The parameters needed for decryption.
- * <pre>
- * Parameters:
- * ===========
- * 'type' => 'message' (REQUIRED)
- * 'pubkey' => PGP public key. (REQUIRED for asymmetric encryption)
- * 'privkey' => PGP private key. (REQUIRED for asymmetric encryption)
- * 'passphrase' => Passphrase for PGP Key. (REQUIRED)
- * </pre>
- *
- * @return stdClass An object with the following properties, or PEAR_Error
- * on error:
- * <pre>
- * 'message' - The decrypted message.
- * 'sig_result' - The result of the signature test.
- * </pre>
- */
- protected function _decryptMessage($text, $params)
- {
- $good_sig_flag = false;
-
- /* Check for required parameters. */
- if (!isset($params['passphrase']) && empty($params['no_passphrase'])) {
- return PEAR::raiseError(_("A passphrase is required to decrypt a message."), 'horde.error');
- }
-
- /* Create temp files. */
- $input = $this->_createTempFile('horde-pgp');
-
- /* Store message in file. */
- $fp = fopen($input, 'w+');
- fputs($fp, $text);
- fclose($fp);
-
- /* Build command line. */
- $cmdline = array(
- '--always-trust',
- '--armor',
- '--batch'
- );
- if (empty($param['no_passphrase'])) {
- $cmdline[] = '--passphrase-fd 0';
- }
- if (!empty($params['pubkey']) && !empty($params['privkey'])) {
- /* Decryption requires both keyrings. */
- $pub_keyring = $this->_putInKeyring(array($params['pubkey']));
- $sec_keyring = $this->_putInKeyring(array($params['privkey']), 'private');
- $cmdline[] = $sec_keyring;
- $cmdline[] = $pub_keyring;
- }
- $cmdline[] = '--decrypt';
- $cmdline[] = $input;
-
- /* Decrypt the document now. */
- if (empty($params['no_passphrase'])) {
- $result = $this->_callGpg($cmdline, 'w', $params['passphrase'], true, true);
- } else {
- $result = $this->_callGpg($cmdline, 'r', null, true, true);
- }
- if (empty($result->output)) {
- $error = preg_replace('/\n.*/', '', $result->stderr);
- return PEAR::raiseError(_("Could not decrypt PGP data: ") . $error, 'horde.error');
- }
-
- /* Create the return object. */
- $ob = new stdClass;
- $ob->message = $result->output;
-
- /* Check the PGP signature. */
- $sig_check = $this->_checkSignatureResult($result->stderr);
- if (is_a($sig_check, 'PEAR_Error')) {
- $ob->sig_result = $sig_check;
- } else {
- $ob->sig_result = ($sig_check) ? $result->stderr : '';
- }
-
- return $ob;
- }
-
- /**
- * Decrypts an PGP signed message using a public key.
- *
- * @param string $text The text to be verified.
- * @param array $params The parameters needed for verification.
- * <pre>
- * Parameters:
- * ===========
- * 'type' => 'signature' or 'detached-signature' (REQUIRED)
- * 'pubkey' => PGP public key. (REQUIRED)
- * 'signature' => PGP signature block. (REQUIRED for detached signature)
- * </pre>
- *
- * @return string The verification message from gpg. If no signature,
- * returns empty string, and PEAR_Error on error.
- */
- protected function _decryptSignature($text, $params)
- {
- /* Check for required parameters. */
- if (!isset($params['pubkey'])) {
- return PEAR::raiseError(_("A public PGP key is required to verify a signed message."), 'horde.error');
- }
- if (($params['type'] === 'detached-signature') &&
- !isset($params['signature'])) {
- return PEAR::raiseError(_("The detached PGP signature block is required to verify the signed message."), 'horde.error');
- }
-
- $good_sig_flag = 0;
-
- /* Create temp files for input. */
- $input = $this->_createTempFile('horde-pgp');
-
- /* Store public key in temporary keyring. */
- $keyring = $this->_putInKeyring($params['pubkey']);
-
- /* Store the message in a temporary file. */
- $fp = fopen($input, 'w+');
- fputs($fp, $text);
- fclose($fp);
-
- /* Options for the GPG binary. */
- $cmdline = array(
- '--armor',
- '--always-trust',
- '--batch',
- '--charset ' . NLS::getCharset(),
- $keyring,
- '--verify'
- );
-
- /* Extra stuff to do if we are using a detached signature. */
- if ($params['type'] === 'detached-signature') {
- $sigfile = $this->_createTempFile('horde-pgp');
- $cmdline[] = $sigfile . ' ' . $input;
-
- $fp = fopen($sigfile, 'w+');
- fputs($fp, $params['signature']);
- fclose($fp);
- } else {
- $cmdline[] = $input;
- }
-
- /* Verify the signature. We need to catch standard error output,
- * since this is where the signature information is sent. */
- $result = $this->_callGpg($cmdline, 'r', null, true, true);
- $sig_result = $this->_checkSignatureResult($result->stderr);
- if (is_a($sig_result, 'PEAR_Error')) {
- return $sig_result;
- } else {
- return ($sig_result) ? $result->stderr : '';
- }
- }
-
- /**
- * Checks signature result from the GnuPG binary.
- *
- * @param string $result The signature result.
- *
- * @return boolean True if signature is good.
- */
- protected function _checkSignatureResult($result)
- {
- /* Good signature:
- * gpg: Good signature from "blah blah blah (Comment)"
- * Bad signature:
- * gpg: BAD signature from "blah blah blah (Comment)" */
- if (strpos($result, 'gpg: BAD signature') !== false) {
- return PEAR::raiseError($result, 'horde.error');
- } elseif (strpos($result, 'gpg: Good signature') !== false) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Signs a MIME part using PGP.
- *
- * @param Horde_Mime_Part $mime_part The object to sign.
- * @param array $params The parameters required for signing.
- * @see _encryptSignature().
- *
- * @return mixed A Horde_Mime_Part object that is signed according to RFC
- * 3156, or PEAR_Error on error.
- */
- public function signMIMEPart($mime_part, $params = array())
- {
- $params = array_merge($params, array('type' => 'signature', 'sigtype' => 'detach'));
-
- /* RFC 3156 Requirements for a PGP signed message:
- * + Content-Type params 'micalg' & 'protocol' are REQUIRED.
- * + The digitally signed message MUST be constrained to 7 bits.
- * + The MIME headers MUST be a part of the signed data. */
-
- $mime_part->strict7bit(true);
- $msg_sign = $this->encrypt($mime_part->toCanonicalString(), $params);
- if (is_a($msg_sign, 'PEAR_Error')) {
- return $msg_sign;
- }
-
- /* Add the PGP signature. */
- $charset = NLS::getEmailCharset();
- $pgp_sign = new Horde_Mime_Part();
- $pgp_sign->setType('application/pgp-signature');
- $pgp_sign->setCharset($charset);
- $pgp_sign->setDisposition('inline');
- $pgp_sign->setDescription(String::convertCharset(_("PGP Digital Signature"), NLS::getCharset(), $charset));
- $pgp_sign->setContents($msg_sign);
-
- /* Get the algorithim information from the signature. Since we are
- * analyzing a signature packet, we need to use the special keyword
- * '_SIGNATURE' - see Horde_Crypt_pgp. */
- $sig_info = $this->pgpPacketSignature($msg_sign, '_SIGNATURE');
-
- /* Setup the multipart MIME Part. */
- $part = new Horde_Mime_Part();
- $part->setType('multipart/signed');
- $part->setContents('This message is in MIME format and has been PGP signed.' . "\n");
- $part->addPart($mime_part);
- $part->addPart($pgp_sign);
- $part->setContentTypeParameter('protocol', 'application/pgp-signature');
- $part->setContentTypeParameter('micalg', $sig_info['micalg']);
-
- return $part;
- }
-
- /**
- * Encrypts a MIME part using PGP.
- *
- * @param Horde_Mime_Part $mime_part The object to encrypt.
- * @param array $params The parameters required for
- * encryption.
- * @see _encryptMessage().
- *
- * @return mixed A Horde_Mime_Part object that is encrypted according to
- * RFC 3156, or PEAR_Error on error.
- */
- public function encryptMIMEPart($mime_part, $params = array())
- {
- $params = array_merge($params, array('type' => 'message'));
-
- $signenc_body = $mime_part->toCanonicalString();
- $message_encrypt = $this->encrypt($signenc_body, $params);
- if (is_a($message_encrypt, 'PEAR_Error')) {
- return $message_encrypt;
- }
-
- /* Set up MIME Structure according to RFC 3156. */
- $charset = NLS::getEmailCharset();
- $part = new Horde_Mime_Part();
- $part->setType('multipart/encrypted');
- $part->setCharset($charset);
- $part->setContentTypeParameter('protocol', 'application/pgp-encrypted');
- $part->setDescription(String::convertCharset(_("PGP Encrypted Data"), NLS::getCharset(), $charset));
- $part->setContents('This message is in MIME format and has been PGP encrypted.' . "\n");
-
- $part1 = new Horde_Mime_Part();
- $part1->setType('application/pgp-encrypted');
- $part1->setCharset(null);
- $part1->setContents("Version: 1\n");
- $part->addPart($part1);
-
- $part2 = new Horde_Mime_Part();
- $part2->setType('application/octet-stream');
- $part2->setCharset(null);
- $part2->setContents($message_encrypt);
- $part2->setDisposition('inline');
- $part->addPart($part2);
-
- return $part;
- }
-
- /**
- * Signs and encrypts a MIME part using PGP.
- *
- * @param Horde_Mime_Part $mime_part The object to sign and encrypt.
- * @param array $sign_params The parameters required for
- * signing. @see _encryptSignature().
- * @param array $encrypt_params The parameters required for
- * encryption. @see _encryptMessage().
- *
- * @return mixed A Horde_Mime_Part object that is signed and encrypted
- * according to RFC 3156, or PEAR_Error on error.
- */
- public function signAndEncryptMIMEPart($mime_part, $sign_params = array(),
- $encrypt_params = array())
- {
- /* RFC 3156 requires that the entire signed message be encrypted. We
- * need to explicitly call using Horde_Crypt_pgp:: because we don't
- * know whether a subclass has extended these methods. */
- $part = $this->signMIMEPart($mime_part, $sign_params);
- if (is_a($part, 'PEAR_Error')) {
- return $part;
- }
- $part = $this->encryptMIMEPart($part, $encrypt_params);
- if (is_a($part, 'PEAR_Error')) {
- return $part;
- }
- $part->setContents('This message is in MIME format and has been PGP signed and encrypted.' . "\n");
-
- $charset = NLS::getEmailCharset();
- $part->setCharset($charset);
- $part->setDescription(String::convertCharset(_("PGP Signed/Encrypted Data"), NLS::getCharset(), $charset));
-
- return $part;
- }
-
- /**
- * Generates a Horde_Mime_Part object, in accordance with RFC 3156, that
- * contains a public key.
- *
- * @param string $key The public key.
- *
- * @return Horde_Mime_Part An object that contains the public key.
- */
- public function publicKeyMIMEPart($key)
- {
- include_once 'Horde/Mime/Part.php';
-
- $charset = NLS::getEmailCharset();
- $part = new Horde_Mime_Part();
- $part->setType('application/pgp-keys');
- $part->setCharset($charset);
- $part->setDescription(String::convertCharset(_("PGP Public Key"), NLS::getCharset(), $charset));
- $part->setContents($key);
-
- return $part;
- }
-
- /**
- * Function that handles interfacing with the GnuPG binary.
- *
- * @param array $options Options and commands to pass to GnuPG.
- * @param string $mode 'r' to read from stdout, 'w' to write to stdin.
- * @param array $input Input to write to stdin.
- * @param boolean $output If true, collect and store output in object returned.
- * @param boolean $stderr If true, collect and store stderr in object returned.
- * @param boolean $verbose If true, run GnuPG with quiet flag.
- *
- * @return stdClass Class with members output, stderr, and stdout.
- */
- protected function _callGpg($options, $mode, $input = array(),
- $output = false, $stderr = false,
- $verbose = false)
- {
- $data = new stdClass;
- $data->output = null;
- $data->stderr = null;
- $data->stdout = null;
-
- /* Verbose output? */
- if (!$verbose) {
- array_unshift($options, '--quiet');
- }
-
- /* Create temp files for output. */
- if ($output) {
- $output_file = $this->_createTempFile('horde-pgp', false);
- array_unshift($options, '--output ' . $output_file);
-
- /* Do we need standard error output? */
- if ($stderr) {
- $stderr_file = $this->_createTempFile('horde-pgp', false);
- $options[] = '2> ' . $stderr_file;
- }
- }
-
- /* Silence errors if not requested. */
- if (!$output || !$stderr) {
- $options[] = '2> /dev/null';
- }
-
- /* Build the command line string now. */
- $cmdline = implode(' ', array_merge($this->_gnupg, $options));
-
- if ($mode == 'w') {
- $fp = popen($cmdline, 'w');
- $win32 = !strncasecmp(PHP_OS, 'WIN', 3);
-
- if (!is_array($input)) {
- $input = array($input);
- }
- foreach ($input as $line) {
- if ($win32 && (strpos($line, "\x0d\x0a") !== false)) {
- $chunks = explode("\x0d\x0a", $line);
- foreach ($chunks as $chunk) {
- fputs($fp, $chunk . "\n");
- }
- } else {
- fputs($fp, $line . "\n");
- }
- }
- } elseif ($mode == 'r') {
- $fp = popen($cmdline, 'r');
- while (!feof($fp)) {
- $data->stdout .= fgets($fp, 1024);
- }
- }
- pclose($fp);
-
- if ($output) {
- $data->output = file_get_contents($output_file);
- unlink($output_file);
- if ($stderr) {
- $data->stderr = file_get_contents($stderr_file);
- unlink($stderr_file);
- }
- }
-
- return $data;
- }
-
- /**
- * Generates a revocation certificate.
- *
- * @param string $key The private key.
- * @param string $email The email to use for the key.
- * @param string $passphrase The passphrase to use for the key.
- *
- * @return string The revocation certificate, or PEAR_Error on error.
- */
- public function generateRevocation($key, $email, $passphrase)
- {
- $keyring = $this->_putInKeyring($key, 'private');
-
- /* Prepare the canned answers. */
- $input = array();
- $input[] = 'y'; // Really generate a revocation certificate
- $input[] = '0'; // Refuse to specify a reason
- $input[] = ''; // Empty comment
- $input[] = 'y'; // Confirm empty comment
- if (!empty($passphrase)) {
- $input[] = $passphrase;
- }
-
- /* Run through gpg binary. */
- $cmdline = array(
- $keyring,
- '--command-fd 0',
- '--gen-revoke' . ' ' . $email,
- );
- $results = $this->_callGpg($cmdline, 'w', $input, true);
-
- /* If the key is empty, something went wrong. */
- if (empty($results->output)) {
- return PEAR::raiseError(_("Revocation key not generated successfully."), 'horde.error');
- }
-
- return $results->output;
- }
-
-}
+++ /dev/null
-<?php
-/**
- * Horde_Crypt_smime:: provides a framework for Horde applications to
- * interact with the OpenSSL library and implement S/MIME.
- *
- * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Mike Cochrane <mike@graftonhall.co.nz>
- * @package Horde_Crypt
- */
-class Horde_Crypt_smime extends Horde_Crypt
-{
- /**
- * Object Identifers to name array.
- *
- * @var array
- */
- protected $_oids = array(
- '2.5.4.3' => 'CommonName',
- '2.5.4.4' => 'Surname',
- '2.5.4.6' => 'Country',
- '2.5.4.7' => 'Location',
- '2.5.4.8' => 'StateOrProvince',
- '2.5.4.9' => 'StreetAddress',
- '2.5.4.10' => 'Organisation',
- '2.5.4.11' => 'OrganisationalUnit',
- '2.5.4.12' => 'Title',
- '2.5.4.20' => 'TelephoneNumber',
- '2.5.4.42' => 'GivenName',
-
- '2.5.29.14' => 'id-ce-subjectKeyIdentifier',
-
- '2.5.29.14' => 'id-ce-subjectKeyIdentifier',
- '2.5.29.15' => 'id-ce-keyUsage',
- '2.5.29.17' => 'id-ce-subjectAltName',
- '2.5.29.19' => 'id-ce-basicConstraints',
- '2.5.29.31' => 'id-ce-CRLDistributionPoints',
- '2.5.29.32' => 'id-ce-certificatePolicies',
- '2.5.29.35' => 'id-ce-authorityKeyIdentifier',
- '2.5.29.37' => 'id-ce-extKeyUsage',
-
- '1.2.840.113549.1.9.1' => 'Email',
- '1.2.840.113549.1.1.1' => 'RSAEncryption',
- '1.2.840.113549.1.1.2' => 'md2WithRSAEncryption',
- '1.2.840.113549.1.1.4' => 'md5withRSAEncryption',
- '1.2.840.113549.1.1.5' => 'SHA-1WithRSAEncryption',
- '1.2.840.10040.4.3' => 'id-dsa-with-sha-1',
-
- '1.3.6.1.5.5.7.3.2' => 'id_kp_clientAuth',
-
- '2.16.840.1.113730.1.1' => 'netscape-cert-type',
- '2.16.840.1.113730.1.2' => 'netscape-base-url',
- '2.16.840.1.113730.1.3' => 'netscape-revocation-url',
- '2.16.840.1.113730.1.4' => 'netscape-ca-revocation-url',
- '2.16.840.1.113730.1.7' => 'netscape-cert-renewal-url',
- '2.16.840.1.113730.1.8' => 'netscape-ca-policy-url',
- '2.16.840.1.113730.1.12' => 'netscape-ssl-server-name',
- '2.16.840.1.113730.1.13' => 'netscape-comment',
- );
-
- /**
- * Constructor.
- *
- * @param array $params Parameter array.
- * 'temp' => Location of temporary directory.
- */
- function __construct($params)
- {
- $this->_tempdir = $params['temp'];
- }
-
- /**
- * Verify a passphrase for a given private key.
- *
- * @param string $private_key The user's private key.
- * @param string $passphrase The user's passphrase.
- *
- * @return boolean Returns true on valid passphrase, false on invalid
- * passphrase.
- * Returns PEAR_Error on error.
- */
- public function verifyPassphrase($private_key, $passphrase)
- {
- if (is_null($passphrase)) {
- $res = openssl_pkey_get_private($private_key);
- } else {
- $res = openssl_pkey_get_private($private_key, $passphrase);
- }
-
- return is_resource($res);
- }
-
- /**
- * Encrypt text using S/MIME.
- *
- * @param string $text The text to be encrypted.
- * @param array $params The parameters needed for encryption.
- * See the individual _encrypt*() functions for
- * the parameter requirements.
- *
- * @return string The encrypted message.
- * Returns PEAR_Error object on error.
- */
- public function encrypt($text, $params = array())
- {
- /* Check for availability of OpenSSL PHP extension. */
- $openssl = $this->checkForOpenSSL();
- if (is_a($openssl, 'PEAR_Error')) {
- return $openssl;
- }
-
- if (isset($params['type'])) {
- if ($params['type'] === 'message') {
- return $this->_encryptMessage($text, $params);
- } elseif ($params['type'] === 'signature') {
- return $this->_encryptSignature($text, $params);
- }
- }
- }
-
- /**
- * Decrypt text via S/MIME.
- *
- * @param string $text The text to be smime decrypted.
- * @param array $params The parameters needed for decryption.
- * See the individual _decrypt*() functions for
- * the parameter requirements.
- *
- * @return string The decrypted message.
- * Returns PEAR_Error object on error.
- */
- public function decrypt($text, $params = array())
- {
- /* Check for availability of OpenSSL PHP extension. */
- $openssl = $this->checkForOpenSSL();
- if (is_a($openssl, 'PEAR_Error')) {
- return $openssl;
- }
-
- if (isset($params['type'])) {
- if ($params['type'] === 'message') {
- return $this->_decryptMessage($text, $params);
- } elseif (($params['type'] === 'signature') ||
- ($params['type'] === 'detached-signature')) {
- return $this->_decryptSignature($text, $params);
- }
- }
- }
-
- /**
- * Verify a signature using via S/MIME.
- *
- * @param string $text The multipart/signed data to be verified.
- * @param mixed $certs Either a single or array of root certificates.
- *
- * @return stdClass Object with the following elements:
- * 'result' -> Returns true on success;
- * PEAR_Error object on error.
- * 'cert' -> The certificate of the signer stored
- * in the message (in PEM format).
- * 'email' -> The email of the signing person.
- */
- public function verify($text, $certs)
- {
- /* Check for availability of OpenSSL PHP extension. */
- $openssl = $this->checkForOpenSSL();
- if (is_a($openssl, 'PEAR_Error')) {
- return $openssl;
- }
-
- /* Create temp files for input/output. */
- $input = $this->_createTempFile('horde-smime');
- $output = $this->_createTempFile('horde-smime');
-
- /* Write text to file */
- file_put_contents($input, $text);
- unset($text);
-
- $root_certs = array();
- if (!is_array($certs)) {
- $certs = array($certs);
- }
- foreach ($certs as $file) {
- if (file_exists($file)) {
- $root_certs[] = $file;
- }
- }
-
- $ob = new stdClass;
-
- if (!empty($root_certs)) {
- $result = openssl_pkcs7_verify($input, 0, $output, $root_certs);
- /* Message verified */
- if ($result === true) {
- $ob->result = true;
- $ob->cert = file_get_contents($output);
- $ob->email = $this->getEmailFromKey($ob->cert);
- return $ob;
- }
- }
-
- /* Try again without verfying the signer's cert */
- $result = openssl_pkcs7_verify($input, PKCS7_NOVERIFY, $output);
-
- if ($result === true) {
- $ob->result = PEAR::raiseError(_("Message Verified Successfully but the signer's certificate could not be verified."), 'horde.warning');
- } elseif ($result == -1) {
- $ob->result = PEAR::raiseError(_("Verification failed - an unknown error has occurred."), 'horde.error');
- } else {
- $ob->result = PEAR::raiseError(_("Verification failed - this message may have been tampered with."), 'horde.error');
- }
-
- $ob->cert = file_get_contents($output);
- $ob->email = $this->getEmailFromKey($ob->cert);
-
- return $ob;
- }
-
- /**
- * Extract the contents from signed S/MIME data.
- *
- * @param string $data The signed S/MIME data.
- * @param string $sslpath The path to the OpenSSL binary.
- *
- * @return string The contents embedded in the signed data.
- * Returns PEAR_Error on error.
- */
- public function extractSignedContents($data, $sslpath)
- {
- /* Check for availability of OpenSSL PHP extension. */
- $openssl = $this->checkForOpenSSL();
- if (is_a($openssl, 'PEAR_Error')) {
- return $openssl;
- }
-
- /* Create temp files for input/output. */
- $input = $this->_createTempFile('horde-smime');
- $output = $this->_createTempFile('horde-smime');
-
- /* Write text to file. */
- file_put_contents($input, $data);
- unset($data);
-
- exec($sslpath . ' smime -verify -noverify -nochain -in ' . $input . ' -out ' . $output);
-
- $ret = file_get_contents($output);
- return $ret
- ? $ret
- : PEAR::raiseError(_("OpenSSL error: Could not extract data from signed S/MIME part."), 'horde.error');
- }
-
- /**
- * Sign a MIME part using S/MIME.
- *
- * @param Horde_Mime_Part $mime_part The object to sign.
- * @param array $params The parameters required for signing.
- *
- * @return mixed A Horde_Mime_Part object that is signed, or a
- * PEAR_Error object on error.
- */
- public function signMIMEPart($mime_part, $params)
- {
- /* Sign the part as a message */
- $message = $this->encrypt($mime_part->toCanonicalString(), $params);
- if (is_a($message, 'PEAR_Error')) {
- return $message;
- }
-
- /* Break the result into its components */
- $mime_message = Horde_Mime_Part::parseMessage($message);
-
- $smime_sign = $mime_message->getPart('2');
- $smime_sign->setDescription(_("S/MIME Cryptographic Signature"));
- $smime_sign->transferDecodeContents();
- $smime_sign->setTransferEncoding('base64');
-
- $smime_part = new Horde_Mime_Part();
- $smime_part->setType('multipart/signed');
- $smime_part->setContents('This is a cryptographically signed message in MIME format.' . "\n");
- $smime_part->setContentTypeParameter('protocol', 'application/pkcs7-signature');
- $smime_part->setContentTypeParameter('micalg', 'sha1');
- $smime_part->addPart($mime_part);
- $smime_part->addPart($smime_sign);
-
- return $smime_part;
- }
-
- /**
- * Encrypt a MIME part using S/MIME.
- *
- * @param Horde_Mime_Part $mime_part The object to encrypt.
- * @param array $params The parameters required for
- * encryption.
- *
- * @return mixed A Horde_Mime_Part object that is encrypted or a
- * PEAR_Error on error.
- */
- public function encryptMIMEPart($mime_part, $params = array())
- {
- /* Sign the part as a message */
- $message = $this->encrypt($mime_part->toCanonicalString(), $params);
- if (is_a($message, 'PEAR_Error')) {
- return $message;
- }
-
- /* Get charset for mime part description. */
- $charset = NLS::getEmailCharset();
-
- $msg = new Horde_Mime_Part();
- $msg->setCharset($charset);
- $msg->setDescription(String::convertCharset(_("S/MIME Encrypted Message"), NLS::getCharset(), $charset));
- $msg->setDisposition('inline');
- $msg->setType('application/pkcs7-mime');
- $msg->setContentTypeParameter('smime-type', 'enveloped-data');
- $msg->setContents(substr($message, strpos($message, "\n\n") + 2));
-
- return $msg;
- }
-
- /**
- * Encrypt a message in S/MIME format using a public key.
- *
- * @param string $text The text to be encrypted.
- * @param array $params The parameters needed for encryption.
- * <pre>
- * Parameters:
- * ===========
- * 'type' => 'message' (REQUIRED)
- * 'pubkey' => public key (REQUIRED)
- * </pre>
- *
- * @return string The encrypted message.
- * Return PEAR_Error object on error.
- */
- protected function _encryptMessage($text, $params)
- {
- /* Check for required parameters. */
- if (!isset($params['pubkey'])) {
- return PEAR::raiseError(_("A public S/MIME key is required to encrypt a message."), 'horde.error');
- }
-
- /* Create temp files for input/output. */
- $input = $this->_createTempFile('horde-smime');
- $output = $this->_createTempFile('horde-smime');
-
- /* Store message in file. */
- file_put_contents($input, $text);
- unset($text);
-
- /* Encrypt the document. */
- if (openssl_pkcs7_encrypt($input, $output, $params['pubkey'], array())) {
- $result = file_get_contents($output);
- if (!empty($result)) {
- return $this->_fixContentType($result, 'encrypt');
- }
- }
-
- return PEAR::raiseError(_("Could not S/MIME encrypt message."), 'horde.error');
- }
-
- /**
- * Sign a message in S/MIME format using a private key.
- *
- * @param string $text The text to be signed.
- * @param array $params The parameters needed for signing.
- * <pre>
- * Parameters:
- * ===========
- * 'certs' => Additional signing certs (Optional)
- * 'passphrase' => Passphrase for key (REQUIRED)
- * 'privkey' => Private key (REQUIRED)
- * 'pubkey' => Public key (REQUIRED)
- * 'sigtype' => Determine the signature type to use. (Optional)
- * 'cleartext' -- Make a clear text signature
- * 'detach' -- Make a detached signature (DEFAULT)
- * 'type' => 'signature' (REQUIRED)
- * </pre>
- *
- * @return string The signed message.
- * Return PEAR_Error object on error.
- */
- protected function _encryptSignature($text, $params)
- {
- /* Check for required parameters. */
- if (!isset($params['pubkey']) ||
- !isset($params['privkey']) ||
- !array_key_exists('passphrase', $params)) {
- return PEAR::raiseError(_("A public S/MIME key, private S/MIME key, and passphrase are required to sign a message."), 'horde.error');
- }
-
- /* Create temp files for input/output/certificates. */
- $input = $this->_createTempFile('horde-smime');
- $output = $this->_createTempFile('horde-smime');
- $certs = $this->_createTempFile('horde-smime');
-
- /* Store message in temporary file. */
- file_put_contents($input, $text);
- unset($text);
-
- /* Store additional certs in temporary file. */
- if (!empty($params['certs'])) {
- file_put_contents($certs, $params['certs']);
- }
-
- /* Determine the signature type to use. */
- if (isset($params['sigtype']) && ($params['sigtype'] == 'cleartext')) {
- $flags = PKCS7_TEXT;
- } else {
- $flags = PKCS7_DETACHED;
- }
-
- $privkey = (is_null($params['passphrase'])) ? $params['privkey'] : array($params['privkey'], $params['passphrase']);
-
- if (empty($params['certs'])) {
- $res = openssl_pkcs7_sign($input, $output, $params['pubkey'], $privkey, array(), $flags);
- } else {
- $res = openssl_pkcs7_sign($input, $output, $params['pubkey'], $privkey, array(), $flags, $certs);
- }
-
- if (!$res) {
- return PEAR::raiseError(_("Could not S/MIME sign message."), 'horde.error');
- }
-
- $data = file_get_contents($output);
- return $this->_fixContentType($data, 'signature');
- }
-
- /**
- * Decrypt an S/MIME encrypted message using a private/public keypair
- * and a passhprase.
- *
- * @param string $text The text to be decrypted.
- * @param array $params The parameters needed for decryption.
- * <pre>
- * Parameters:
- * ===========
- * 'type' => 'message' (REQUIRED)
- * 'pubkey' => public key. (REQUIRED)
- * 'privkey' => private key. (REQUIRED)
- * 'passphrase' => Passphrase for Key. (REQUIRED)
- * </pre>
- *
- * @return string The decrypted message.
- * Returns PEAR_Error object on error.
- */
- protected function _decryptMessage($text, $params)
- {
- /* Check for required parameters. */
- if (!isset($params['pubkey']) ||
- !isset($params['privkey']) ||
- !array_key_exists('passphrase', $params)) {
- return PEAR::raiseError(_("A public S/MIME key, private S/MIME key, and passphrase are required to decrypt a message."), 'horde.error');
- }
-
- /* Create temp files for input/output. */
- $input = $this->_createTempFile('horde-smime');
- $output = $this->_createTempFile('horde-smime');
-
- /* Store message in file. */
- file_put_contents($input, $text);
- unset($text);
-
- $privkey = (is_null($params['passphrase'])) ? $params['privkey'] : array($params['privkey'], $params['passphrase']);
- if (openssl_pkcs7_decrypt($input, $output, $params['pubkey'], $privkey)) {
- return file_get_contents($output);
- }
-
- return PEAR::raiseError(_("Could not decrypt S/MIME data."), 'horde.error');
- }
-
- /**
- * Sign and Encrypt a MIME part using S/MIME.
- *
- * @param Horde_Mime_Part $mime_part The object to sign and encrypt.
- * @param array $sign_params The parameters required for
- * signing. @see _encryptSignature().
- * @param array $encrypt_params The parameters required for
- * encryption.
- * @see _encryptMessage().
- *
- * @return mixed A Horde_Mime_Part object that is signed and encrypted.
- * Returns PEAR_Error on error.
- */
- public function signAndEncryptMIMEPart($mime_part, $sign_params = array(),
- $encrypt_params = array())
- {
- $part = $this->signMIMEPart($mime_part, $sign_params);
- if (is_a($part, 'PEAR_Error')) {
- return $part;
- }
-
- return $this->encryptMIMEPart($part, $encrypt_params);
- }
-
- /**
- * Convert a PEM format certificate to readable HTML version
- *
- * @param string $cert PEM format certificate
- *
- * @return string HTML detailing the certificate.
- */
- public function certToHTML($cert)
- {
- /* Common Fields */
- $fieldnames = array(
- 'Email' => _("Email Address"),
- 'CommonName' => _("Common Name"),
- 'Organisation' => _("Organisation"),
- 'OrganisationalUnit' => _("Organisational Unit"),
- 'Country' => _("Country"),
- 'StateOrProvince' => _("State or Province"),
- 'Location' => _("Location"),
- 'StreetAddress' => _("Street Address"),
- 'TelephoneNumber' => _("Telephone Number"),
- 'Surname' => _("Surname"),
- 'GivenName' => _("Given Name")
- );
-
- /* Netscape Extensions */
- $fieldnames += array(
- 'netscape-cert-type' => _("Netscape certificate type"),
- 'netscape-base-url' => _("Netscape Base URL"),
- 'netscape-revocation-url' => _("Netscape Revocation URL"),
- 'netscape-ca-revocation-url' => _("Netscape CA Revocation URL"),
- 'netscape-cert-renewal-url' => _("Netscape Renewal URL"),
- 'netscape-ca-policy-url' => _("Netscape CA policy URL"),
- 'netscape-ssl-server-name' => _("Netscape SSL server name"),
- 'netscape-comment' => _("Netscape certificate comment")
- );
-
- /* X590v3 Extensions */
- $fieldnames += array(
- 'id-ce-extKeyUsage' => _("X509v3 Extended Key Usage"),
- 'id-ce-basicConstraints' => _("X509v3 Basic Constraints"),
- 'id-ce-subjectAltName' => _("X509v3 Subject Alternative Name"),
- 'id-ce-subjectKeyIdentifier' => _("X509v3 Subject Key Identifier"),
- 'id-ce-certificatePolicies' => _("Certificate Policies"),
- 'id-ce-CRLDistributionPoints' => _("CRL Distribution Points"),
- 'id-ce-keyUsage' => _("Key Usage")
- );
-
- $cert_details = $this->parseCert($cert);
- if (!is_array($cert_details)) {
- return '<pre class="fixed">' . _("Unable to extract certificate details") . '</pre>';
- }
- $certificate = $cert_details['certificate'];
-
- $text = '<pre class="fixed">';
-
- /* Subject (a/k/a Certificate Owner) */
- if (isset($certificate['subject'])) {
- $text .= "<strong>" . _("Certificate Owner") . ":</strong>\n";
-
- foreach ($certificate['subject'] as $key => $value) {
- if (isset($fieldnames[$key])) {
- $text .= sprintf(" %s: %s\n", $fieldnames[$key], $value);
- } else {
- $text .= sprintf(" *%s: %s\n", $key, $value);
- }
- }
- $text .= "\n";
- }
-
- /* Issuer */
- if (isset($certificate['issuer'])) {
- $text .= "<strong>" . _("Issuer") . ":</strong>\n";
-
- foreach ($certificate['issuer'] as $key => $value) {
- if (isset($fieldnames[$key])) {
- $text .= sprintf(" %s: %s\n", $fieldnames[$key], $value);
- } else {
- $text .= sprintf(" *%s: %s\n", $key, $value);
- }
- }
- $text .= "\n";
- }
-
- /* Dates */
- $text .= "<strong>" . _("Validity") . ":</strong>\n";
- $text .= sprintf(" %s: %s\n", _("Not Before"), strftime("%x %X", $certificate['validity']['notbefore']));
- $text .= sprintf(" %s: %s\n", _("Not After"), strftime("%x %X", $certificate['validity']['notafter']));
- $text .= "\n";
-
- /* Certificate Owner - Public Key Info */
- $text .= "<strong>" . _("Public Key Info") . ":</strong>\n";
- $text .= sprintf(" %s: %s\n", _("Public Key Algorithm"), $certificate['subjectPublicKeyInfo']['algorithm']);
- if ($certificate['subjectPublicKeyInfo']['algorithm'] == 'rsaEncryption') {
- if (Util::extensionExists('bcmath')) {
- $modulus = $certificate['subjectPublicKeyInfo']['subjectPublicKey']['modulus'];
- $modulus_hex = '';
- while ($modulus != '0') {
- $modulus_hex = dechex(bcmod($modulus, '16')) . $modulus_hex;
- $modulus = bcdiv($modulus, '16', 0);
- }
-
- if ((strlen($modulus_hex) > 64) &&
- (strlen($modulus_hex) < 128)) {
- str_pad($modulus_hex, 128, '0', STR_PAD_RIGHT);
- } elseif ((strlen($modulus_hex) > 128) &&
- (strlen($modulus_hex) < 256)) {
- str_pad($modulus_hex, 256, '0', STR_PAD_RIGHT);
- }
-
- $text .= " " . sprintf(_("RSA Public Key (%d bit)"), strlen($modulus_hex) * 4) . ":\n";
-
- $modulus_str = '';
- for ($i = 0; $i < strlen($modulus_hex); $i += 2) {
- if (($i % 32) == 0) {
- $modulus_str .= "\n ";
- }
- $modulus_str .= substr($modulus_hex, $i, 2) . ':';
- }
-
- $text .= sprintf(" %s: %s\n", _("Modulus"), $modulus_str);
- }
-
- $text .= sprintf(" %s: %s\n", _("Exponent"), $certificate['subjectPublicKeyInfo']['subjectPublicKey']['publicExponent']);
- }
- $text .= "\n";
-
- /* X509v3 extensions */
- if (isset($certificate['extensions'])) {
- $text .= "<strong>" . _("X509v3 extensions") . ":</strong>\n";
-
- foreach ($certificate['extensions'] as $key => $value) {
- if (is_array($value)) {
- $value = _("Unsupported Extension");
- }
- if (isset($fieldnames[$key])) {
- $text .= sprintf(" %s:\n %s\n", $fieldnames[$key], wordwrap($value, 40, "\n "));
- } else {
- $text .= sprintf(" %s:\n %s\n", $key, wordwrap($value, 60, "\n "));
- }
- }
-
- $text .= "\n";
- }
-
- /* Certificate Details */
- $text .= "<strong>" . _("Certificate Details") . ":</strong>\n";
- $text .= sprintf(" %s: %d\n", _("Version"), $certificate['version']);
- $text .= sprintf(" %s: %d\n", _("Serial Number"), $certificate['serialNumber']);
-
- foreach ($cert_details['fingerprints'] as $hash => $fingerprint) {
- $label = sprintf(_("%s Fingerprint"), String::upper($hash));
- $fingerprint_str = '';
- for ($i = 0; $i < strlen($fingerprint); $i += 2) {
- $fingerprint_str .= substr($fingerprint, $i, 2) . ':';
- }
- $text .= sprintf(" %s:\n %s\n", $label, $fingerprint_str);
- }
- $text .= sprintf(" %s: %s\n", _("Signature Algorithm"), $cert_details['signatureAlgorithm']);
- $text .= sprintf(" %s:", _("Signature"));
-
- $sig_str = '';
- for ($i = 0; $i < strlen($cert_details['signature']); $i++) {
- if (($i % 16) == 0) {
- $sig_str .= "\n ";
- }
- $sig_str .= sprintf("%02x:", ord($cert_details['signature'][$i]));
- }
-
- return $text . $sig_str . "\n</pre>";
- }
-
- /**
- * Extract the contents of a PEM format certificate to an array.
- *
- * @param string $cert PEM format certificate
- *
- * @return array Array containing all extractable information about
- * the certificate.
- */
- public function parseCert($cert)
- {
- $cert_split = preg_split('/(-----((BEGIN)|(END)) CERTIFICATE-----)/', $cert);
- if (!isset($cert_split[1])) {
- $raw_cert = base64_decode($cert);
- } else {
- $raw_cert = base64_decode($cert_split[1]);
- }
-
- $cert_data = $this->_parseASN($raw_cert);
- if (!is_array($cert_data) || ($cert_data[0] == 'UNKNOWN')) {
- return false;
- }
-
- $cert_details = array();
- $cert_details['fingerprints']['md5'] = hash('md5', $raw_cert);
- $cert_details['fingerprints']['sha1'] = hash('sha1', $raw_cert);
-
- $cert_details['certificate']['extensions'] = array();
- $cert_details['certificate']['version'] = $cert_data[1][0][1][0][1] + 1;
- $cert_details['certificate']['serialNumber'] = $cert_data[1][0][1][1][1];
- $cert_details['certificate']['signature'] = $cert_data[1][0][1][2][1][0][1];
- $cert_details['certificate']['issuer'] = $cert_data[1][0][1][3][1];
- $cert_details['certificate']['validity'] = $cert_data[1][0][1][4][1];
- $cert_details['certificate']['subject'] = @$cert_data[1][0][1][5][1];
- $cert_details['certificate']['subjectPublicKeyInfo'] = $cert_data[1][0][1][6][1];
-
- $cert_details['signatureAlgorithm'] = $cert_data[1][1][1][0][1];
- $cert_details['signature'] = $cert_data[1][2][1];
-
- // issuer
- $issuer = array();
- foreach ($cert_details['certificate']['issuer'] as $value) {
- $issuer[$value[1][1][0][1]] = $value[1][1][1][1];
- }
- $cert_details['certificate']['issuer'] = $issuer;
-
- // subject
- $subject = array();
- foreach ($cert_details['certificate']['subject'] as $value) {
- $subject[$value[1][1][0][1]] = $value[1][1][1][1];
- }
- $cert_details['certificate']['subject'] = $subject;
-
- // validity
- $vals = $cert_details['certificate']['validity'];
- $cert_details['certificate']['validity'] = array();
- $cert_details['certificate']['validity']['notbefore'] = $vals[0][1];
- $cert_details['certificate']['validity']['notafter'] = $vals[1][1];
- foreach ($cert_details['certificate']['validity'] as $key => $val) {
- $year = substr($val, 0, 2);
- $month = substr($val, 2, 2);
- $day = substr($val, 4, 2);
- $hour = substr($val, 6, 2);
- $minute = substr($val, 8, 2);
- if (($val[11] == '-') || ($val[9] == '+')) {
- // handle time zone offset here
- $seconds = 0;
- } elseif (String::upper($val[11]) == 'Z') {
- $seconds = 0;
- } else {
- $seconds = substr($val, 10, 2);
- if (($val[11] == '-') || ($val[9] == '+')) {
- // handle time zone offset here
- }
- }
- $cert_details['certificate']['validity'][$key] = mktime ($hour, $minute, $seconds, $month, $day, $year);
- }
-
- // Split the Public Key into components.
- $subjectPublicKeyInfo = array();
- $subjectPublicKeyInfo['algorithm'] = $cert_details['certificate']['subjectPublicKeyInfo'][0][1][0][1];
- if ($subjectPublicKeyInfo['algorithm'] == 'rsaEncryption') {
- $subjectPublicKey = $this->_parseASN($cert_details['certificate']['subjectPublicKeyInfo'][1][1]);
- $subjectPublicKeyInfo['subjectPublicKey']['modulus'] = $subjectPublicKey[1][0][1];
- $subjectPublicKeyInfo['subjectPublicKey']['publicExponent'] = $subjectPublicKey[1][1][1];
- }
- $cert_details['certificate']['subjectPublicKeyInfo'] = $subjectPublicKeyInfo;
-
- if (isset($cert_data[1][0][1][7]) &&
- is_array($cert_data[1][0][1][7][1])) {
- foreach ($cert_data[1][0][1][7][1] as $ext) {
- $oid = $ext[1][0][1];
- $cert_details['certificate']['extensions'][$oid] = $ext[1][1];
- }
- }
-
- $i = 9;
-
- while (isset($cert_data[1][0][1][$i]) &&
- is_array($cert_data[1][0][1][$i][1])) {
- $oid = $cert_data[1][0][1][$i][1][0][1];
- $cert_details['certificate']['extensions'][$oid] = $cert_data[1][0][1][$i][1][1];
- ++$i;
- }
-
- foreach ($cert_details['certificate']['extensions'] as $oid => $val) {
- switch ($oid) {
- case 'netscape-base-url':
- case 'netscape-revocation-url':
- case 'netscape-ca-revocation-url':
- case 'netscape-cert-renewal-url':
- case 'netscape-ca-policy-url':
- case 'netscape-ssl-server-name':
- case 'netscape-comment':
- $val = $this->_parseASN($val[1]);
- $cert_details['certificate']['extensions'][$oid] = $val[1];
- break;
-
- case 'id-ce-subjectAltName':
- $val = $this->_parseASN($val[1]);
- $cert_details['certificate']['extensions'][$oid] = '';
- foreach ($val[1] as $name) {
- if (!empty($cert_details['certificate']['extensions'][$oid])) {
- $cert_details['certificate']['extensions'][$oid] .= ', ';
- }
- $cert_details['certificate']['extensions'][$oid] .= $name[1];
- }
- break;
-
- case 'netscape-cert-type':
- $val = $this->_parseASN($val[1]);
- $val = ord($val[1]);
- $newVal = '';
-
- if ($val & 0x80) {
- $newVal .= empty($newVal) ? 'SSL client' : ', SSL client';
- }
- if ($val & 0x40) {
- $newVal .= empty($newVal) ? 'SSL server' : ', SSL server';
- }
- if ($val & 0x20) {
- $newVal .= empty($newVal) ? 'S/MIME' : ', S/MIME';
- }
- if ($val & 0x10) {
- $newVal .= empty($newVal) ? 'Object Signing' : ', Object Signing';
- }
- if ($val & 0x04) {
- $newVal .= empty($newVal) ? 'SSL CA' : ', SSL CA';
- }
- if ($val & 0x02) {
- $newVal .= empty($newVal) ? 'S/MIME CA' : ', S/MIME CA';
- }
- if ($val & 0x01) {
- $newVal .= empty($newVal) ? 'Object Signing CA' : ', Object Signing CA';
- }
-
- $cert_details['certificate']['extensions'][$oid] = $newVal;
- break;
-
- case 'id-ce-extKeyUsage':
- $val = $this->_parseASN($val[1]);
- $val = $val[1];
-
- $newVal = '';
- if ($val[0][1] != 'sequence') {
- $val = array($val);
- } else {
- $val = $val[1][1];
- }
- foreach ($val as $usage) {
- if ($usage[1] == 'id_kp_clientAuth') {
- $newVal .= empty($newVal) ? 'TLS Web Client Authentication' : ', TLS Web Client Authentication';
- } else {
- $newVal .= empty($newVal) ? $usage[1] : ', ' . $usage[1];
- }
- }
- $cert_details['certificate']['extensions'][$oid] = $newVal;
- break;
-
- case 'id-ce-subjectKeyIdentifier':
- $val = $this->_parseASN($val[1]);
- $val = $val[1];
-
- $newVal = '';
-
- for ($i = 0; $i < strlen($val); $i++) {
- $newVal .= sprintf("%02x:", ord($val[$i]));
- }
- $cert_details['certificate']['extensions'][$oid] = $newVal;
- break;
-
- case 'id-ce-authorityKeyIdentifier':
- $val = $this->_parseASN($val[1]);
- if ($val[0] == 'string') {
- $val = $val[1];
-
- $newVal = '';
- for ($i = 0; $i < strlen($val); $i++) {
- $newVal .= sprintf("%02x:", ord($val[$i]));
- }
- $cert_details['certificate']['extensions'][$oid] = $newVal;
- } else {
- $cert_details['certificate']['extensions'][$oid] = _("Unsupported Extension");
- }
- break;
-
- case 'id-ce-basicConstraints':
- case 'default':
- $cert_details['certificate']['extensions'][$oid] = _("Unsupported Extension");
- break;
- }
- }
-
- return $cert_details;
- }
-
- /**
- * Attempt to parse ASN.1 formated data.
- *
- * @param string $data ASN.1 formated data
- *
- * @return array Array contained the extracted values.
- */
- protected function _parseASN($data)
- {
- $result = array();
-
- while (strlen($data) > 1) {
- $class = ord($data[0]);
- switch ($class) {
- case 0x30:
- // Sequence
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
- $sequence_data = substr($data, 2 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
-
- $values = $this->_parseASN($sequence_data);
- if (!is_array($values) || is_string($values[0])) {
- $values = array($values);
- }
- $sequence_values = array();
- $i = 0;
- foreach ($values as $val) {
- if ($val[0] == 'extension') {
- $sequence_values['extensions'][] = $val;
- } else {
- $sequence_values[$i++] = $val;
- }
- }
- $result[] = array('sequence', $sequence_values);
- break;
-
- case 0x31:
- // Set of
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
- $sequence_data = substr($data, 2 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
- $result[] = array('set', $this->_parseASN($sequence_data));
- break;
-
- case 0x01:
- // Boolean type
- $boolean_value = (ord($data[2]) == 0xff);
- $data = substr($data, 3);
- $result[] = array('boolean', $boolean_value);
- break;
-
- case 0x02:
- // Integer type
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
-
- $integer_data = substr($data, 2 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
-
- $value = 0;
- if ($len <= 4) {
- /* Method works fine for small integers */
- for ($i = 0; $i < strlen($integer_data); $i++) {
- $value = ($value << 8) | ord($integer_data[$i]);
- }
- } else {
- /* Method works for arbitrary length integers */
- if (Util::extensionExists('bcmath')) {
- for ($i = 0; $i < strlen($integer_data); $i++) {
- $value = bcadd(bcmul($value, 256), ord($integer_data[$i]));
- }
- } else {
- $value = -1;
- }
- }
- $result[] = array('integer(' . $len . ')', $value);
- break;
-
- case 0x03:
- // Bitstring type
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
- $bitstring_data = substr($data, 3 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
- $result[] = array('bit string', $bitstring_data);
- break;
-
- case 0x04:
- // Octetstring type
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
- $octectstring_data = substr($data, 2 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
- $result[] = array('octet string', $octectstring_data);
- break;
-
- case 0x05:
- // Null type
- $data = substr($data, 2);
- $result[] = array('null', null);
- break;
-
- case 0x06:
- // Object identifier type
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
- $oid_data = substr($data, 2 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
-
- // Unpack the OID
- $plain = floor(ord($oid_data[0]) / 40);
- $plain .= '.' . ord($oid_data[0]) % 40;
-
- $value = 0;
- $i = 1;
- while ($i < strlen($oid_data)) {
- $value = $value << 7;
- $value = $value | (ord($oid_data[$i]) & 0x7f);
-
- if (!(ord($oid_data[$i]) & 0x80)) {
- $plain .= '.' . $value;
- $value = 0;
- }
- $i++;
- }
-
- if (isset($this->_oids[$plain])) {
- $result[] = array('oid', $this->_oids[$plain]);
- } else {
- $result[] = array('oid', $plain);
- }
-
- break;
-
- case 0x12:
- case 0x13:
- case 0x14:
- case 0x15:
- case 0x16:
- case 0x81:
- case 0x80:
- // Character string type
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
- $string_data = substr($data, 2 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
- $result[] = array('string', $string_data);
- break;
-
- case 0x17:
- // Time types
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
- $time_data = substr($data, 2 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
- $result[] = array('utctime', $time_data);
- break;
-
- case 0x82:
- // X509v3 extensions?
- $len = ord($data[1]);
- $bytes = 0;
- if ($len & 0x80) {
- $bytes = $len & 0x0f;
- $len = 0;
- for ($i = 0; $i < $bytes; $i++) {
- $len = ($len << 8) | ord($data[$i + 2]);
- }
- }
- $sequence_data = substr($data, 2 + $bytes, $len);
- $data = substr($data, 2 + $bytes + $len);
- $result[] = array('extension', 'X509v3 extensions');
- $result[] = $this->_parseASN($sequence_data);
- break;
-
- case 0xa0:
- case 0xa3:
- // Extensions
- $extension_data = substr($data, 0, 2);
- $data = substr($data, 2);
- $result[] = array('extension', dechex($extension_data));
- break;
-
- case 0xe6:
- $extension_data = substr($data, 0, 1);
- $data = substr($data, 1);
- $result[] = array('extension', dechex($extension_data));
- break;
-
- case 0xa1:
- $extension_data = substr($data, 0, 1);
- $data = substr($data, 6);
- $result[] = array('extension', dechex($extension_data));
- break;
-
- default:
- // Unknown
- $result[] = array('UNKNOWN', dechex($data));
- $data = '';
- break;
- }
- }
-
- return (count($result) > 1) ? $result : array_pop($result);
- }
-
- /**
- * Decrypt an S/MIME signed message using a public key.
- *
- * @param string $text The text to be verified.
- * @param array $params The parameters needed for verification.
- *
- * @return string The verification message.
- * Returns PEAR_Error object on error.
- */
- protected function _decryptSignature($text, $params)
- {
- return PEAR::raiseError('_decryptSignature() ' . _("not yet implemented"));
- }
-
- /**
- * Check for the presence of the OpenSSL extension to PHP.
- *
- * @return boolean Returns true if the openssl extension is available.
- * Returns a PEAR_Error if not.
- */
- public function checkForOpenSSL()
- {
- if (!Util::extensionExists('openssl')) {
- return PEAR::raiseError(_("The openssl module is required for the Horde_Crypt_smime:: class."));
- }
- }
-
- /**
- * Extract the email address from a public key.
- *
- * @param string $key The public key.
- *
- * @return mixed Returns the first email address found, or null if
- * there are none.
- */
- public function getEmailFromKey($key)
- {
- $key_info = openssl_x509_parse($key);
- if (!is_array($key_info)) {
- return null;
- }
-
- if (isset($key_info['subject'])) {
- if (isset($key_info['subject']['Email'])) {
- return $key_info['subject']['Email'];
- } elseif (isset($key_info['subject']['emailAddress'])) {
- return $key_info['subject']['emailAddress'];
- }
- }
-
- // Check subjectAltName per http://www.ietf.org/rfc/rfc3850.txt
- if (isset($key_info['extensions']['subjectAltName'])) {
- $names = preg_split('/\s*,\s*/', $key_info['extensions']['subjectAltName'], -1, PREG_SPLIT_NO_EMPTY);
- foreach ($names as $name) {
- if (strpos($name, ':') === false) {
- continue;
- }
- list($kind, $value) = explode(':', $name, 2);
- if (String::lower($kind) == 'email') {
- return $value;
- }
- }
- }
-
- return null;
- }
-
- /**
- * Convert a PKCS 12 encrypted certificate package into a private key,
- * public key, and any additional keys.
- *
- * @param string $text The PKCS 12 data.
- * @param array $params The parameters needed for parsing.
- * <pre>
- * Parameters:
- * ===========
- * 'sslpath' => The path to the OpenSSL binary. (REQUIRED)
- * 'password' => The password to use to decrypt the data. (Optional)
- * 'newpassword' => The password to use to encrypt the private key.
- * (Optional)
- * </pre>
- *
- * @return stdClass An object.
- * 'private' - The private key in PEM format.
- * 'public' - The public key in PEM format.
- * 'certs' - An array of additional certs.
- * Returns PEAR_Error on error.
- */
- public function parsePKCS12Data($pkcs12, $params)
- {
- /* Check for availability of OpenSSL PHP extension. */
- $openssl = $this->checkForOpenSSL();
- if (is_a($openssl, 'PEAR_Error')) {
- return $openssl;
- }
-
- if (!isset($params['sslpath'])) {
- return PEAR::raiseError(_("No path to the OpenSSL binary provided. The OpenSSL binary is necessary to work with PKCS 12 data."), 'horde.error');
- }
- $sslpath = escapeshellcmd($params['sslpath']);
-
- /* Create temp files for input/output. */
- $input = $this->_createTempFile('horde-smime');
- $output = $this->_createTempFile('horde-smime');
-
- $ob = new stdClass;
-
- /* Write text to file */
- file_put_contents($input, $pkcs12);
- unset($pkcs12);
-
- /* Extract the private key from the file first. */
- $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nocerts';
- if (isset($params['password'])) {
- $cmdline .= ' -passin stdin';
- if (!empty($params['newpassword'])) {
- $cmdline .= ' -passout stdin';
- } else {
- $cmdline .= ' -nodes';
- }
- $fd = popen($cmdline, 'w');
- fwrite($fd, $params['password'] . "\n");
- if (!empty($params['newpassword'])) {
- fwrite($fd, $params['newpassword'] . "\n");
- }
- pclose($fd);
- } else {
- $cmdline .= ' -nodes';
- exec($cmdline);
- }
- $ob->private = trim(file_get_contents($output));
- if (empty($ob->private)) {
- return PEAR::raiseError(_("Password incorrect"), 'horde.error');
- }
-
- /* Extract the client public key next. */
- $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nokeys -clcerts';
- if (isset($params['password'])) {
- $cmdline .= ' -passin stdin';
- $fd = popen($cmdline, 'w');
- fwrite($fd, $params['password'] . "\n");
- pclose($fd);
- } else {
- exec($cmdline);
- }
- $ob->public = trim(file_get_contents($output));
-
- /* Extract the CA public key next. */
- $cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nokeys -cacerts';
- if (isset($params['password'])) {
- $cmdline .= ' -passin stdin';
- $fd = popen($cmdline, 'w');
- fwrite($fd, $params['password'] . "\n");
- pclose($fd);
- } else {
- exec($cmdline);
- }
- $ob->certs = trim(file_get_contents($output));
-
- return $ob;
- }
-
- /**
- * The Content-Type parameters PHP's openssl_pkcs7_* functions return are
- * deprecated. Fix these headers to the correct ones (see RFC 2311).
- *
- * @param string $text The PKCS7 data.
- * @param string $type Is this 'message' or 'signature' data?
- *
- * @return string The PKCS7 data with the correct Content-Type parameter.
- */
- protected function _fixContentType($text, $type)
- {
- if ($type == 'message') {
- $from = 'application/x-pkcs7-mime';
- $to = 'application/pkcs7-mime';
- } else {
- $from = 'application/x-pkcs7-signature';
- $to = 'application/pkcs7-signature';
- }
- return str_replace('Content-Type: ' . $from, 'Content-Type: ' . $to, $text);
- }
-
-}
<dir name="lib">
<dir name="Horde">
<dir name="Crypt">
- <file name="pgp.php" role="php" />
- <file name="smime.php" role="php" />
+ <file name="Pgp.php" role="php" />
+ <file name="Smime.php" role="php" />
</dir> <!-- /lib/Horde/Crypt -->
<file name="Crypt.php" role="php" />
</dir> <!-- /lib/Horde -->
</dependencies>
<phprelease>
<filelist>
- <install name="lib/Horde/Crypt/pgp.php" as="Horde/Crypt/pgp.php" />
- <install name="lib/Horde/Crypt/smime.php" as="Horde/Crypt/smime.php" />
+ <install name="lib/Horde/Crypt/Pgp.php" as="Horde/Crypt/Pgp.php" />
+ <install name="lib/Horde/Crypt/Smime.php" as="Horde/Crypt/Smime.php" />
<install name="lib/Horde/Crypt.php" as="Horde/Crypt.php" />
</filelist>
</phprelease>
$_SERVER['HTTPS'] = 'on';
$browser = &Browser::singleton();
-$pgp = Horde_Crypt::factory('pgp', array(
+$pgp = Horde_Crypt::factory('Pgp', array(
'program' => '/usr/bin/gpg',
'temp' => Util::getTempDir()
));
require 'Horde/Util.php';
require dirname(__FILE__) . '/../../../lib/Horde/Crypt.php';
-$smime = Horde_Crypt::factory('smime', array('temp' => Util::getTempDir()));
+$smime = Horde_Crypt::factory('Smime', array('temp' => Util::getTempDir()));