Overview

Packages

  • Crypt

Classes

  • Horde_Crypt
  • Horde_Crypt_Exception
  • Horde_Crypt_Pgp
  • Horde_Crypt_Smime
  • Horde_Crypt_Translation
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Horde_Crypt_Pgp:: provides a framework for Horde applications to interact
   4:  * with the GNU Privacy Guard program ("GnuPG").  GnuPG implements the OpenPGP
   5:  * standard (RFC 2440).
   6:  *
   7:  * GnuPG Website: http://www.gnupg.org/
   8:  *
   9:  * This class has been developed with, and is only guaranteed to work with,
  10:  * Version 1.21 or above of GnuPG.
  11:  *
  12:  * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
  13:  *
  14:  * See the enclosed file COPYING for license information (LGPL). If you
  15:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  16:  *
  17:  * @author   Michael Slusarz <slusarz@horde.org>
  18:  * @category Horde
  19:  * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
  20:  * @package  Crypt
  21:  */
  22: class Horde_Crypt_Pgp extends Horde_Crypt
  23: {
  24:     /**
  25:      * Armor Header Lines - From RFC 2440:
  26:      *
  27:      * An Armor Header Line consists of the appropriate header line text
  28:      * surrounded by five (5) dashes ('-', 0x2D) on either side of the header
  29:      * line text. The header line text is chosen based upon the type of data
  30:      * that is being encoded in Armor, and how it is being encoded.
  31:      *
  32:      *  All Armor Header Lines are prefixed with 'PGP'.
  33:      *
  34:      *  The Armor Tail Line is composed in the same manner as the Armor Header
  35:      *  Line, except the string "BEGIN" is replaced by the string "END."
  36:      */
  37: 
  38:     /* Used for signed, encrypted, or compressed files. */
  39:     const ARMOR_MESSAGE = 1;
  40: 
  41:     /* Used for signed files. */
  42:     const ARMOR_SIGNED_MESSAGE = 2;
  43: 
  44:     /* Used for armoring public keys. */
  45:     const ARMOR_PUBLIC_KEY = 3;
  46: 
  47:     /* Used for armoring private keys. */
  48:     const ARMOR_PRIVATE_KEY = 4;
  49: 
  50:     /* Used for detached signatures, PGP/MIME signatures, and natures
  51:      * following clearsigned messages. */
  52:     const ARMOR_SIGNATURE = 5;
  53: 
  54:     /* Regular text contained in an PGP message. */
  55:     const ARMOR_TEXT = 6;
  56: 
  57:     /**
  58:      * Strings in armor header lines used to distinguish between the different
  59:      * types of PGP decryption/encryption.
  60:      *
  61:      * @var array
  62:      */
  63:     protected $_armor = array(
  64:         'MESSAGE' => self::ARMOR_MESSAGE,
  65:         'SIGNED MESSAGE' => self::ARMOR_SIGNED_MESSAGE,
  66:         'PUBLIC KEY BLOCK' => self::ARMOR_PUBLIC_KEY,
  67:         'PRIVATE KEY BLOCK' => self::ARMOR_PRIVATE_KEY,
  68:         'SIGNATURE' => self::ARMOR_SIGNATURE
  69:     );
  70: 
  71:     /* The default public PGP keyserver to use. */
  72:     const KEYSERVER_PUBLIC = 'pgp.mit.edu';
  73: 
  74:     /* The number of times the keyserver refuses connection before an error is
  75:      * returned. */
  76:     const KEYSERVER_REFUSE = 3;
  77: 
  78:     /* The number of seconds that PHP will attempt to connect to the keyserver
  79:      * before it will stop processing the request. */
  80:     const KEYSERVER_TIMEOUT = 10;
  81: 
  82:     /**
  83:      * The list of PGP hash algorithms (from RFC 3156).
  84:      *
  85:      * @var array
  86:      */
  87:     protected $_hashAlg = array(
  88:         1 => 'pgp-md5',
  89:         2 => 'pgp-sha1',
  90:         3 => 'pgp-ripemd160',
  91:         5 => 'pgp-md2',
  92:         6 => 'pgp-tiger192',
  93:         7 => 'pgp-haval-5-160',
  94:         8 => 'pgp-sha256',
  95:         9 => 'pgp-sha384',
  96:         10 => 'pgp-sha512',
  97:         11 => 'pgp-sha224',
  98:     );
  99: 
 100:     /**
 101:      * GnuPG program location/common options.
 102:      *
 103:      * @var array
 104:      */
 105:     protected $_gnupg;
 106: 
 107:     /**
 108:      * Filename of the temporary public keyring.
 109:      *
 110:      * @var string
 111:      */
 112:     protected $_publicKeyring;
 113: 
 114:     /**
 115:      * Filename of the temporary private keyring.
 116:      *
 117:      * @var string
 118:      */
 119:     protected $_privateKeyring;
 120: 
 121:     /**
 122:      * Configuration parameters.
 123:      *
 124:      * @var array
 125:      */
 126:     protected $_params = array();
 127: 
 128:     /**
 129:      * Constructor.
 130:      *
 131:      * @param array $params  The following parameters:
 132:      * <pre>
 133:      * 'program' - (string) [REQUIRED] The path to the GnuPG binary.
 134:      * 'proxy_host - (string) Proxy host.
 135:      * 'proxy_port - (integer) Proxy port.
 136:      * </pre>
 137:      *
 138:      * @throws InvalidArgumentException
 139:      */
 140:     public function __construct($params = array())
 141:     {
 142:         parent::__construct($params);
 143: 
 144:         if (empty($params['program'])) {
 145:             throw new InvalidArgumentException('The location of the GnuPG binary must be given to the Horde_Crypt_Pgp:: class.');
 146:         }
 147: 
 148:         /* Store the location of GnuPG and set common options. */
 149:         $this->_gnupg = array(
 150:             $params['program'],
 151:             '--no-tty',
 152:             '--no-secmem-warning',
 153:             '--no-options',
 154:             '--no-default-keyring',
 155:             '--yes',
 156:             '--homedir ' . $this->_tempdir
 157:         );
 158: 
 159:         if (strncasecmp(PHP_OS, 'WIN', 3)) {
 160:             array_unshift($this->_gnupg, 'LANG= ;');
 161:         }
 162: 
 163:         $this->_params = $params;
 164:     }
 165: 
 166:     /**
 167:      * Generates a personal Public/Private keypair combination.
 168:      *
 169:      * @param string $realname    The name to use for the key.
 170:      * @param string $email       The email to use for the key.
 171:      * @param string $passphrase  The passphrase to use for the key.
 172:      * @param string $comment     The comment to use for the key.
 173:      * @param integer $keylength  The keylength to use for the key.
 174:      * @param integer $expire     The expiration date (UNIX timestamp). No
 175:      *                            expiration if empty (since 1.1.0).
 176:      *
 177:      * @return array  An array consisting of:
 178:      * <pre>
 179:      * Key            Value
 180:      * --------------------------
 181:      * 'public'   =>  Public Key
 182:      * 'private'  =>  Private Key
 183:      * </pre>
 184:      * @throws Horde_Crypt_Exception
 185:      */
 186:     public function generateKey($realname, $email, $passphrase, $comment = '',
 187:                                 $keylength = 1024, $expire = null)
 188:     {
 189:         /* Create temp files to hold the generated keys. */
 190:         $pub_file = $this->_createTempFile('horde-pgp');
 191:         $secret_file = $this->_createTempFile('horde-pgp');
 192: 
 193:         $expire = empty($expire)
 194:             ? 0
 195:             : date('Y-m-d', $expire);
 196: 
 197:         /* Create the config file necessary for GnuPG to run in batch mode. */
 198:         /* TODO: Sanitize input, More user customizable? */
 199:         $input = array(
 200:             '%pubring ' . $pub_file,
 201:             '%secring ' . $secret_file,
 202:             'Key-Type: DSA',
 203:             'Key-Length: 1024',
 204:             'Subkey-Type: ELG-E',
 205:             'Subkey-Length: ' . $keylength,
 206:             'Name-Real: ' . $realname,
 207:             'Name-Email: ' . $email,
 208:             'Expire-Date: ' . $expire,
 209:             'Passphrase: ' . $passphrase
 210:         );
 211:         if (!empty($comment)) {
 212:             $input[] = 'Name-Comment: ' . $comment;
 213:         }
 214:         $input[] = '%commit';
 215: 
 216:         /* Run through gpg binary. */
 217:         $cmdline = array(
 218:             '--gen-key',
 219:             '--batch',
 220:             '--armor'
 221:         );
 222: 
 223:         $result = $this->_callGpg($cmdline, 'w', $input, true, true);
 224: 
 225:         /* Get the keys from the temp files. */
 226:         $public_key = file($pub_file);
 227:         $secret_key = file($secret_file);
 228: 
 229:         /* If either key is empty, something went wrong. */
 230:         if (empty($public_key) || empty($secret_key)) {
 231:             $msg = Horde_Crypt_Translation::t("Public/Private keypair not generated successfully.");
 232:             if (!empty($result->stderr)) {
 233:                 $msg .= ' ' . Horde_Crypt_Translation::t("Returned error message:") . ' ' . $result->stderr;
 234:             }
 235:             throw new Horde_Crypt_Exception($msg);
 236:         }
 237: 
 238:         return array('public' => $public_key, 'private' => $secret_key);
 239:     }
 240: 
 241:     /**
 242:      * Returns information on a PGP data block.
 243:      *
 244:      * @param string $pgpdata  The PGP data block.
 245:      *
 246:      * @return array  An array with information on the PGP data block. If an
 247:      *                element is not present in the data block, it will
 248:      *                likewise not be set in the array.
 249:      * <pre>
 250:      * Array Format:
 251:      * -------------
 252:      * [public_key]/[secret_key] => Array
 253:      *   (
 254:      *     [created] => Key creation - UNIX timestamp
 255:      *     [expires] => Key expiration - UNIX timestamp (0 = never expires)
 256:      *     [size]    => Size of the key in bits
 257:      *   )
 258:      *
 259:      * [keyid] => Key ID of the PGP data (if available)
 260:      *            16-bit hex value (as of Horde 3.2)
 261:      *
 262:      * [signature] => Array (
 263:      *     [id{n}/'_SIGNATURE'] => Array (
 264:      *         [name]        => Full Name
 265:      *         [comment]     => Comment
 266:      *         [email]       => E-mail Address
 267:      *         [keyid]       => 16-bit hex value (as of Horde 3.2)
 268:      *         [created]     => Signature creation - UNIX timestamp
 269:      *         [expires]     => Signature expiration - UNIX timestamp
 270:      *         [micalg]      => The hash used to create the signature
 271:      *         [sig_{hex}]   => Array [details of a sig verifying the ID] (
 272:      *             [created]     => Signature creation - UNIX timestamp
 273:      *             [expires]     => Signature expiration - UNIX timestamp
 274:      *             [keyid]       => 16-bit hex value (as of Horde 3.2)
 275:      *             [micalg]      => The hash used to create the signature
 276:      *         )
 277:      *     )
 278:      * )
 279:      * </pre>
 280:      *
 281:      * Each user ID will be stored in the array 'signature' and have data
 282:      * associated with it, including an array for information on each
 283:      * signature that has signed that UID. Signatures not associated with a
 284:      * UID (e.g. revocation signatures and sub keys) will be stored under the
 285:      * special keyword '_SIGNATURE'.
 286:      *
 287:      * @throws Horde_Crypt_Exception
 288:      */
 289:     public function pgpPacketInformation($pgpdata)
 290:     {
 291:         $data_array = array();
 292:         $keyid = '';
 293:         $header = null;
 294:         $input = $this->_createTempFile('horde-pgp');
 295:         $sig_id = $uid_idx = 0;
 296: 
 297:         /* Store message in temporary file. */
 298:         file_put_contents($input, $pgpdata);
 299: 
 300:         $cmdline = array(
 301:             '--list-packets',
 302:             $input
 303:         );
 304:         $result = $this->_callGpg($cmdline, 'r');
 305: 
 306:         foreach (explode("\n", $result->stdout) as $line) {
 307:             /* Headers are prefaced with a ':' as the first character on the
 308:                line. */
 309:             if (strpos($line, ':') === 0) {
 310:                 $lowerLine = Horde_String::lower($line);
 311: 
 312:                 /* If we have a key (rather than a signature block), get the
 313:                    key's ID */
 314:                 if (strpos($lowerLine, ':public key packet:') !== false ||
 315:                     strpos($lowerLine, ':secret key packet:') !== false) {
 316:                     $cmdline = array(
 317:                         '--with-colons',
 318:                         $input
 319:                     );
 320:                     $data = $this->_callGpg($cmdline, 'r');
 321:                     if (preg_match("/(sec|pub):.*:.*:.*:([A-F0-9]{16}):/", $data->stdout, $matches)) {
 322:                         $keyid = $matches[2];
 323:                     }
 324:                 }
 325: 
 326:                 if (strpos($lowerLine, ':public key packet:') !== false) {
 327:                     $header = 'public_key';
 328:                 } elseif (strpos($lowerLine, ':secret key packet:') !== false) {
 329:                     $header = 'secret_key';
 330:                 } elseif (strpos($lowerLine, ':user id packet:') !== false) {
 331:                     $uid_idx++;
 332:                     $line = preg_replace_callback('/\\\\x([0-9a-f]{2})/', array($this, '_pgpPacketInformationHelper'), $line);
 333:                     if (preg_match("/\"([^\<]+)\<([^\>]+)\>\"/", $line, $matches)) {
 334:                         $header = 'id' . $uid_idx;
 335:                         if (preg_match('/([^\(]+)\((.+)\)$/', trim($matches[1]), $comment_matches)) {
 336:                             $data_array['signature'][$header]['name'] = trim($comment_matches[1]);
 337:                             $data_array['signature'][$header]['comment'] = $comment_matches[2];
 338:                         } else {
 339:                             $data_array['signature'][$header]['name'] = trim($matches[1]);
 340:                             $data_array['signature'][$header]['comment'] = '';
 341:                         }
 342:                         $data_array['signature'][$header]['email'] = $matches[2];
 343:                         $data_array['signature'][$header]['keyid'] = $keyid;
 344:                     }
 345:                 } elseif (strpos($lowerLine, ':signature packet:') !== false) {
 346:                     if (empty($header) || empty($uid_idx)) {
 347:                         $header = '_SIGNATURE';
 348:                     }
 349:                     if (preg_match("/keyid\s+([0-9A-F]+)/i", $line, $matches)) {
 350:                         $sig_id = $matches[1];
 351:                         $data_array['signature'][$header]['sig_' . $sig_id]['keyid'] = $matches[1];
 352:                         $data_array['keyid'] = $matches[1];
 353:                     }
 354:                 } elseif (strpos($lowerLine, ':literal data packet:') !== false) {
 355:                     $header = 'literal';
 356:                 } elseif (strpos($lowerLine, ':encrypted data packet:') !== false) {
 357:                     $header = 'encrypted';
 358:                 } else {
 359:                     $header = null;
 360:                 }
 361:             } else {
 362:                 if ($header == 'secret_key' || $header == 'public_key') {
 363:                     if (preg_match("/created\s+(\d+),\s+expires\s+(\d+)/i", $line, $matches)) {
 364:                         $data_array[$header]['created'] = $matches[1];
 365:                         $data_array[$header]['expires'] = $matches[2];
 366:                     } elseif (preg_match("/\s+[sp]key\[0\]:\s+\[(\d+)/i", $line, $matches)) {
 367:                         $data_array[$header]['size'] = $matches[1];
 368:                     }
 369:                 } elseif ($header == 'literal' || $header == 'encrypted') {
 370:                     $data_array[$header] = true;
 371:                 } elseif ($header) {
 372:                     if (preg_match("/version\s+\d+,\s+created\s+(\d+)/i", $line, $matches)) {
 373:                         $data_array['signature'][$header]['sig_' . $sig_id]['created'] = $matches[1];
 374:                     } elseif (isset($data_array['signature'][$header]['sig_' . $sig_id]['created']) &&
 375:                               preg_match('/expires after (\d+y\d+d\d+h\d+m)\)$/', $line, $matches)) {
 376:                         $expires = $matches[1];
 377:                         preg_match('/^(\d+)y(\d+)d(\d+)h(\d+)m$/', $expires, $matches);
 378:                         list(, $years, $days, $hours, $minutes) = $matches;
 379:                         $data_array['signature'][$header]['sig_' . $sig_id]['expires'] =
 380:                             strtotime('+ ' . $years . ' years + ' . $days . ' days + ' . $hours . ' hours + ' . $minutes . ' minutes', $data_array['signature'][$header]['sig_' . $sig_id]['created']);
 381:                     } elseif (preg_match("/digest algo\s+(\d{1})/", $line, $matches)) {
 382:                         $micalg = $this->_hashAlg[$matches[1]];
 383:                         $data_array['signature'][$header]['sig_' . $sig_id]['micalg'] = $micalg;
 384:                         if ($header == '_SIGNATURE') {
 385:                             /* Likely a signature block, not a key. */
 386:                             $data_array['signature']['_SIGNATURE']['micalg'] = $micalg;
 387:                         }
 388:                         if ($sig_id == $keyid) {
 389:                             /* Self signing signature - we can assume
 390:                              * the micalg value from this signature is
 391:                              * that for the key */
 392:                             $data_array['signature']['_SIGNATURE']['micalg'] = $micalg;
 393:                             $data_array['signature'][$header]['micalg'] = $micalg;
 394:                         }
 395:                     }
 396:                 }
 397:             }
 398:         }
 399: 
 400:         $keyid && $data_array['keyid'] = $keyid;
 401: 
 402:         return $data_array;
 403:     }
 404: 
 405:     /**
 406:      * TODO
 407:      */
 408:     protected function _pgpPacketInformationHelper($a)
 409:     {
 410:         return chr(hexdec($a[1]));
 411:     }
 412: 
 413:     /**
 414:      * Returns human readable information on a PGP key.
 415:      *
 416:      * @param string $pgpdata  The PGP data block.
 417:      *
 418:      * @return string  Tabular information on the PGP key.
 419:      * @throws Horde_Crypt_Exception
 420:      */
 421:     public function pgpPrettyKey($pgpdata)
 422:     {
 423:         $msg = '';
 424:         $packet_info = $this->pgpPacketInformation($pgpdata);
 425:         $fingerprints = $this->getFingerprintsFromKey($pgpdata);
 426: 
 427:         if (!empty($packet_info['signature'])) {
 428:             /* Making the property names the same width for all
 429:              * localizations .*/
 430:             $leftrow = array(
 431:                 Horde_Crypt_Translation::t("Name"),
 432:                 Horde_Crypt_Translation::t("Key Type"),
 433:                 Horde_Crypt_Translation::t("Key Creation"),
 434:                 Horde_Crypt_Translation::t("Expiration Date"),
 435:                 Horde_Crypt_Translation::t("Key Length"),
 436:                 Horde_Crypt_Translation::t("Comment"),
 437:                 Horde_Crypt_Translation::t("E-Mail"),
 438:                 Horde_Crypt_Translation::t("Hash-Algorithm"),
 439:                 Horde_Crypt_Translation::t("Key ID"),
 440:                 Horde_Crypt_Translation::t("Key Fingerprint")
 441:             );
 442:             $leftwidth = array_map('strlen', $leftrow);
 443:             $maxwidth  = max($leftwidth) + 2;
 444:             array_walk($leftrow, array($this, '_pgpPrettyKeyFormatter'), $maxwidth);
 445: 
 446:             foreach ($packet_info['signature'] as $uid_idx => $val) {
 447:                 if ($uid_idx == '_SIGNATURE') {
 448:                     continue;
 449:                 }
 450:                 $key_info = $this->pgpPacketSignatureByUidIndex($pgpdata, $uid_idx);
 451: 
 452:                 $keyid = empty($key_info['keyid'])
 453:                     ? null
 454:                     : $this->_getKeyIDString($key_info['keyid']);
 455:                 $fingerprint = isset($fingerprints[$keyid])
 456:                     ? $fingerprints[$keyid]
 457:                     : null;
 458:                 $sig_key = 'sig_' . $key_info['keyid'];
 459: 
 460:                 $msg .= $leftrow[0] . (isset($key_info['name']) ? stripcslashes($key_info['name']) : '') . "\n"
 461:                     . $leftrow[1] . (($key_info['key_type'] == 'public_key') ? Horde_Crypt_Translation::t("Public Key") : Horde_Crypt_Translation::t("Private Key")) . "\n"
 462:                     . $leftrow[2] . strftime("%D", $val[$sig_key]['created']) . "\n"
 463:                     . $leftrow[3] . (empty($val[$sig_key]['expires']) ? '[' . Horde_Crypt_Translation::t("Never") . ']' : strftime("%D", $val[$sig_key]['expires'])) . "\n"
 464:                     . $leftrow[4] . $key_info['key_size'] . " Bytes\n"
 465:                     . $leftrow[5] . (empty($key_info['comment']) ? '[' . Horde_Crypt_Translation::t("None") . ']' : $key_info['comment']) . "\n"
 466:                     . $leftrow[6] . (empty($key_info['email']) ? '[' . Horde_Crypt_Translation::t("None") . ']' : $key_info['email']) . "\n"
 467:                     . $leftrow[7] . (empty($key_info['micalg']) ? '[' . Horde_Crypt_Translation::t("Unknown") . ']' : $key_info['micalg']) . "\n"
 468:                     . $leftrow[8] . (empty($keyid) ? '[' . Horde_Crypt_Translation::t("Unknown") . ']' : $keyid) . "\n"
 469:                     . $leftrow[9] . (empty($fingerprint) ? '[' . Horde_Crypt_Translation::t("Unknown") . ']' : $fingerprint) . "\n\n";
 470:             }
 471:         }
 472: 
 473:         return $msg;
 474:     }
 475: 
 476:     /**
 477:      * TODO
 478:      */
 479:     protected function _pgpPrettyKeyFormatter(&$s, $k, $m)
 480:     {
 481:         $s .= ':' . str_repeat(' ', $m - Horde_String::length($s));
 482:     }
 483: 
 484:     /**
 485:      * TODO
 486:      */
 487:     protected function _getKeyIDString($keyid)
 488:     {
 489:         /* Get the 8 character key ID string. */
 490:         if (strpos($keyid, '0x') === 0) {
 491:             $keyid = substr($keyid, 2);
 492:         }
 493:         if (strlen($keyid) > 8) {
 494:             $keyid = substr($keyid, -8);
 495:         }
 496:         return '0x' . $keyid;
 497:     }
 498: 
 499:     /**
 500:      * Returns only information on the first ID that matches the email address
 501:      * input.
 502:      *
 503:      * @param string $pgpdata  The PGP data block.
 504:      * @param string $email    An e-mail address.
 505:      *
 506:      * @return array  An array with information on the PGP data block. If an
 507:      *                element is not present in the data block, it will
 508:      *                likewise not be set in the array.
 509:      * <pre>
 510:      * Array Fields:
 511:      * -------------
 512:      * key_created  =>  Key creation - UNIX timestamp
 513:      * key_expires  =>  Key expiration - UNIX timestamp (0 = never expires)
 514:      * key_size     =>  Size of the key in bits
 515:      * key_type     =>  The key type (public_key or secret_key)
 516:      * name         =>  Full Name
 517:      * comment      =>  Comment
 518:      * email        =>  E-mail Address
 519:      * keyid        =>  16-bit hex value
 520:      * created      =>  Signature creation - UNIX timestamp
 521:      * micalg       =>  The hash used to create the signature
 522:      * </pre>
 523:      * @throws Horde_Crypt_Exception
 524:      */
 525:     public function pgpPacketSignature($pgpdata, $email)
 526:     {
 527:         $data = $this->pgpPacketInformation($pgpdata);
 528:         $return_array = array();
 529: 
 530:         /* Check that [signature] key exists. */
 531:         if (!isset($data['signature'])) {
 532:             return $return_array;
 533:         }
 534: 
 535:         /* Store the signature information now. */
 536:         if (($email == '_SIGNATURE') &&
 537:             isset($data['signature']['_SIGNATURE'])) {
 538:             foreach ($data['signature'][$email] as $key => $value) {
 539:                 $return_array[$key] = $value;
 540:             }
 541:         } else {
 542:             $uid_idx = 1;
 543: 
 544:             while (isset($data['signature']['id' . $uid_idx])) {
 545:                 if ($data['signature']['id' . $uid_idx]['email'] == $email) {
 546:                     foreach ($data['signature']['id' . $uid_idx] as $key => $val) {
 547:                         $return_array[$key] = $val;
 548:                     }
 549:                     break;
 550:                 }
 551:                 $uid_idx++;
 552:             }
 553:         }
 554: 
 555:         return $this->_pgpPacketSignature($data, $return_array);
 556:     }
 557: 
 558:     /**
 559:      * Returns information on a PGP signature embedded in PGP data.  Similar
 560:      * to pgpPacketSignature(), but returns information by unique User ID
 561:      * Index (format id{n} where n is an integer of 1 or greater).
 562:      *
 563:      * @param string $pgpdata  See pgpPacketSignature().
 564:      * @param string $uid_idx  The UID index.
 565:      *
 566:      * @return array  See pgpPacketSignature().
 567:      * @throws Horde_Crypt_Exception
 568:      */
 569:     public function pgpPacketSignatureByUidIndex($pgpdata, $uid_idx)
 570:     {
 571:         $data = $this->pgpPacketInformation($pgpdata);
 572:         $return_array = array();
 573: 
 574:         /* Search for the UID index. */
 575:         if (!isset($data['signature']) ||
 576:             !isset($data['signature'][$uid_idx])) {
 577:             return $return_array;
 578:         }
 579: 
 580:         /* Store the signature information now. */
 581:         foreach ($data['signature'][$uid_idx] as $key => $value) {
 582:             $return_array[$key] = $value;
 583:         }
 584: 
 585:         return $this->_pgpPacketSignature($data, $return_array);
 586:     }
 587: 
 588:     /**
 589:      * Adds some data to the pgpPacketSignature*() function array.
 590:      *
 591:      * @param array $data      See pgpPacketSignature().
 592:      * @param array $retarray  The return array.
 593:      *
 594:      * @return array  The return array.
 595:      */
 596:     protected function _pgpPacketSignature($data, $retarray)
 597:     {
 598:         /* If empty, return now. */
 599:         if (empty($retarray)) {
 600:             return $retarray;
 601:         }
 602: 
 603:         $key_type = null;
 604: 
 605:         /* Store any public/private key information. */
 606:         if (isset($data['public_key'])) {
 607:             $key_type = 'public_key';
 608:         } elseif (isset($data['secret_key'])) {
 609:             $key_type = 'secret_key';
 610:         }
 611: 
 612:         if ($key_type) {
 613:             $retarray['key_type'] = $key_type;
 614:             if (isset($data[$key_type]['created'])) {
 615:                 $retarray['key_created'] = $data[$key_type]['created'];
 616:             }
 617:             if (isset($data[$key_type]['expires'])) {
 618:                 $retarray['key_expires'] = $data[$key_type]['expires'];
 619:             }
 620:             if (isset($data[$key_type]['size'])) {
 621:                 $retarray['key_size'] = $data[$key_type]['size'];
 622:             }
 623:         }
 624: 
 625:         return $retarray;
 626:     }
 627: 
 628:     /**
 629:      * Returns the key ID of the key used to sign a block of PGP data.
 630:      *
 631:      * @param string $text  The PGP signed text block.
 632:      *
 633:      * @return string  The key ID of the key used to sign $text.
 634:      * @throws Horde_Crypt_Exception
 635:      */
 636:     public function getSignersKeyID($text)
 637:     {
 638:         $keyid = null;
 639: 
 640:         $input = $this->_createTempFile('horde-pgp');
 641:         file_put_contents($input, $text);
 642: 
 643:         $cmdline = array(
 644:             '--verify',
 645:             $input
 646:         );
 647:         $result = $this->_callGpg($cmdline, 'r', null, true, true);
 648:         if (preg_match('/gpg:\sSignature\smade.*ID\s+([A-F0-9]{8})\s+/', $result->stderr, $matches)) {
 649:             $keyid = $matches[1];
 650:         }
 651: 
 652:         return $keyid;
 653:     }
 654: 
 655:     /**
 656:      * Verify a passphrase for a given public/private keypair.
 657:      *
 658:      * @param string $public_key   The user's PGP public key.
 659:      * @param string $private_key  The user's PGP private key.
 660:      * @param string $passphrase   The user's passphrase.
 661:      *
 662:      * @return boolean  Returns true on valid passphrase, false on invalid
 663:      *                  passphrase.
 664:      * @throws Horde_Crypt_Exception
 665:      */
 666:     public function verifyPassphrase($public_key, $private_key, $passphrase)
 667:     {
 668:         /* Get e-mail address of public key. */
 669:         $key_info = $this->pgpPacketInformation($public_key);
 670:         if (!isset($key_info['signature']['id1']['email'])) {
 671:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not determine the recipient's e-mail address."));
 672:         }
 673: 
 674:         /* Encrypt a test message. */
 675:         try {
 676:             $result = $this->encrypt('Test', array('type' => 'message', 'pubkey' => $public_key, 'recips' => array($key_info['signature']['id1']['email'] => $public_key)));
 677:         } catch (Horde_Crypt_Exception $e) {
 678:             return false;
 679:         }
 680: 
 681:         /* Try to decrypt the message. */
 682:         try {
 683:             $this->decrypt($result, array('type' => 'message', 'pubkey' => $public_key, 'privkey' => $private_key, 'passphrase' => $passphrase));
 684:         } catch (Horde_Crypt_Exception $e) {
 685:             return false;
 686:         }
 687: 
 688:         return true;
 689:     }
 690: 
 691:     /**
 692:      * Parses a message into text and PGP components.
 693:      *
 694:      * @param string $text  The text to parse.
 695:      *
 696:      * @return array  An array with the parsed text, returned in blocks of
 697:      *                text corresponding to their actual order. Keys:
 698:      * <pre>
 699:      * 'type' -  (integer) The type of data contained in block.
 700:      *           Valid types are defined at the top of this class
 701:      *           (the ARMOR_* constants).
 702:      * 'data' - (array) The data for each section. Each line has been stripped
 703:      *          of EOL characters.
 704:      * </pre>
 705:      */
 706:     public function parsePGPData($text)
 707:     {
 708:         $data = array();
 709:         $temp = array(
 710:             'type' => self::ARMOR_TEXT
 711:         );
 712: 
 713:         $buffer = explode("\n", $text);
 714:         while (list(,$val) = each($buffer)) {
 715:             $val = rtrim($val, "\r");
 716:             if (preg_match('/^-----(BEGIN|END) PGP ([^-]+)-----\s*$/', $val, $matches)) {
 717:                 if (isset($temp['data'])) {
 718:                     $data[] = $temp;
 719:                 }
 720:                 $temp = array();
 721: 
 722:                 if ($matches[1] == 'BEGIN') {
 723:                     $temp['type'] = $this->_armor[$matches[2]];
 724:                     $temp['data'][] = $val;
 725:                 } elseif ($matches[1] == 'END') {
 726:                     $temp['type'] = self::ARMOR_TEXT;
 727:                     $data[count($data) - 1]['data'][] = $val;
 728:                 }
 729:             } else {
 730:                 $temp['data'][] = $val;
 731:             }
 732:         }
 733: 
 734:         if (isset($temp['data']) &&
 735:             ((count($temp['data']) > 1) || !empty($temp['data'][0]))) {
 736:             $data[] = $temp;
 737:         }
 738: 
 739:         return $data;
 740:     }
 741: 
 742:     /**
 743:      * Returns a PGP public key from a public keyserver.
 744:      *
 745:      * @param string $keyid    The key ID of the PGP key.
 746:      * @param string $server   The keyserver to use.
 747:      * @param float $timeout   The keyserver timeout.
 748:      * @param string $address  The email address of the PGP key.
 749:      *
 750:      * @return string  The PGP public key.
 751:      * @throws Horde_Crypt_Exception
 752:      */
 753:     public function getPublicKeyserver($keyid,
 754:                                        $server = self::KEYSERVER_PUBLIC,
 755:                                        $timeout = self::KEYSERVER_TIMEOUT,
 756:                                        $address = null)
 757:     {
 758:         if (empty($keyid) && !empty($address)) {
 759:             $keyid = $this->getKeyID($address, $server, $timeout);
 760:         }
 761: 
 762:         /* Connect to the public keyserver. */
 763:         $uri = '/pks/lookup?op=get&search=' . $this->_getKeyIDString($keyid);
 764:         $output = $this->_connectKeyserver('GET', $server, $uri, '', $timeout);
 765: 
 766:         /* Strip HTML Tags from output. */
 767:         if (($start = strstr($output, '-----BEGIN'))) {
 768:             $length = strpos($start, '-----END') + 34;
 769:             return substr($start, 0, $length);
 770:         }
 771: 
 772:         throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not obtain public key from the keyserver."));
 773:     }
 774: 
 775:     /**
 776:      * Sends a PGP public key to a public keyserver.
 777:      *
 778:      * @param string $pubkey  The PGP public key
 779:      * @param string $server  The keyserver to use.
 780:      * @param float $timeout  The keyserver timeout.
 781:      *
 782:      * @throws Horde_Crypt_Exception
 783:      */
 784:     public function putPublicKeyserver($pubkey,
 785:                                        $server = self::KEYSERVER_PUBLIC,
 786:                                        $timeout = self::KEYSERVER_TIMEOUT)
 787:     {
 788:         /* Get the key ID of the public key. */
 789:         $info = $this->pgpPacketInformation($pubkey);
 790: 
 791:         /* See if the public key already exists on the keyserver. */
 792:         try {
 793:             $this->getPublicKeyserver($info['keyid'], $server, $timeout);
 794:         } catch (Horde_Crypt_Exception $e) {
 795:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Key already exists on the public keyserver."));
 796:         }
 797: 
 798:         /* Connect to the public keyserver. _connectKeyserver() */
 799:         $pubkey = 'keytext=' . urlencode(rtrim($pubkey));
 800:         $cmd = array(
 801:             'Host: ' . $server . ':11371',
 802:             'User-Agent: Horde Application Framework',
 803:             'Content-Type: application/x-www-form-urlencoded',
 804:             'Content-Length: ' . strlen($pubkey),
 805:             'Connection: close',
 806:             '',
 807:             $pubkey
 808:         );
 809: 
 810:         return $this->_connectKeyserver('POST', $server, '/pks/add', implode("\r\n", $cmd), $timeout);
 811:     }
 812: 
 813:     /**
 814:      * Returns the first matching key ID for an email address from a
 815:      * public keyserver.
 816:      *
 817:      * @param string $address  The email address of the PGP key.
 818:      * @param string $server   The keyserver to use.
 819:      * @param float $timeout   The keyserver timeout.
 820:      *
 821:      * @return string  The PGP key ID.
 822:      * @throws Horde_Crypt_Exception
 823:      */
 824:     public function getKeyID($address, $server = self::KEYSERVER_PUBLIC,
 825:                              $timeout = self::KEYSERVER_TIMEOUT)
 826:     {
 827:         $pubkey = null;
 828: 
 829:         /* Connect to the public keyserver. */
 830:         $uri = '/pks/lookup?op=index&options=mr&search=' . urlencode($address);
 831:         $output = $this->_connectKeyserver('GET', $server, $uri, '', $timeout);
 832: 
 833:         if (strpos($output, '-----BEGIN PGP PUBLIC KEY BLOCK') !== false) {
 834:             $pubkey = $output;
 835:         } elseif (strpos($output, 'pub:') !== false) {
 836:             $output = explode("\n", $output);
 837:             $keyids = $keyuids = array();
 838:             $curid = null;
 839: 
 840:             foreach ($output as $line) {
 841:                 if (substr($line, 0, 4) == 'pub:') {
 842:                     $line = explode(':', $line);
 843:                     /* Ignore invalid lines and expired keys. */
 844:                     if (count($line) != 7 ||
 845:                         (!empty($line[5]) && $line[5] <= time())) {
 846:                         continue;
 847:                     }
 848:                     $curid = $line[4];
 849:                     $keyids[$curid] = $line[1];
 850:                 } elseif (!is_null($curid) && substr($line, 0, 4) == 'uid:') {
 851:                     preg_match("/>([^>]+)>/", $line, $matches);
 852:                     $keyuids[$curid][] = $matches[1];
 853:                 }
 854:             }
 855: 
 856:             /* Remove keys without a matching UID. */
 857:             foreach ($keyuids as $id => $uids) {
 858:                 $match = false;
 859:                 foreach ($uids as $uid) {
 860:                     if ($uid == $address) {
 861:                         $match = true;
 862:                         break;
 863:                     }
 864:                 }
 865:                 if (!$match) {
 866:                     unset($keyids[$id]);
 867:                 }
 868:             }
 869: 
 870:             /* Sort by timestamp to use the newest key. */
 871:             if (count($keyids)) {
 872:                 ksort($keyids);
 873:                 $pubkey = $this->getPublicKeyserver(array_pop($keyids), $server, $timeout);
 874:             }
 875:         }
 876: 
 877:         if ($pubkey) {
 878:             $sig = $this->pgpPacketSignature($pubkey, $address);
 879:             if (!empty($sig['keyid']) &&
 880:                 (empty($sig['public_key']['expires']) ||
 881:                  $sig['public_key']['expires'] > time())) {
 882:                 return substr($this->_getKeyIDString($sig['keyid']), 2);
 883:             }
 884:         }
 885: 
 886:         throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not obtain public key from the keyserver."));
 887:     }
 888: 
 889:     /**
 890:      * Get the fingerprints from a key block.
 891:      *
 892:      * @param string $pgpdata  The PGP data block.
 893:      *
 894:      * @return array  The fingerprints in $pgpdata indexed by key id.
 895:      * @throws Horde_Crypt_Exception
 896:      */
 897:     public function getFingerprintsFromKey($pgpdata)
 898:     {
 899:         $fingerprints = array();
 900: 
 901:         /* Store the key in a temporary keyring. */
 902:         $keyring = $this->_putInKeyring($pgpdata);
 903: 
 904:         /* Options for the GPG binary. */
 905:         $cmdline = array(
 906:             '--fingerprint',
 907:             $keyring,
 908:         );
 909: 
 910:         $result = $this->_callGpg($cmdline, 'r');
 911:         if (!$result || !$result->stdout) {
 912:             return $fingerprints;
 913:         }
 914: 
 915:         /* Parse fingerprints and key ids from output. */
 916:         $lines = explode("\n", $result->stdout);
 917:         $keyid = null;
 918:         foreach ($lines as $line) {
 919:             if (preg_match('/pub\s+\w+\/(\w{8})/', $line, $matches)) {
 920:                 $keyid = '0x' . $matches[1];
 921:             } elseif ($keyid && preg_match('/^\s+[\s\w]+=\s*([\w\s]+)$/m', $line, $matches)) {
 922:                 $fingerprints[$keyid] = trim($matches[1]);
 923:                 $keyid = null;
 924:             }
 925:         }
 926: 
 927:         return $fingerprints;
 928:     }
 929: 
 930:     /**
 931:      * Connects to a public key server via HKP (Horrowitz Keyserver Protocol).
 932:      * http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
 933:      *
 934:      * @param string $method    POST, GET, etc.
 935:      * @param string $server    The keyserver to use.
 936:      * @param string $resource  The URI to access (relative to the server).
 937:      * @param string $command   The PGP command to run.
 938:      * @param float $timeout    The timeout value.
 939:      *
 940:      * @return string  The text from standard output on success.
 941:      * @throws Horde_Crypt_Exception
 942:      */
 943:     protected function _connectKeyserver($method, $server, $resource,
 944:                                          $command, $timeout)
 945:     {
 946:         $connRefuse = 0;
 947:         $output = '';
 948: 
 949:         $port = '11371';
 950:         if (!empty($this->_params['proxy_host'])) {
 951:             $resource = 'http://' . $server . ':' . $port . $resource;
 952: 
 953:             $server = $this->_params['proxy_host'];
 954:             $port = isset($this->_params['proxy_port'])
 955:                 ? $this->_params['proxy_port']
 956:                 : 80;
 957:         }
 958: 
 959:         $command = $method . ' ' . $resource . ' HTTP/1.0' . ($command ? "\r\n" . $command : '');
 960: 
 961:         /* Attempt to get the key from the keyserver. */
 962:         do {
 963:             $errno = $errstr = null;
 964: 
 965:             /* The HKP server is located on port 11371. */
 966:             $fp = @fsockopen($server, $port, $errno, $errstr, $timeout);
 967:             if ($fp) {
 968:                 fputs($fp, $command . "\n\n");
 969:                 while (!feof($fp)) {
 970:                     $output .= fgets($fp, 1024);
 971:                 }
 972:                 fclose($fp);
 973:                 return $output;
 974:             }
 975:         } while (++$connRefuse < self::KEYSERVER_REFUSE);
 976: 
 977:         if ($errno == 0) {
 978:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Connection refused to the public keyserver."));
 979:         } else {
 980:             $charset = 'UTF-8';
 981:             $lang_charset = setlocale(LC_ALL, 0);
 982:             if ((strpos($lang_charset, ';') === false) &&
 983:                 (strpos($lang_charset, '/') === false)) {
 984:                 $lang_charset = explode('.', $lang_charset);
 985:                 if ((count($lang_charset) == 2) && !empty($lang_charset[1])) {
 986:                     $charset = $lang_charset[1];
 987:                 }
 988:             }
 989:             throw new Horde_Crypt_Exception(sprintf(Horde_Crypt_Translation::t("Connection refused to the public keyserver. Reason: %s (%s)"), Horde_String::convertCharset($errstr, $charset, 'UTF-8'), $errno));
 990:         }
 991:     }
 992: 
 993:     /**
 994:      * Encrypts text using PGP.
 995:      *
 996:      * @param string $text   The text to be PGP encrypted.
 997:      * @param array $params  The parameters needed for encryption.
 998:      *                       See the individual _encrypt*() functions for the
 999:      *                       parameter requirements.
1000:      *
1001:      * @return string  The encrypted message.
1002:      * @throws Horde_Crypt_Exception
1003:      */
1004:     public function encrypt($text, $params = array())
1005:     {
1006:         if (isset($params['type'])) {
1007:             if ($params['type'] === 'message') {
1008:                 return $this->_encryptMessage($text, $params);
1009:             } elseif ($params['type'] === 'signature') {
1010:                 return $this->_encryptSignature($text, $params);
1011:             }
1012:         }
1013:     }
1014: 
1015:     /**
1016:      * Decrypts text using PGP.
1017:      *
1018:      * @param string $text   The text to be PGP decrypted.
1019:      * @param array $params  The parameters needed for decryption.
1020:      *                       See the individual _decrypt*() functions for the
1021:      *                       parameter requirements.
1022:      *
1023:      * @return stdClass  An object with the following properties:
1024:      * <pre>
1025:      * 'message' - (string) The signature result text.
1026:      * 'result' - (boolean) The result of the signature test.
1027:      * </pre>
1028:      * @throws Horde_Crypt_Exception
1029:      */
1030:     public function decrypt($text, $params = array())
1031:     {
1032:         if (isset($params['type'])) {
1033:             if ($params['type'] === 'message') {
1034:                 return $this->_decryptMessage($text, $params);
1035:             } elseif (($params['type'] === 'signature') ||
1036:                       ($params['type'] === 'detached-signature')) {
1037:                 return $this->_decryptSignature($text, $params);
1038:             }
1039:         }
1040:     }
1041: 
1042:     /**
1043:      * Returns whether a text has been encrypted symmetrically.
1044:      *
1045:      * @param string $text  The PGP encrypted text.
1046:      *
1047:      * @return boolean  True if the text is symmetricallly encrypted.
1048:      * @throws Horde_Crypt_Exception
1049:      */
1050:     public function encryptedSymmetrically($text)
1051:     {
1052:         $cmdline = array(
1053:             '--decrypt',
1054:             '--batch'
1055:         );
1056:         $result = $this->_callGpg($cmdline, 'w', $text, true, true, true);
1057:         return strpos($result->stderr, 'gpg: encrypted with 1 passphrase') !== false;
1058:     }
1059: 
1060:     /**
1061:      * Creates a temporary gpg keyring.
1062:      *
1063:      * @param string $type  The type of key to analyze. Either 'public'
1064:      *                      (Default) or 'private'
1065:      *
1066:      * @return string  Command line keystring option to use with gpg program.
1067:      */
1068:     protected function _createKeyring($type = 'public')
1069:     {
1070:         $type = Horde_String::lower($type);
1071: 
1072:         if ($type === 'public') {
1073:             if (empty($this->_publicKeyring)) {
1074:                 $this->_publicKeyring = $this->_createTempFile('horde-pgp');
1075:             }
1076:             return '--keyring ' . $this->_publicKeyring;
1077:         } elseif ($type === 'private') {
1078:             if (empty($this->_privateKeyring)) {
1079:                 $this->_privateKeyring = $this->_createTempFile('horde-pgp');
1080:             }
1081:             return '--secret-keyring ' . $this->_privateKeyring;
1082:         }
1083:     }
1084: 
1085:     /**
1086:      * Adds PGP keys to the keyring.
1087:      *
1088:      * @param mixed $keys   A single key or an array of key(s) to add to the
1089:      *                      keyring.
1090:      * @param string $type  The type of key(s) to add. Either 'public'
1091:      *                      (Default) or 'private'
1092:      *
1093:      * @return string  Command line keystring option to use with gpg program.
1094:      * @throws Horde_Crypt_Exception
1095:      */
1096:     protected function _putInKeyring($keys = array(), $type = 'public')
1097:     {
1098:         $type = Horde_String::lower($type);
1099: 
1100:         if (!is_array($keys)) {
1101:             $keys = array($keys);
1102:         }
1103: 
1104:         /* Create the keyrings if they don't already exist. */
1105:         $keyring = $this->_createKeyring($type);
1106: 
1107:         /* Store the key(s) in the keyring. */
1108:         $cmdline = array(
1109:             '--allow-secret-key-import',
1110:             '--fast-import',
1111:             $keyring
1112:         );
1113:         $this->_callGpg($cmdline, 'w', array_values($keys));
1114: 
1115:         return $keyring;
1116:     }
1117: 
1118:     /**
1119:      * Encrypts a message in PGP format using a public key.
1120:      *
1121:      * @param string $text   The text to be encrypted.
1122:      * @param array $params  The parameters needed for encryption.
1123:      * <pre>
1124:      * Parameters:
1125:      * ===========
1126:      * 'type'       => 'message' (REQUIRED)
1127:      * 'symmetric'  => Whether to use symmetric instead of asymmetric
1128:      *                 encryption (defaults to false)
1129:      * 'recips'     => An array with the e-mail address of the recipient as
1130:      *                 the key and that person's public key as the value.
1131:      *                 (REQUIRED if 'symmetric' is false)
1132:      * 'passphrase' => The passphrase for the symmetric encryption (REQUIRED if
1133:      *                 'symmetric' is true)
1134:      * </pre>
1135:      *
1136:      * @return string  The encrypted message.
1137:      * @throws Horde_Crypt_Exception
1138:      */
1139:     protected function _encryptMessage($text, $params)
1140:     {
1141:         /* Create temp files for input. */
1142:         $input = $this->_createTempFile('horde-pgp');
1143:         file_put_contents($input, $text);
1144: 
1145:         /* Build command line. */
1146:         $cmdline = array(
1147:             '--armor',
1148:             '--batch',
1149:             '--always-trust'
1150:         );
1151: 
1152:         if (empty($params['symmetric'])) {
1153:             /* Store public key in temporary keyring. */
1154:             $keyring = $this->_putInKeyring(array_values($params['recips']));
1155: 
1156:             $cmdline[] = $keyring;
1157:             $cmdline[] = '--encrypt';
1158:             foreach (array_keys($params['recips']) as $val) {
1159:                 $cmdline[] = '--recipient ' . $val;
1160:             }
1161:         } else {
1162:             $cmdline[] = '--symmetric';
1163:             $cmdline[] = '--passphrase-fd 0';
1164:         }
1165:         $cmdline[] = $input;
1166: 
1167:         /* Encrypt the document. */
1168:         $result = $this->_callGpg($cmdline, 'w', empty($params['symmetric']) ? null : $params['passphrase'], true, true);
1169:         if (empty($result->output)) {
1170:             $error = preg_replace('/\n.*/', '', $result->stderr);
1171:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not PGP encrypt message: ") . $error);
1172:         }
1173: 
1174:         return $result->output;
1175:     }
1176: 
1177:     /**
1178:      * Signs a message in PGP format using a private key.
1179:      *
1180:      * @param string $text   The text to be signed.
1181:      * @param array $params  The parameters needed for signing.
1182:      * <pre>
1183:      * Parameters:
1184:      * ===========
1185:      * 'type'        =>  'signature' (REQUIRED)
1186:      * 'pubkey'      =>  PGP public key. (REQUIRED)
1187:      * 'privkey'     =>  PGP private key. (REQUIRED)
1188:      * 'passphrase'  =>  Passphrase for PGP Key. (REQUIRED)
1189:      * 'sigtype'     =>  Determine the signature type to use. (Optional)
1190:      *                   'cleartext'  --  Make a clear text signature
1191:      *                   'detach'     --  Make a detached signature (DEFAULT)
1192:      * </pre>
1193:      *
1194:      * @return string  The signed message.
1195:      * @throws Horde_Crypt_Exception
1196:      */
1197:     protected function _encryptSignature($text, $params)
1198:     {
1199:         /* Check for required parameters. */
1200:         if (!isset($params['pubkey']) ||
1201:             !isset($params['privkey']) ||
1202:             !isset($params['passphrase'])) {
1203:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public PGP key, private PGP key, and passphrase are required to sign a message."));
1204:         }
1205: 
1206:         /* Create temp files for input. */
1207:         $input = $this->_createTempFile('horde-pgp');
1208: 
1209:         /* Encryption requires both keyrings. */
1210:         $pub_keyring = $this->_putInKeyring(array($params['pubkey']));
1211:         $sec_keyring = $this->_putInKeyring(array($params['privkey']), 'private');
1212: 
1213:         /* Store message in temporary file. */
1214:         file_put_contents($input, $text);
1215: 
1216:         /* Determine the signature type to use. */
1217:         $cmdline = array();
1218:         if (isset($params['sigtype']) &&
1219:             $params['sigtype'] == 'cleartext') {
1220:             $sign_type = '--clearsign';
1221:         } else {
1222:             $sign_type = '--detach-sign';
1223:         }
1224: 
1225:         /* Additional GPG options. */
1226:         $cmdline += array(
1227:             '--armor',
1228:             '--batch',
1229:             '--passphrase-fd 0',
1230:             $sec_keyring,
1231:             $pub_keyring,
1232:             $sign_type,
1233:             $input
1234:         );
1235: 
1236:         /* Sign the document. */
1237:         $result = $this->_callGpg($cmdline, 'w', $params['passphrase'], true, true);
1238:         if (empty($result->output)) {
1239:             $error = preg_replace('/\n.*/', '', $result->stderr);
1240:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not PGP sign message: ") . $error);
1241:         }
1242: 
1243:         return $result->output;
1244:     }
1245: 
1246:     /**
1247:      * Decrypts an PGP encrypted message using a private/public keypair and a
1248:      * passhprase.
1249:      *
1250:      * @param string $text   The text to be decrypted.
1251:      * @param array $params  The parameters needed for decryption.
1252:      * <pre>
1253:      * Parameters:
1254:      * ===========
1255:      * 'type'        =>  'message' (REQUIRED)
1256:      * 'pubkey'      =>  PGP public key. (REQUIRED for asymmetric encryption)
1257:      * 'privkey'     =>  PGP private key. (REQUIRED for asymmetric encryption)
1258:      * 'passphrase'  =>  Passphrase for PGP Key. (REQUIRED)
1259:      * </pre>
1260:      *
1261:      * @return stdClass  An object with the following properties:
1262:      * <pre>
1263:      * 'message' - (string) The signature result text.
1264:      * 'result' - (boolean) The result of the signature test.
1265:      * </pre>
1266:      * @throws Horde_Crypt_Exception
1267:      */
1268:     protected function _decryptMessage($text, $params)
1269:     {
1270:         /* Check for required parameters. */
1271:         if (!isset($params['passphrase']) && empty($params['no_passphrase'])) {
1272:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A passphrase is required to decrypt a message."));
1273:         }
1274: 
1275:         /* Create temp files. */
1276:         $input = $this->_createTempFile('horde-pgp');
1277: 
1278:         /* Store message in file. */
1279:         file_put_contents($input, $text);
1280: 
1281:         /* Build command line. */
1282:         $cmdline = array(
1283:             '--always-trust',
1284:             '--armor',
1285:             '--batch'
1286:         );
1287:         if (empty($params['no_passphrase'])) {
1288:             $cmdline[] = '--passphrase-fd 0';
1289:         }
1290:         if (!empty($params['pubkey']) && !empty($params['privkey'])) {
1291:             /* Decryption requires both keyrings. */
1292:             $pub_keyring = $this->_putInKeyring(array($params['pubkey']));
1293:             $sec_keyring = $this->_putInKeyring(array($params['privkey']), 'private');
1294:             $cmdline[] = $sec_keyring;
1295:             $cmdline[] = $pub_keyring;
1296:         }
1297:         $cmdline[] = '--decrypt';
1298:         $cmdline[] = $input;
1299: 
1300:         /* Decrypt the document now. */
1301:         $language = setlocale(LC_MESSAGES, 0);
1302:         setlocale(LC_MESSAGES, 'C');
1303:         if (empty($params['no_passphrase'])) {
1304:             $result = $this->_callGpg($cmdline, 'w', $params['passphrase'], true, true);
1305:         } else {
1306:             $result = $this->_callGpg($cmdline, 'r', null, true, true);
1307:         }
1308:         setlocale(LC_MESSAGES, $language);
1309:         if (empty($result->output)) {
1310:             $error = preg_replace('/\n.*/', '', $result->stderr);
1311:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not decrypt PGP data: ") . $error);
1312:         }
1313: 
1314:         /* Create the return object. */
1315:         return $this->_checkSignatureResult($result->stderr, $result->output);
1316:     }
1317: 
1318:     /**
1319:      * Decrypts an PGP signed message using a public key.
1320:      *
1321:      * @param string $text   The text to be verified.
1322:      * @param array $params  The parameters needed for verification.
1323:      * <pre>
1324:      * Parameters:
1325:      * ===========
1326:      * 'type'       =>  'signature' or 'detached-signature' (REQUIRED)
1327:      * 'pubkey'     =>  PGP public key. (REQUIRED)
1328:      * 'signature'  =>  PGP signature block. (REQUIRED for detached signature)
1329:      * 'charset'    =>  charset of the message body (OPTIONAL)
1330:      * </pre>
1331:      *
1332:      * @return stdClass  An object with the following properties:
1333:      * <pre>
1334:      * 'message' - (string) The signature result text.
1335:      * 'result' - (boolean) The result of the signature test.
1336:      * </pre>
1337:      * @throws Horde_Crypt_Exception
1338:      */
1339:     protected function _decryptSignature($text, $params)
1340:     {
1341:         /* Check for required parameters. */
1342:         if (!isset($params['pubkey'])) {
1343:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public PGP key is required to verify a signed message."));
1344:         }
1345:         if (($params['type'] === 'detached-signature') &&
1346:             !isset($params['signature'])) {
1347:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("The detached PGP signature block is required to verify the signed message."));
1348:         }
1349: 
1350:         /* Create temp files for input. */
1351:         $input = $this->_createTempFile('horde-pgp');
1352: 
1353:         /* Store public key in temporary keyring. */
1354:         $keyring = $this->_putInKeyring($params['pubkey']);
1355: 
1356:         /* Store the message in a temporary file. */
1357:         file_put_contents($input, $text);
1358: 
1359:         /* Options for the GPG binary. */
1360:         $cmdline = array(
1361:             '--armor',
1362:             '--always-trust',
1363:             '--batch',
1364:             '--charset ' . (isset($params['charset']) ? $params['charset'] : 'UTF-8'),
1365:             $keyring,
1366:             '--verify'
1367:         );
1368: 
1369:         /* Extra stuff to do if we are using a detached signature. */
1370:         if ($params['type'] === 'detached-signature') {
1371:             $sigfile = $this->_createTempFile('horde-pgp');
1372:             $cmdline[] = $sigfile . ' ' . $input;
1373:             file_put_contents($sigfile, $params['signature']);
1374:         } else {
1375:             $cmdline[] = $input;
1376:         }
1377: 
1378:         /* Verify the signature.  We need to catch standard error output,
1379:          * since this is where the signature information is sent. */
1380:         $language = setlocale(LC_MESSAGES, 0);
1381:         setlocale(LC_MESSAGES, 'C');
1382:         $result = $this->_callGpg($cmdline, 'r', null, true, true);
1383:         setlocale(LC_MESSAGES, $language);
1384:         return $this->_checkSignatureResult($result->stderr, $result->stderr);
1385:     }
1386: 
1387:     /**
1388:      * Checks signature result from the GnuPG binary.
1389:      *
1390:      * @param string $result   The signature result.
1391:      * @param string $message  The decrypted message data.
1392:      *
1393:      * @return stdClass  An object with the following properties:
1394:      *   - message: (string) The signature result text.
1395:      *   - result: (string) The result of the signature test.
1396:      *
1397:      * @throws Horde_Crypt_Exception
1398:      */
1399:     protected function _checkSignatureResult($result, $message = null)
1400:     {
1401:         /* Good signature:
1402:          *   gpg: Good signature from "blah blah blah (Comment)"
1403:          * Bad signature:
1404:          *   gpg: BAD signature from "blah blah blah (Comment)" */
1405:         if (strpos($result, 'gpg: BAD signature') !== false) {
1406:             throw new Horde_Crypt_Exception($result);
1407:         }
1408: 
1409:         $ob = new stdClass;
1410:         $ob->message = $message;
1411:         $ob->result = $result;
1412: 
1413:         return $ob;
1414:     }
1415: 
1416:     /**
1417:      * Signs a MIME part using PGP.
1418:      *
1419:      * @param Horde_Mime_Part $mime_part  The object to sign.
1420:      * @param array $params               The parameters required for signing.
1421:      *                                    @see _encryptSignature().
1422:      *
1423:      * @return mixed  A Horde_Mime_Part object that is signed according to RFC
1424:      *                3156.
1425:      * @throws Horde_Crypt_Exception
1426:      */
1427:     public function signMIMEPart($mime_part, $params = array())
1428:     {
1429:         $params = array_merge($params, array('type' => 'signature', 'sigtype' => 'detach'));
1430: 
1431:         /* RFC 3156 Requirements for a PGP signed message:
1432:          * + Content-Type params 'micalg' & 'protocol' are REQUIRED.
1433:          * + The digitally signed message MUST be constrained to 7 bits.
1434:          * + The MIME headers MUST be a part of the signed data. */
1435:         $msg_sign = $this->encrypt($mime_part->toString(array('headers' => true, 'canonical' => true, 'encode' => Horde_Mime_Part::ENCODE_7BIT)), $params);
1436: 
1437:         /* Add the PGP signature. */
1438:         $pgp_sign = new Horde_Mime_Part();
1439:         $pgp_sign->setType('application/pgp-signature');
1440:         $pgp_sign->setHeaderCharset('UTF-8');
1441:         $pgp_sign->setDisposition('inline');
1442:         $pgp_sign->setDescription(Horde_Crypt_Translation::t("PGP Digital Signature"));
1443:         $pgp_sign->setContents($msg_sign, array('encoding' => '7bit'));
1444: 
1445:         /* Get the algorithim information from the signature. Since we are
1446:          * analyzing a signature packet, we need to use the special keyword
1447:          * '_SIGNATURE' - see Horde_Crypt_Pgp. */
1448:         $sig_info = $this->pgpPacketSignature($msg_sign, '_SIGNATURE');
1449: 
1450:         /* Setup the multipart MIME Part. */
1451:         $part = new Horde_Mime_Part();
1452:         $part->setType('multipart/signed');
1453:         $part->setContents("This message is in MIME format and has been PGP signed.\n");
1454:         $part->addPart($mime_part);
1455:         $part->addPart($pgp_sign);
1456:         $part->setContentTypeParameter('protocol', 'application/pgp-signature');
1457:         $part->setContentTypeParameter('micalg', $sig_info['micalg']);
1458: 
1459:         return $part;
1460:     }
1461: 
1462:     /**
1463:      * Encrypts a MIME part using PGP.
1464:      *
1465:      * @param Horde_Mime_Part $mime_part  The object to encrypt.
1466:      * @param array $params               The parameters required for
1467:      *                                    encryption.
1468:      *                                    @see _encryptMessage().
1469:      *
1470:      * @return mixed  A Horde_Mime_Part object that is encrypted according to
1471:      *                RFC 3156.
1472:      * @throws Horde_Crypt_Exception
1473:      */
1474:     public function encryptMIMEPart($mime_part, $params = array())
1475:     {
1476:         $params = array_merge($params, array('type' => 'message'));
1477: 
1478:         $signenc_body = $mime_part->toString(array('headers' => true, 'canonical' => true));
1479:         $message_encrypt = $this->encrypt($signenc_body, $params);
1480: 
1481:         /* Set up MIME Structure according to RFC 3156. */
1482:         $part = new Horde_Mime_Part();
1483:         $part->setType('multipart/encrypted');
1484:         $part->setHeaderCharset('UTF-8');
1485:         $part->setContentTypeParameter('protocol', 'application/pgp-encrypted');
1486:         $part->setDescription(Horde_Crypt_Translation::t("PGP Encrypted Data"));
1487:         $part->setContents("This message is in MIME format and has been PGP encrypted.\n");
1488: 
1489:         $part1 = new Horde_Mime_Part();
1490:         $part1->setType('application/pgp-encrypted');
1491:         $part1->setCharset(null);
1492:         $part1->setContents("Version: 1\n", array('encoding' => '7bit'));
1493:         $part->addPart($part1);
1494: 
1495:         $part2 = new Horde_Mime_Part();
1496:         $part2->setType('application/octet-stream');
1497:         $part2->setCharset(null);
1498:         $part2->setContents($message_encrypt, array('encoding' => '7bit'));
1499:         $part2->setDisposition('inline');
1500:         $part->addPart($part2);
1501: 
1502:         return $part;
1503:     }
1504: 
1505:     /**
1506:      * Signs and encrypts a MIME part using PGP.
1507:      *
1508:      * @param Horde_Mime_Part $mime_part   The object to sign and encrypt.
1509:      * @param array $sign_params           The parameters required for
1510:      *                                     signing. @see _encryptSignature().
1511:      * @param array $encrypt_params        The parameters required for
1512:      *                                     encryption. @see _encryptMessage().
1513:      *
1514:      * @return mixed  A Horde_Mime_Part object that is signed and encrypted
1515:      *                according to RFC 3156.
1516:      * @throws Horde_Crypt_Exception
1517:      */
1518:     public function signAndEncryptMIMEPart($mime_part, $sign_params = array(),
1519:                                            $encrypt_params = array())
1520:     {
1521:         /* RFC 3156 requires that the entire signed message be encrypted.  We
1522:          * need to explicitly call using Horde_Crypt_Pgp:: because we don't
1523:          * know whether a subclass has extended these methods. */
1524:         $part = $this->signMIMEPart($mime_part, $sign_params);
1525:         $part = $this->encryptMIMEPart($part, $encrypt_params);
1526:         $part->setContents("This message is in MIME format and has been PGP signed and encrypted.\n");
1527: 
1528:         $part->setCharset($this->_params['email_charset']);
1529:         $part->setDescription(Horde_String::convertCharset(Horde_Crypt_Translation::t("PGP Signed/Encrypted Data"), 'UTF-8', $this->_params['email_charset']));
1530: 
1531:         return $part;
1532:     }
1533: 
1534:     /**
1535:      * Generates a Horde_Mime_Part object, in accordance with RFC 3156, that
1536:      * contains a public key.
1537:      *
1538:      * @param string $key  The public key.
1539:      *
1540:      * @return Horde_Mime_Part  An object that contains the public key.
1541:      */
1542:     public function publicKeyMIMEPart($key)
1543:     {
1544:         $part = new Horde_Mime_Part();
1545:         $part->setType('application/pgp-keys');
1546:         $part->setHeaderCharset('UTF-8');
1547:         $part->setDescription(Horde_Crypt_Translation::t("PGP Public Key"));
1548:         $part->setContents($key, array('encoding' => '7bit'));
1549: 
1550:         return $part;
1551:     }
1552: 
1553:     /**
1554:      * Function that handles interfacing with the GnuPG binary.
1555:      *
1556:      * @param array $options    Options and commands to pass to GnuPG.
1557:      * @param string $mode      'r' to read from stdout, 'w' to write to
1558:      *                          stdin.
1559:      * @param array $input      Input to write to stdin.
1560:      * @param boolean $output   Collect and store output in object returned?
1561:      * @param boolean $stderr   Collect and store stderr in object returned?
1562:      * @param boolean $verbose  Run GnuPG with verbose flag?
1563:      *
1564:      * @return stdClass  Class with members output, stderr, and stdout.
1565:      * @throws Horde_Crypt_Exception
1566:      */
1567:     protected function _callGpg($options, $mode, $input = array(),
1568:                                 $output = false, $stderr = false,
1569:                                 $verbose = false)
1570:     {
1571:         $data = new stdClass;
1572:         $data->output = null;
1573:         $data->stderr = null;
1574:         $data->stdout = null;
1575: 
1576:         /* Verbose output? */
1577:         if (!$verbose) {
1578:             array_unshift($options, '--quiet');
1579:         }
1580: 
1581:         /* Create temp files for output. */
1582:         if ($output) {
1583:             $output_file = $this->_createTempFile('horde-pgp', false);
1584:             array_unshift($options, '--output ' . $output_file);
1585: 
1586:             /* Do we need standard error output? */
1587:             if ($stderr) {
1588:                 $stderr_file = $this->_createTempFile('horde-pgp', false);
1589:                 $options[] = '2> ' . $stderr_file;
1590:             }
1591:         }
1592: 
1593:         /* Silence errors if not requested. */
1594:         if (!$output || !$stderr) {
1595:             $options[] = '2> /dev/null';
1596:         }
1597: 
1598:         /* Build the command line string now. */
1599:         $cmdline = implode(' ', array_merge($this->_gnupg, $options));
1600: 
1601:         if ($mode == 'w') {
1602:             if ($fp = popen($cmdline, 'w')) {
1603:                 $win32 = !strncasecmp(PHP_OS, 'WIN', 3);
1604: 
1605:                 if (!is_array($input)) {
1606:                     $input = array($input);
1607:                 }
1608: 
1609:                 foreach ($input as $line) {
1610:                     if ($win32 && (strpos($line, "\x0d\x0a") !== false)) {
1611:                         $chunks = explode("\x0d\x0a", $line);
1612:                         foreach ($chunks as $chunk) {
1613:                             fputs($fp, $chunk . "\n");
1614:                         }
1615:                     } else {
1616:                         fputs($fp, $line . "\n");
1617:                     }
1618:                 }
1619:             } else {
1620:                 throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to pgp binary."));
1621:             }
1622:         } elseif ($mode == 'r') {
1623:             if ($fp = popen($cmdline, 'r')) {
1624:                 while (!feof($fp)) {
1625:                     $data->stdout .= fgets($fp, 1024);
1626:                 }
1627:             } else {
1628:                 throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to pgp binary."));
1629:             }
1630:         }
1631:         pclose($fp);
1632: 
1633:         if ($output) {
1634:             $data->output = file_get_contents($output_file);
1635:             unlink($output_file);
1636:             if ($stderr) {
1637:                 $data->stderr = file_get_contents($stderr_file);
1638:                 unlink($stderr_file);
1639:             }
1640:         }
1641: 
1642:         return $data;
1643:     }
1644: 
1645:     /**
1646:      * Generates a revocation certificate.
1647:      *
1648:      * @param string $key         The private key.
1649:      * @param string $email       The email to use for the key.
1650:      * @param string $passphrase  The passphrase to use for the key.
1651:      *
1652:      * @return string  The revocation certificate.
1653:      * @throws Horde_Crypt_Exception
1654:      */
1655:     public function generateRevocation($key, $email, $passphrase)
1656:     {
1657:         $keyring = $this->_putInKeyring($key, 'private');
1658: 
1659:         /* Prepare the canned answers. */
1660:         $input = array(
1661:             'y', // Really generate a revocation certificate
1662:             '0', // Refuse to specify a reason
1663:             '',  // Empty comment
1664:             'y', // Confirm empty comment
1665:         );
1666:         if (!empty($passphrase)) {
1667:             $input[] = $passphrase;
1668:         }
1669: 
1670:         /* Run through gpg binary. */
1671:         $cmdline = array(
1672:             $keyring,
1673:             '--command-fd 0',
1674:             '--gen-revoke ' . $email,
1675:         );
1676:         $results = $this->_callGpg($cmdline, 'w', $input, true);
1677: 
1678:         /* If the key is empty, something went wrong. */
1679:         if (empty($results->output)) {
1680:             throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Revocation key not generated successfully."));
1681:         }
1682: 
1683:         return $results->output;
1684:     }
1685: 
1686: }
1687: 
API documentation generated by ApiGen