From: Michael M Slusarz Date: Thu, 26 Feb 2009 05:30:40 +0000 (-0700) Subject: Horde 4-ify Horde_Crypt. X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=84cbc20b0bb2da5532ae8f5514d826ade13e447f;p=horde.git Horde 4-ify Horde_Crypt. Rework driver naming scheme Use file_put_contents() in Pgp driver. Convert to full Exception handling. --- diff --git a/framework/Crypt/lib/Horde/Crypt.php b/framework/Crypt/lib/Horde/Crypt.php index 5d35bcc64..0dfa3ef28 100644 --- a/framework/Crypt/lib/Horde/Crypt.php +++ b/framework/Crypt/lib/Horde/Crypt.php @@ -14,6 +14,13 @@ class Horde_Crypt { /** + * Singleton instances. + * + * @var array + */ + static protected $_instances = array(); + + /** * The temporary directory to use. * * @var string @@ -30,8 +37,8 @@ class Horde_Crypt * @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()) { @@ -43,26 +50,17 @@ class Horde_Crypt } /* 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.'); } /** @@ -73,7 +71,7 @@ class Horde_Crypt * 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 @@ -82,26 +80,25 @@ class Horde_Crypt * @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() { @@ -131,7 +128,7 @@ class Horde_Crypt } } - return PEAR::raiseError(_("The encryption features require a secure web connection.")); + throw new Horde_Exception (_("The encryption features require a secure web connection.")); } /** @@ -156,6 +153,7 @@ class Horde_Crypt * @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()) { @@ -166,8 +164,6 @@ class Horde_Crypt * 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? * diff --git a/framework/Crypt/lib/Horde/Crypt/Pgp.php b/framework/Crypt/lib/Horde/Crypt/Pgp.php new file mode 100644 index 000000000..a8b07e87e --- /dev/null +++ b/framework/Crypt/lib/Horde/Crypt/Pgp.php @@ -0,0 +1,1609 @@ + + * @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: + *
+     * Key            Value
+     * --------------------------
+     * 'public'   =>  Public Key
+     * 'private'  =>  Private Key
+     * 
+ * @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. + *
+     * 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
+     *         )
+     *     )
+     * )
+     * 
+ * + * 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. + *
+     * 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
+     * 
+ */ + 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: + *
+     * '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.
+     * 
+ */ + 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: + *
+     * 'message' - (string) The signature result text.
+     * 'result' - (boolean) The result of the signature test.
+     * 
+ * @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. + *
+     * 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)
+     * 
+ * + * @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. + *
+     * 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)
+     * 
+ * + * @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. + *
+     * 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)
+     * 
+ * + * @return stdClass An object with the following properties: + *
+     * 'message'     -  The decrypted message.
+     * 'sig_result'  -  The result of the signature test.
+     * 
+ * @return stdClass An object with the following properties: + *
+     * 'message' - (string) The signature result text.
+     * 'result' - (boolean) The result of the signature test.
+     * 
+ * @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. + *
+     * Parameters:
+     * ===========
+     * 'type'       =>  'signature' or 'detached-signature' (REQUIRED)
+     * 'pubkey'     =>  PGP public key. (REQUIRED)
+     * 'signature'  =>  PGP signature block. (REQUIRED for detached signature)
+     * 
+ * + * @return stdClass An object with the following properties: + *
+     * 'message' - (string) The signature result text.
+     * 'result' - (boolean) The result of the signature test.
+     * 
+ * @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: + *
+     * 'message' - (string) The signature result text.
+     * 'result' - (boolean) The result of the signature test.
+     * 
+ * @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; + } + +} diff --git a/framework/Crypt/lib/Horde/Crypt/Smime.php b/framework/Crypt/lib/Horde/Crypt/Smime.php new file mode 100644 index 000000000..ec1cd7468 --- /dev/null +++ b/framework/Crypt/lib/Horde/Crypt/Smime.php @@ -0,0 +1,1302 @@ + + * @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. + *
+     * Parameters:
+     * ===========
+     * 'type'   => 'message' (REQUIRED)
+     * 'pubkey' => public key (REQUIRED)
+     * 
+ * + * @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. + *
+     * 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)
+     * 
+ * + * @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. + *
+     * Parameters:
+     * ===========
+     * 'type'        =>  'message' (REQUIRED)
+     * 'pubkey'      =>  public key. (REQUIRED)
+     * 'privkey'     =>  private key. (REQUIRED)
+     * 'passphrase'  =>  Passphrase for Key. (REQUIRED)
+     * 
+ * + * @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 '
' . _("Unable to extract certificate details") . '
'; + } + $certificate = $cert_details['certificate']; + + $text = '
';
+
+        /* Subject (a/k/a Certificate Owner) */
+        if (isset($certificate['subject'])) {
+            $text .= "" . _("Certificate Owner") . ":\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 .= "" . _("Issuer") . ":\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 .= "" . _("Validity") . ":\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 .= "" . _("Public Key Info") . ":\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 .= "" . _("X509v3 extensions") . ":\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 .= "" . _("Certificate Details") . ":\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
"; + } + + /** + * 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. + *
+     * 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)
+     * 
+ * + * @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); + } + +} diff --git a/framework/Crypt/lib/Horde/Crypt/pgp.php b/framework/Crypt/lib/Horde/Crypt/pgp.php deleted file mode 100644 index db144b141..000000000 --- a/framework/Crypt/lib/Horde/Crypt/pgp.php +++ /dev/null @@ -1,1631 +0,0 @@ - - * @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. - *
-     * Return array:
-     * Key            Value
-     * --------------------------
-     * 'public'   =>  Public Key
-     * 'private'  =>  Private Key
-     * 
- */ - 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. - *
-     * 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
-     *         )
-     *     )
-     * )
-     * 
- * - * 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. - *
-     * 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
-     * 
- */ - 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: - *
-     * '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.
-     * 
- */ - 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. - *
-     * 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)
-     * 
- * - * @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. - *
-     * 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)
-     * 
- * - * @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. - *
-     * 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)
-     * 
- * - * @return stdClass An object with the following properties, or PEAR_Error - * on error: - *
-     * 'message'     -  The decrypted message.
-     * 'sig_result'  -  The result of the signature test.
-     * 
- */ - 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. - *
-     * Parameters:
-     * ===========
-     * 'type'       =>  'signature' or 'detached-signature' (REQUIRED)
-     * 'pubkey'     =>  PGP public key. (REQUIRED)
-     * 'signature'  =>  PGP signature block. (REQUIRED for detached signature)
-     * 
- * - * @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; - } - -} diff --git a/framework/Crypt/lib/Horde/Crypt/smime.php b/framework/Crypt/lib/Horde/Crypt/smime.php deleted file mode 100644 index 44d4eca20..000000000 --- a/framework/Crypt/lib/Horde/Crypt/smime.php +++ /dev/null @@ -1,1331 +0,0 @@ - - * @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. - *
-     * Parameters:
-     * ===========
-     * 'type'   => 'message' (REQUIRED)
-     * 'pubkey' => public key (REQUIRED)
-     * 
- * - * @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. - *
-     * 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)
-     * 
- * - * @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. - *
-     * Parameters:
-     * ===========
-     * 'type'        =>  'message' (REQUIRED)
-     * 'pubkey'      =>  public key. (REQUIRED)
-     * 'privkey'     =>  private key. (REQUIRED)
-     * 'passphrase'  =>  Passphrase for Key. (REQUIRED)
-     * 
- * - * @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 '
' . _("Unable to extract certificate details") . '
'; - } - $certificate = $cert_details['certificate']; - - $text = '
';
-
-        /* Subject (a/k/a Certificate Owner) */
-        if (isset($certificate['subject'])) {
-            $text .= "" . _("Certificate Owner") . ":\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 .= "" . _("Issuer") . ":\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 .= "" . _("Validity") . ":\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 .= "" . _("Public Key Info") . ":\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 .= "" . _("X509v3 extensions") . ":\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 .= "" . _("Certificate Details") . ":\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
"; - } - - /** - * 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. - *
-     * 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)
-     * 
- * - * @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); - } - -} diff --git a/framework/Crypt/package.xml b/framework/Crypt/package.xml index 8b911b9de..b7e42bcba 100644 --- a/framework/Crypt/package.xml +++ b/framework/Crypt/package.xml @@ -30,8 +30,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> - - + + @@ -99,8 +99,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> - - + + diff --git a/framework/Crypt/test/Horde/Crypt/pgp.inc b/framework/Crypt/test/Horde/Crypt/pgp.inc index ea02f312d..f294dcdb8 100644 --- a/framework/Crypt/test/Horde/Crypt/pgp.inc +++ b/framework/Crypt/test/Horde/Crypt/pgp.inc @@ -13,7 +13,7 @@ require $filedir . '/../../../lib/Horde/Crypt.php'; $_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() )); diff --git a/framework/Crypt/test/Horde/Crypt/smime.inc b/framework/Crypt/test/Horde/Crypt/smime.inc index fab7acda2..77de29817 100644 --- a/framework/Crypt/test/Horde/Crypt/smime.inc +++ b/framework/Crypt/test/Horde/Crypt/smime.inc @@ -8,4 +8,4 @@ require 'Horde/String.php'; 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()));