From 3e98f8be718578644bb15ee6a992a875f6468e8f Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Fri, 27 Dec 2013 13:14:40 +0100 Subject: Add some code for S/MIME signatures verification, update Crypt_GPG package --- program/lib/Crypt/GPG.php | 812 ++++++++----------- program/lib/Crypt/GPG/ByteUtils.php | 105 +++ program/lib/Crypt/GPG/DecryptStatusHandler.php | 40 +- program/lib/Crypt/GPG/Engine.php | 560 +++++++++---- program/lib/Crypt/GPG/Exceptions.php | 129 ++- program/lib/Crypt/GPG/Key.php | 2 +- program/lib/Crypt/GPG/KeyGenerator.php | 790 +++++++++++++++++++ program/lib/Crypt/GPG/KeyGeneratorErrorHandler.php | 121 +++ .../lib/Crypt/GPG/KeyGeneratorStatusHandler.php | 173 ++++ program/lib/Crypt/GPG/PinEntry.php | 875 +++++++++++++++++++++ program/lib/Crypt/GPG/ProcessControl.php | 150 ++++ program/lib/Crypt/GPG/Signature.php | 9 +- program/lib/Crypt/GPG/SubKey.php | 27 +- program/lib/Crypt/GPG/UserId.php | 2 +- program/lib/Crypt/GPG/VerifyStatusHandler.php | 2 +- program/lib/Crypt/GPGAbstract.php | 508 ++++++++++++ 16 files changed, 3637 insertions(+), 668 deletions(-) create mode 100644 program/lib/Crypt/GPG/ByteUtils.php create mode 100644 program/lib/Crypt/GPG/KeyGenerator.php create mode 100644 program/lib/Crypt/GPG/KeyGeneratorErrorHandler.php create mode 100644 program/lib/Crypt/GPG/KeyGeneratorStatusHandler.php create mode 100644 program/lib/Crypt/GPG/PinEntry.php create mode 100644 program/lib/Crypt/GPG/ProcessControl.php create mode 100644 program/lib/Crypt/GPGAbstract.php (limited to 'program/lib/Crypt') diff --git a/program/lib/Crypt/GPG.php b/program/lib/Crypt/GPG.php index 6e8e717e8..5c2231289 100644 --- a/program/lib/Crypt/GPG.php +++ b/program/lib/Crypt/GPG.php @@ -47,14 +47,19 @@ * @package Crypt_GPG * @author Nathan Fredrickson * @author Michael Gauthier - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: GPG.php 302814 2010-08-26 15:43:07Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG * @link http://pear.php.net/manual/en/package.encryption.crypt-gpg.php * @link http://www.gnupg.org/ */ +/** + * Base class for GPG methods + */ +require_once 'Crypt/GPGAbstract.php'; + /** * Signature handler class */ @@ -65,31 +70,6 @@ require_once 'Crypt/GPG/VerifyStatusHandler.php'; */ require_once 'Crypt/GPG/DecryptStatusHandler.php'; -/** - * GPG key class - */ -require_once 'Crypt/GPG/Key.php'; - -/** - * GPG sub-key class - */ -require_once 'Crypt/GPG/SubKey.php'; - -/** - * GPG user id class - */ -require_once 'Crypt/GPG/UserId.php'; - -/** - * GPG process and I/O engine class - */ -require_once 'Crypt/GPG/Engine.php'; - -/** - * GPG exception classes - */ -require_once 'Crypt/GPG/Exceptions.php'; - // {{{ class Crypt_GPG /** @@ -104,82 +84,13 @@ require_once 'Crypt/GPG/Exceptions.php'; * @package Crypt_GPG * @author Nathan Fredrickson * @author Michael Gauthier - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ */ -class Crypt_GPG +class Crypt_GPG extends Crypt_GPGAbstract { - // {{{ class error constants - - /** - * Error code returned when there is no error. - */ - const ERROR_NONE = 0; - - /** - * Error code returned when an unknown or unhandled error occurs. - */ - const ERROR_UNKNOWN = 1; - - /** - * Error code returned when a bad passphrase is used. - */ - const ERROR_BAD_PASSPHRASE = 2; - - /** - * Error code returned when a required passphrase is missing. - */ - const ERROR_MISSING_PASSPHRASE = 3; - - /** - * Error code returned when a key that is already in the keyring is - * imported. - */ - const ERROR_DUPLICATE_KEY = 4; - - /** - * Error code returned the required data is missing for an operation. - * - * This could be missing key data, missing encrypted data or missing - * signature data. - */ - const ERROR_NO_DATA = 5; - - /** - * Error code returned when an unsigned key is used. - */ - const ERROR_UNSIGNED_KEY = 6; - - /** - * Error code returned when a key that is not self-signed is used. - */ - const ERROR_NOT_SELF_SIGNED = 7; - - /** - * Error code returned when a public or private key that is not in the - * keyring is used. - */ - const ERROR_KEY_NOT_FOUND = 8; - - /** - * Error code returned when an attempt to delete public key having a - * private key is made. - */ - const ERROR_DELETE_PRIVATE_KEY = 9; - - /** - * Error code returned when one or more bad signatures are detected. - */ - const ERROR_BAD_SIGNATURE = 10; - - /** - * Error code returned when there is a problem reading GnuPG data files. - */ - const ERROR_FILE_PERMISSIONS = 11; - - // }}} // {{{ class constants for data signing modes /** @@ -249,12 +160,27 @@ class Crypt_GPG const FORMAT_X509 = 3; // }}} - // {{{ other class constants + // {{{ class constants for boolean options + + /** + * Use to specify ASCII armored mode for returned data + */ + const ARMOR_ASCII = true; + + /** + * Use to specify binary mode for returned data + */ + const ARMOR_BINARY = false; + + /** + * Use to specify that line breaks in signed text should be normalized + */ + const TEXT_NORMALIZED = true; /** - * URI at which package bugs may be reported. + * Use to specify that line breaks in signed text should not be normalized */ - const BUG_URI = 'http://pear.php.net/bugs/report.php?package=Crypt_GPG'; + const TEXT_RAW = false; // }}} // {{{ protected class properties @@ -325,88 +251,6 @@ class Crypt_GPG */ protected $decryptKeys = array(); - // }}} - // {{{ __construct() - - /** - * Creates a new GPG object - * - * Available options are: - * - * - string homedir - the directory where the GPG - * keyring files are stored. If not - * specified, Crypt_GPG uses the - * default of ~/.gnupg. - * - string publicKeyring - the file path of the public - * keyring. Use this if the public - * keyring is not in the homedir, or - * if the keyring is in a directory - * not writable by the process - * invoking GPG (like Apache). Then - * you can specify the path to the - * keyring with this option - * (/foo/bar/pubring.gpg), and specify - * a writable directory (like /tmp) - * using the homedir option. - * - string privateKeyring - the file path of the private - * keyring. Use this if the private - * keyring is not in the homedir, or - * if the keyring is in a directory - * not writable by the process - * invoking GPG (like Apache). Then - * you can specify the path to the - * keyring with this option - * (/foo/bar/secring.gpg), and specify - * a writable directory (like /tmp) - * using the homedir option. - * - string trustDb - the file path of the web-of-trust - * database. Use this if the trust - * database is not in the homedir, or - * if the database is in a directory - * not writable by the process - * invoking GPG (like Apache). Then - * you can specify the path to the - * trust database with this option - * (/foo/bar/trustdb.gpg), and specify - * a writable directory (like /tmp) - * using the homedir option. - * - string binary - the location of the GPG binary. If - * not specified, the driver attempts - * to auto-detect the GPG binary - * location using a list of known - * default locations for the current - * operating system. The option - * gpgBinary is a - * deprecated alias for this option. - * - boolean debug - whether or not to use debug mode. - * When debug mode is on, all - * communication to and from the GPG - * subprocess is logged. This can be - * - * @param array $options optional. An array of options used to create the - * GPG object. All options are optional and are - * represented as key-value pairs. - * - * @throws Crypt_GPG_FileException if the homedir does not exist - * and cannot be created. This can happen if homedir is - * not specified, Crypt_GPG is run as the web user, and the web - * user has no home directory. This exception is also thrown if any - * of the options publicKeyring, - * privateKeyring or trustDb options are - * specified but the files do not exist or are are not readable. - * This can happen if the user running the Crypt_GPG process (for - * example, the Apache user) does not have permission to read the - * files. - * - * @throws PEAR_Exception if the provided binary is invalid, or - * if no binary is provided and no suitable binary could - * be found. - */ - public function __construct(array $options = array()) - { - $this->setEngine(new Crypt_GPG_Engine($options)); - } - // }}} // {{{ importKey() @@ -520,7 +364,9 @@ class Crypt_GPG if ($fingerprint === null) { throw new Crypt_GPG_KeyNotFoundException( 'Public key not found: ' . $keyId, - Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId); + self::ERROR_KEY_NOT_FOUND, + $keyId + ); } $keyData = ''; @@ -534,11 +380,13 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); - if ($code !== Crypt_GPG::ERROR_NONE) { + if ($code !== self::ERROR_NONE) { throw new Crypt_GPG_Exception( 'Unknown error exporting public key. Please use the ' . '\'debug\' option when creating the Crypt_GPG object, and ' . - 'file a bug report at ' . self::BUG_URI, $code); + 'file a bug report at ' . self::BUG_URI, + $code + ); } return $keyData; @@ -583,7 +431,9 @@ class Crypt_GPG if ($fingerprint === null) { throw new Crypt_GPG_KeyNotFoundException( 'Public key not found: ' . $keyId, - Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId); + self::ERROR_KEY_NOT_FOUND, + $keyId + ); } $operation = '--delete-key ' . escapeshellarg($fingerprint); @@ -599,17 +449,22 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: + case self::ERROR_NONE: break; - case Crypt_GPG::ERROR_DELETE_PRIVATE_KEY: + case self::ERROR_DELETE_PRIVATE_KEY: throw new Crypt_GPG_DeletePrivateKeyException( 'Private key must be deleted before public key can be ' . - 'deleted.', $code, $keyId); + 'deleted.', + $code, + $keyId + ); default: throw new Crypt_GPG_Exception( 'Unknown error deleting public key. Please use the ' . '\'debug\' option when creating the Crypt_GPG object, and ' . - 'file a bug report at ' . self::BUG_URI, $code); + 'file a bug report at ' . self::BUG_URI, + $code + ); } } @@ -647,7 +502,9 @@ class Crypt_GPG if ($fingerprint === null) { throw new Crypt_GPG_KeyNotFoundException( 'Private key not found: ' . $keyId, - Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId); + self::ERROR_KEY_NOT_FOUND, + $keyId + ); } $operation = '--delete-secret-key ' . escapeshellarg($fingerprint); @@ -663,17 +520,21 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: + case self::ERROR_NONE: break; - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + case self::ERROR_KEY_NOT_FOUND: throw new Crypt_GPG_KeyNotFoundException( 'Private key not found: ' . $keyId, - $code, $keyId); + $code, + $keyId + ); default: throw new Crypt_GPG_Exception( 'Unknown error deleting private key. Please use the ' . '\'debug\' option when creating the Crypt_GPG object, and ' . - 'file a bug report at ' . self::BUG_URI, $code); + 'file a bug report at ' . self::BUG_URI, + $code + ); } } @@ -705,161 +566,7 @@ class Crypt_GPG */ public function getKeys($keyId = '') { - // get private key fingerprints - if ($keyId == '') { - $operation = '--list-secret-keys'; - } else { - $operation = '--list-secret-keys ' . escapeshellarg($keyId); - } - - // According to The file 'doc/DETAILS' in the GnuPG distribution, using - // double '--with-fingerprint' also prints the fingerprint for subkeys. - $arguments = array( - '--with-colons', - '--with-fingerprint', - '--with-fingerprint', - '--fixed-list-mode' - ); - - $output = ''; - - $this->engine->reset(); - $this->engine->setOutput($output); - $this->engine->setOperation($operation, $arguments); - $this->engine->run(); - - $code = $this->engine->getErrorCode(); - - switch ($code) { - case Crypt_GPG::ERROR_NONE: - case Crypt_GPG::ERROR_KEY_NOT_FOUND: - // ignore not found key errors - break; - case Crypt_GPG::ERROR_FILE_PERMISSIONS: - $filename = $this->engine->getErrorFilename(); - if ($filename) { - throw new Crypt_GPG_FileException(sprintf( - 'Error reading GnuPG data file \'%s\'. Check to make ' . - 'sure it is readable by the current user.', $filename), - $code, $filename); - } - throw new Crypt_GPG_FileException( - 'Error reading GnuPG data file. Check to make GnuPG data ' . - 'files are readable by the current user.', $code); - default: - throw new Crypt_GPG_Exception( - 'Unknown error getting keys. Please use the \'debug\' option ' . - 'when creating the Crypt_GPG object, and file a bug report ' . - 'at ' . self::BUG_URI, $code); - } - - $privateKeyFingerprints = array(); - - $lines = explode(PHP_EOL, $output); - foreach ($lines as $line) { - $lineExp = explode(':', $line); - if ($lineExp[0] == 'fpr') { - $privateKeyFingerprints[] = $lineExp[9]; - } - } - - // get public keys - if ($keyId == '') { - $operation = '--list-public-keys'; - } else { - $operation = '--list-public-keys ' . escapeshellarg($keyId); - } - - $output = ''; - - $this->engine->reset(); - $this->engine->setOutput($output); - $this->engine->setOperation($operation, $arguments); - $this->engine->run(); - - $code = $this->engine->getErrorCode(); - - switch ($code) { - case Crypt_GPG::ERROR_NONE: - case Crypt_GPG::ERROR_KEY_NOT_FOUND: - // ignore not found key errors - break; - case Crypt_GPG::ERROR_FILE_PERMISSIONS: - $filename = $this->engine->getErrorFilename(); - if ($filename) { - throw new Crypt_GPG_FileException(sprintf( - 'Error reading GnuPG data file \'%s\'. Check to make ' . - 'sure it is readable by the current user.', $filename), - $code, $filename); - } - throw new Crypt_GPG_FileException( - 'Error reading GnuPG data file. Check to make GnuPG data ' . - 'files are readable by the current user.', $code); - default: - throw new Crypt_GPG_Exception( - 'Unknown error getting keys. Please use the \'debug\' option ' . - 'when creating the Crypt_GPG object, and file a bug report ' . - 'at ' . self::BUG_URI, $code); - } - - $keys = array(); - - $key = null; // current key - $subKey = null; // current sub-key - - $lines = explode(PHP_EOL, $output); - foreach ($lines as $line) { - $lineExp = explode(':', $line); - - if ($lineExp[0] == 'pub') { - - // new primary key means last key should be added to the array - if ($key !== null) { - $keys[] = $key; - } - - $key = new Crypt_GPG_Key(); - - $subKey = Crypt_GPG_SubKey::parse($line); - $key->addSubKey($subKey); - - } elseif ($lineExp[0] == 'sub') { - - $subKey = Crypt_GPG_SubKey::parse($line); - $key->addSubKey($subKey); - - } elseif ($lineExp[0] == 'fpr') { - - $fingerprint = $lineExp[9]; - - // set current sub-key fingerprint - $subKey->setFingerprint($fingerprint); - - // if private key exists, set has private to true - if (in_array($fingerprint, $privateKeyFingerprints)) { - $subKey->setHasPrivate(true); - } - - } elseif ($lineExp[0] == 'uid') { - - $string = stripcslashes($lineExp[9]); // as per documentation - $userId = new Crypt_GPG_UserId($string); - - if ($lineExp[1] == 'r') { - $userId->setRevoked(true); - } - - $key->addUserId($userId); - - } - } - - // add last key - if ($key !== null) { - $keys[] = $key; - } - - return $keys; + return parent::_getKeys($keyId); } // }}} @@ -895,7 +602,7 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - public function getFingerprint($keyId, $format = Crypt_GPG::FORMAT_NONE) + public function getFingerprint($keyId, $format = self::FORMAT_NONE) { $output = ''; $operation = '--list-keys ' . escapeshellarg($keyId); @@ -912,15 +619,17 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + case self::ERROR_NONE: + case self::ERROR_KEY_NOT_FOUND: // ignore not found key errors break; default: throw new Crypt_GPG_Exception( 'Unknown error getting key fingerprint. Please use the ' . '\'debug\' option when creating the Crypt_GPG object, and ' . - 'file a bug report at ' . self::BUG_URI, $code); + 'file a bug report at ' . self::BUG_URI, + $code + ); } $fingerprint = null; @@ -932,13 +641,13 @@ class Crypt_GPG $fingerprint = $lineExp[9]; switch ($format) { - case Crypt_GPG::FORMAT_CANONICAL: + case self::FORMAT_CANONICAL: $fingerprintExp = str_split($fingerprint, 4); $format = '%s %s %s %s %s %s %s %s %s %s'; $fingerprint = vsprintf($format, $fingerprintExp); break; - case Crypt_GPG::FORMAT_X509: + case self::FORMAT_X509: $fingerprintExp = str_split($fingerprint, 2); $fingerprint = implode(':', $fingerprintExp); break; @@ -976,7 +685,7 @@ class Crypt_GPG * * @sensitive $data */ - public function encrypt($data, $armor = true) + public function encrypt($data, $armor = self::ARMOR_ASCII) { return $this->_encrypt($data, false, null, $armor); } @@ -1012,8 +721,11 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - public function encryptFile($filename, $encryptedFile = null, $armor = true) - { + public function encryptFile( + $filename, + $encryptedFile = null, + $armor = self::ARMOR_ASCII + ) { return $this->_encrypt($filename, true, $encryptedFile, $armor); } @@ -1052,7 +764,7 @@ class Crypt_GPG * * @see Crypt_GPG::decryptAndVerify() */ - public function encryptAndSign($data, $armor = true) + public function encryptAndSign($data, $armor = self::ARMOR_ASCII) { return $this->_encryptAndSign($data, false, null, $armor); } @@ -1103,8 +815,10 @@ class Crypt_GPG * * @see Crypt_GPG::decryptAndVerifyFile() */ - public function encryptAndSignFile($filename, $signedFile = null, - $armor = true + public function encryptAndSignFile( + $filename, + $signedFile = null, + $armor = self::ARMOR_ASCII ) { return $this->_encryptAndSign($filename, true, $signedFile, $armor); } @@ -1315,8 +1029,11 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - public function sign($data, $mode = Crypt_GPG::SIGN_MODE_NORMAL, - $armor = true, $textmode = false + public function sign( + $data, + $mode = self::SIGN_MODE_NORMAL, + $armor = self::ARMOR_ASCII, + $textmode = self::TEXT_RAW ) { return $this->_sign($data, false, null, $mode, $armor, $textmode); } @@ -1376,8 +1093,12 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - public function signFile($filename, $signedFile = null, - $mode = Crypt_GPG::SIGN_MODE_NORMAL, $armor = true, $textmode = false + public function signFile( + $filename, + $signedFile = null, + $mode = self::SIGN_MODE_NORMAL, + $armor = self::ARMOR_ASCII, + $textmode = self::TEXT_RAW ) { return $this->_sign( $filename, @@ -1472,7 +1193,7 @@ class Crypt_GPG * @param string $passphrase optional. The passphrase of the key required * for decryption. * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::decrypt() * @see Crypt_GPG::decryptFile() @@ -1485,6 +1206,7 @@ class Crypt_GPG public function addDecryptKey($key, $passphrase = null) { $this->_addKey($this->decryptKeys, true, false, $key, $passphrase); + return $this; } // }}} @@ -1498,7 +1220,7 @@ class Crypt_GPG * {@link Crypt_GPG_SubKey}. The key must be able to * encrypt. * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::encrypt() * @see Crypt_GPG::encryptFile() @@ -1508,6 +1230,7 @@ class Crypt_GPG public function addEncryptKey($key) { $this->_addKey($this->encryptKeys, true, false, $key); + return $this; } // }}} @@ -1523,7 +1246,7 @@ class Crypt_GPG * @param string $passphrase optional. The passphrase of the key required * for signing. * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::sign() * @see Crypt_GPG::signFile() @@ -1536,6 +1259,7 @@ class Crypt_GPG public function addSignKey($key, $passphrase = null) { $this->_addKey($this->signKeys, false, true, $key, $passphrase); + return $this; } // }}} @@ -1544,7 +1268,7 @@ class Crypt_GPG /** * Clears all decryption keys * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::decrypt() * @see Crypt_GPG::addDecryptKey() @@ -1552,6 +1276,7 @@ class Crypt_GPG public function clearDecryptKeys() { $this->decryptKeys = array(); + return $this; } // }}} @@ -1560,7 +1285,7 @@ class Crypt_GPG /** * Clears all encryption keys * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::encrypt() * @see Crypt_GPG::addEncryptKey() @@ -1568,6 +1293,7 @@ class Crypt_GPG public function clearEncryptKeys() { $this->encryptKeys = array(); + return $this; } // }}} @@ -1576,7 +1302,7 @@ class Crypt_GPG /** * Clears all signing keys * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::sign() * @see Crypt_GPG::addSignKey() @@ -1584,6 +1310,7 @@ class Crypt_GPG public function clearSignKeys() { $this->signKeys = array(); + return $this; } // }}} @@ -1657,24 +1384,6 @@ class Crypt_GPG } } - // }}} - // {{{ setEngine() - - /** - * Sets the I/O engine to use for GnuPG operations - * - * Normally this method does not need to be used. It provides a means for - * dependency injection. - * - * @param Crypt_GPG_Engine $engine the engine to use. - * - * @return void - */ - public function setEngine(Crypt_GPG_Engine $engine) - { - $this->engine = $engine; - } - // }}} // {{{ _addKey() @@ -1698,7 +1407,7 @@ class Crypt_GPG * * @sensitive $passphrase */ - private function _addKey(array &$array, $encrypt, $sign, $key, + protected function _addKey(array &$array, $encrypt, $sign, $key, $passphrase = null ) { $subKeys = array(); @@ -1707,7 +1416,10 @@ class Crypt_GPG $keys = $this->getKeys($key); if (count($keys) == 0) { throw new Crypt_GPG_KeyNotFoundException( - 'Key "' . $key . '" not found.', 0, $key); + 'Key "' . $key . '" not found.', + 0, + $key + ); } $key = $keys[0]; } @@ -1715,12 +1427,14 @@ class Crypt_GPG if ($key instanceof Crypt_GPG_Key) { if ($encrypt && !$key->canEncrypt()) { throw new InvalidArgumentException( - 'Key "' . $key . '" cannot encrypt.'); + 'Key "' . $key . '" cannot encrypt.' + ); } if ($sign && !$key->canSign()) { throw new InvalidArgumentException( - 'Key "' . $key . '" cannot sign.'); + 'Key "' . $key . '" cannot sign.' + ); } foreach ($key->getSubKeys() as $subKey) { @@ -1741,18 +1455,21 @@ class Crypt_GPG if (count($subKeys) === 0) { throw new InvalidArgumentException( - 'Key "' . $key . '" is not in a recognized format.'); + 'Key "' . $key . '" is not in a recognized format.' + ); } foreach ($subKeys as $subKey) { if ($encrypt && !$subKey->canEncrypt()) { throw new InvalidArgumentException( - 'Key "' . $key . '" cannot encrypt.'); + 'Key "' . $key . '" cannot encrypt.' + ); } if ($sign && !$subKey->canSign()) { throw new InvalidArgumentException( - 'Key "' . $key . '" cannot sign.'); + 'Key "' . $key . '" cannot sign.' + ); } $array[$subKey->getId()] = array( @@ -1762,6 +1479,37 @@ class Crypt_GPG } } + // }}} + // {{{ _setPinEntryEnv() + + /** + * Sets the PINENTRY_USER_DATA environment variable with the currently + * added keys and passphrases + * + * Keys and pasphrases are stored as an indexed array of associative + * arrays that is JSON encoded to a flat string. + * + * For GnuPG 2.x this is how passphrases are passed. For GnuPG 1.x the + * environment variable is set but not used. + * + * @param array $keys the internal key array to use. + * + * @return void + */ + protected function _setPinEntryEnv(array $keys) + { + $envKeys = array(); + foreach ($keys as $id => $key) { + $envKeys[] = array( + 'keyId' => $id, + 'fingerprint' => $key['fingerprint'], + 'passphrase' => $key['passphrase'] + ); + } + $envKeys = json_encode($envKeys); + $_ENV['PINENTRY_USER_DATA'] = $envKeys; + } + // }}} // {{{ _importKey() @@ -1792,21 +1540,26 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - private function _importKey($key, $isFile) + protected function _importKey($key, $isFile) { $result = array(); if ($isFile) { $input = @fopen($key, 'rb'); if ($input === false) { - throw new Crypt_GPG_FileException('Could not open key file "' . - $key . '" for importing.', 0, $key); + throw new Crypt_GPG_FileException( + 'Could not open key file "' . $key . '" for importing.', + 0, + $key + ); } } else { $input = strval($key); if ($input == '') { throw new Crypt_GPG_NoDataException( - 'No valid GPG key data found.', Crypt_GPG::ERROR_NO_DATA); + 'No valid GPG key data found.', + self::ERROR_NO_DATA + ); } } @@ -1836,18 +1589,22 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_DUPLICATE_KEY: - case Crypt_GPG::ERROR_NONE: + case self::ERROR_DUPLICATE_KEY: + case self::ERROR_NONE: // ignore duplicate key import errors break; - case Crypt_GPG::ERROR_NO_DATA: + case self::ERROR_NO_DATA: throw new Crypt_GPG_NoDataException( - 'No valid GPG key data found.', $code); + 'No valid GPG key data found.', + $code + ); default: throw new Crypt_GPG_Exception( 'Unknown error importing GPG key. Please use the \'debug\' ' . 'option when creating the Crypt_GPG object, and file a bug ' . - 'report at ' . self::BUG_URI, $code); + 'report at ' . self::BUG_URI, + $code + ); } return $result; @@ -1880,18 +1637,23 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - private function _encrypt($data, $isFile, $outputFile, $armor) + protected function _encrypt($data, $isFile, $outputFile, $armor) { if (count($this->encryptKeys) === 0) { throw new Crypt_GPG_KeyNotFoundException( - 'No encryption keys specified.'); + 'No encryption keys specified.' + ); } if ($isFile) { $input = @fopen($data, 'rb'); if ($input === false) { - throw new Crypt_GPG_FileException('Could not open input file "' . - $data . '" for encryption.', 0, $data); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . + '" for encryption.', + 0, + $data + ); } } else { $input = strval($data); @@ -1905,9 +1667,12 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing encrypted data.', - 0, $outputFile); + throw new Crypt_GPG_FileException( + 'Could not open output file "' . $outputFile . + '" for storing encrypted data.', + 0, + $outputFile + ); } } @@ -1932,11 +1697,13 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); - if ($code !== Crypt_GPG::ERROR_NONE) { + if ($code !== self::ERROR_NONE) { throw new Crypt_GPG_Exception( 'Unknown error encrypting data. Please use the \'debug\' ' . 'option when creating the Crypt_GPG object, and file a bug ' . - 'report at ' . self::BUG_URI, $code); + 'report at ' . self::BUG_URI, + $code + ); } if ($outputFile === null) { @@ -1976,20 +1743,26 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - private function _decrypt($data, $isFile, $outputFile) + protected function _decrypt($data, $isFile, $outputFile) { if ($isFile) { $input = @fopen($data, 'rb'); if ($input === false) { - throw new Crypt_GPG_FileException('Could not open input file "' . - $data . '" for decryption.', 0, $data); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . + '" for decryption.', + 0, + $data + ); } } else { $input = strval($data); if ($input == '') { throw new Crypt_GPG_NoDataException( 'Cannot decrypt data. No PGP encrypted data was found in '. - 'the provided data.', Crypt_GPG::ERROR_NO_DATA); + 'the provided data.', + self::ERROR_NO_DATA + ); } } @@ -2001,14 +1774,22 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing decrypted data.', - 0, $outputFile); + throw new Crypt_GPG_FileException( + 'Could not open output file "' . $outputFile . + '" for storing decrypted data.', + 0, + $outputFile + ); } } - $handler = new Crypt_GPG_DecryptStatusHandler($this->engine, - $this->decryptKeys); + $handler = new Crypt_GPG_DecryptStatusHandler( + $this->engine, + $this->decryptKeys + ); + + // If using gpg-agent, set the decrypt pins used by the pinentry + $this->_setPinEntryEnv($this->decryptKeys); $this->engine->reset(); $this->engine->addStatusHandler(array($handler, 'handle')); @@ -2080,19 +1861,23 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - private function _sign($data, $isFile, $outputFile, $mode, $armor, + protected function _sign($data, $isFile, $outputFile, $mode, $armor, $textmode ) { if (count($this->signKeys) === 0) { throw new Crypt_GPG_KeyNotFoundException( - 'No signing keys specified.'); + 'No signing keys specified.' + ); } if ($isFile) { $input = @fopen($data, 'rb'); if ($input === false) { - throw new Crypt_GPG_FileException('Could not open input ' . - 'file "' . $data . '" for signing.', 0, $data); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . '" for signing.', + 0, + $data + ); } } else { $input = strval($data); @@ -2106,20 +1891,23 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing signed ' . - 'data.', 0, $outputFile); + throw new Crypt_GPG_FileException( + 'Could not open output file "' . $outputFile . + '" for storing signed data.', + 0, + $outputFile + ); } } switch ($mode) { - case Crypt_GPG::SIGN_MODE_DETACHED: + case self::SIGN_MODE_DETACHED: $operation = '--detach-sign'; break; - case Crypt_GPG::SIGN_MODE_CLEAR: + case self::SIGN_MODE_CLEAR: $operation = '--clearsign'; break; - case Crypt_GPG::SIGN_MODE_NORMAL: + case self::SIGN_MODE_NORMAL: default: $operation = '--sign'; break; @@ -2139,6 +1927,9 @@ class Crypt_GPG escapeshellarg($key['fingerprint']); } + // If using gpg-agent, set the sign pins used by the pinentry + $this->_setPinEntryEnv($this->signKeys); + $this->engine->reset(); $this->engine->addStatusHandler(array($this, 'handleSignStatus')); $this->engine->setInput($input); @@ -2157,24 +1948,32 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: + case self::ERROR_NONE: break; - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + case self::ERROR_KEY_NOT_FOUND: throw new Crypt_GPG_KeyNotFoundException( 'Cannot sign data. Private key not found. Import the '. - 'private key before trying to sign data.', $code, - $this->engine->getErrorKeyId()); - case Crypt_GPG::ERROR_BAD_PASSPHRASE: + 'private key before trying to sign data.', + $code, + $this->engine->getErrorKeyId() + ); + case self::ERROR_BAD_PASSPHRASE: throw new Crypt_GPG_BadPassphraseException( - 'Cannot sign data. Incorrect passphrase provided.', $code); - case Crypt_GPG::ERROR_MISSING_PASSPHRASE: + 'Cannot sign data. Incorrect passphrase provided.', + $code + ); + case self::ERROR_MISSING_PASSPHRASE: throw new Crypt_GPG_BadPassphraseException( - 'Cannot sign data. No passphrase provided.', $code); + 'Cannot sign data. No passphrase provided.', + $code + ); default: throw new Crypt_GPG_Exception( 'Unknown error signing data. Please use the \'debug\' option ' . 'when creating the Crypt_GPG object, and file a bug report ' . - 'at ' . self::BUG_URI, $code); + 'at ' . self::BUG_URI, + $code + ); } if ($outputFile === null) { @@ -2216,25 +2015,30 @@ class Crypt_GPG * Use the debug option and file a bug report if these * exceptions occur. */ - private function _encryptAndSign($data, $isFile, $outputFile, $armor) + protected function _encryptAndSign($data, $isFile, $outputFile, $armor) { if (count($this->signKeys) === 0) { throw new Crypt_GPG_KeyNotFoundException( - 'No signing keys specified.'); + 'No signing keys specified.' + ); } if (count($this->encryptKeys) === 0) { throw new Crypt_GPG_KeyNotFoundException( - 'No encryption keys specified.'); + 'No encryption keys specified.' + ); } if ($isFile) { $input = @fopen($data, 'rb'); if ($input === false) { - throw new Crypt_GPG_FileException('Could not open input ' . - 'file "' . $data . '" for encrypting and signing.', 0, - $data); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . + '" for encrypting and signing.', + 0, + $data + ); } } else { $input = strval($data); @@ -2248,9 +2052,12 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing encrypted, ' . - 'signed data.', 0, $outputFile); + throw new Crypt_GPG_FileException( + 'Could not open output file "' . $outputFile . + '" for storing encrypted, signed data.', + 0, + $outputFile + ); } } @@ -2261,6 +2068,9 @@ class Crypt_GPG escapeshellarg($key['fingerprint']); } + // If using gpg-agent, set the sign pins used by the pinentry + $this->_setPinEntryEnv($this->signKeys); + foreach ($this->encryptKeys as $key) { $arguments[] = '--recipient ' . escapeshellarg($key['fingerprint']); } @@ -2283,25 +2093,32 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: + case self::ERROR_NONE: break; - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + case self::ERROR_KEY_NOT_FOUND: throw new Crypt_GPG_KeyNotFoundException( 'Cannot sign encrypted data. Private key not found. Import '. 'the private key before trying to sign the encrypted data.', - $code, $this->engine->getErrorKeyId()); - case Crypt_GPG::ERROR_BAD_PASSPHRASE: + $code, + $this->engine->getErrorKeyId() + ); + case self::ERROR_BAD_PASSPHRASE: throw new Crypt_GPG_BadPassphraseException( 'Cannot sign encrypted data. Incorrect passphrase provided.', - $code); - case Crypt_GPG::ERROR_MISSING_PASSPHRASE: + $code + ); + case self::ERROR_MISSING_PASSPHRASE: throw new Crypt_GPG_BadPassphraseException( - 'Cannot sign encrypted data. No passphrase provided.', $code); + 'Cannot sign encrypted data. No passphrase provided.', + $code + ); default: throw new Crypt_GPG_Exception( 'Unknown error encrypting and signing data. Please use the ' . '\'debug\' option when creating the Crypt_GPG object, and ' . - 'file a bug report at ' . self::BUG_URI, $code); + 'file a bug report at ' . self::BUG_URI, + $code + ); } if ($outputFile === null) { @@ -2335,7 +2152,7 @@ class Crypt_GPG * * @see Crypt_GPG_Signature */ - private function _verify($data, $isFile, $signature) + protected function _verify($data, $isFile, $signature) { if ($signature == '') { $operation = '--verify'; @@ -2352,14 +2169,19 @@ class Crypt_GPG if ($isFile) { $input = @fopen($data, 'rb'); if ($input === false) { - throw new Crypt_GPG_FileException('Could not open input ' . - 'file "' . $data . '" for verifying.', 0, $data); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . '" for verifying.', + 0, + $data + ); } } else { $input = strval($data); if ($input == '') { throw new Crypt_GPG_NoDataException( - 'No valid signature data found.', Crypt_GPG::ERROR_NO_DATA); + 'No valid signature data found.', + self::ERROR_NO_DATA + ); } } @@ -2385,21 +2207,27 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: - case Crypt_GPG::ERROR_BAD_SIGNATURE: + case self::ERROR_NONE: + case self::ERROR_BAD_SIGNATURE: break; - case Crypt_GPG::ERROR_NO_DATA: + case self::ERROR_NO_DATA: throw new Crypt_GPG_NoDataException( - 'No valid signature data found.', $code); - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + 'No valid signature data found.', + $code + ); + case self::ERROR_KEY_NOT_FOUND: throw new Crypt_GPG_KeyNotFoundException( 'Public key required for data verification not in keyring.', - $code, $this->engine->getErrorKeyId()); + $code, + $this->engine->getErrorKeyId() + ); default: throw new Crypt_GPG_Exception( 'Unknown error validating signature details. Please use the ' . '\'debug\' option when creating the Crypt_GPG object, and ' . - 'file a bug report at ' . self::BUG_URI, $code); + 'file a bug report at ' . self::BUG_URI, + $code + ); } return $handler->getSignatures(); @@ -2445,21 +2273,25 @@ class Crypt_GPG * * @see Crypt_GPG_Signature */ - private function _decryptAndVerify($data, $isFile, $outputFile) + protected function _decryptAndVerify($data, $isFile, $outputFile) { if ($isFile) { $input = @fopen($data, 'rb'); if ($input === false) { - throw new Crypt_GPG_FileException('Could not open input ' . - 'file "' . $data . '" for decrypting and verifying.', 0, - $data); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . + '" for decrypting and verifying.', + 0, + $data + ); } } else { $input = strval($data); if ($input == '') { throw new Crypt_GPG_NoDataException( 'No valid encrypted signed data found.', - Crypt_GPG::ERROR_NO_DATA); + self::ERROR_NO_DATA + ); } } @@ -2471,16 +2303,24 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing decrypted data.', - 0, $outputFile); + throw new Crypt_GPG_FileException( + 'Could not open output file "' . $outputFile . + '" for storing decrypted data.', + 0, + $outputFile + ); } } $verifyHandler = new Crypt_GPG_VerifyStatusHandler(); - $decryptHandler = new Crypt_GPG_DecryptStatusHandler($this->engine, - $this->decryptKeys); + $decryptHandler = new Crypt_GPG_DecryptStatusHandler( + $this->engine, + $this->decryptKeys + ); + + // If using gpg-agent, set the decrypt pins used by the pinentry + $this->_setPinEntryEnv($this->decryptKeys); $this->engine->reset(); $this->engine->addStatusHandler(array($verifyHandler, 'handle')); @@ -2515,13 +2355,17 @@ class Crypt_GPG 'is in the keyring or the public key required for data ' . 'verification is not in the keyring. Import a suitable ' . 'key before trying to decrypt and verify this data.', - self::ERROR_KEY_NOT_FOUND, $this->engine->getErrorKeyId()); + self::ERROR_KEY_NOT_FOUND, + $this->engine->getErrorKeyId() + ); } if ($e instanceof Crypt_GPG_NoDataException) { throw new Crypt_GPG_NoDataException( 'Cannot decrypt and verify data. No PGP encrypted data ' . - 'was found in the provided data.', self::ERROR_NO_DATA); + 'was found in the provided data.', + self::ERROR_NO_DATA + ); } throw $e; diff --git a/program/lib/Crypt/GPG/ByteUtils.php b/program/lib/Crypt/GPG/ByteUtils.php new file mode 100644 index 000000000..342905471 --- /dev/null +++ b/program/lib/Crypt/GPG/ByteUtils.php @@ -0,0 +1,105 @@ + + * @copyright 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id$ + * @link http://pear.php.net/package/Crypt_GPG + */ + +// {{{ class Crypt_GPG_ByteUtils + +/** + * A class for performing byte-wise string operations + * + * GPG I/O streams are managed using bytes rather than characters. This class + * requires the mbstring extension to be available. + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier + * @copyright 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + * @link http://php.net/mbstring + */ +class Crypt_GPG_ByteUtils +{ + // {{{ strlen() + + /** + * Gets the length of a string in bytes + * + * This is used for stream-based communication with the GPG subprocess. + * + * @param string $string the string for which to get the length. + * + * @return integer the length of the string in bytes. + */ + public static function strlen($string) + { + return mb_strlen($string, '8bit'); + } + + // }}} + // {{{ substr() + + /** + * Gets the substring of a string in bytes + * + * This is used for stream-based communication with the GPG subprocess. + * + * @param string $string the input string. + * @param integer $start the starting point at which to get the substring. + * @param integer $length optional. The length of the substring. + * + * @return string the extracted part of the string. Unlike the default PHP + * substr() function, the returned value is + * always a string and never false. + */ + public static function substr($string, $start, $length = null) + { + if ($length === null) { + return mb_substr( + $string, + $start, + self::strlen($string) - $start, '8bit' + ); + } + + return mb_substr($string, $start, $length, '8bit'); + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Crypt/GPG/DecryptStatusHandler.php b/program/lib/Crypt/GPG/DecryptStatusHandler.php index 40e8d50ed..67c0dd74b 100644 --- a/program/lib/Crypt/GPG/DecryptStatusHandler.php +++ b/program/lib/Crypt/GPG/DecryptStatusHandler.php @@ -3,9 +3,9 @@ /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ /** - * Crypt_GPG is a package to use GPG from PHP + * Crypt_GPG is a package to use GnuPG from PHP * - * This file contains an object that handles GPG's status output for the + * This file contains an object that handles GnuPG's status output for the * decrypt operation. * * PHP version 5 @@ -29,9 +29,9 @@ * @category Encryption * @package Crypt_GPG * @author Michael Gauthier - * @copyright 2008-2009 silverorange + * @copyright 2008-2013 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: DecryptStatusHandler.php 302814 2010-08-26 15:43:07Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ */ @@ -42,7 +42,7 @@ require_once 'Crypt/GPG.php'; /** - * GPG exception classes + * Crypt_GPG exception classes */ require_once 'Crypt/GPG/Exceptions.php'; @@ -55,8 +55,8 @@ require_once 'Crypt/GPG/Exceptions.php'; * * This class is responsible for sending the passphrase commands when required * by the {@link Crypt_GPG::decrypt()} method. See doc/DETAILS in the - * {@link http://www.gnupg.org/download/ GPG distribution} for detailed - * information on GPG's status output for the decrypt operation. + * {@link http://www.gnupg.org/download/ GnuPG distribution} for detailed + * information on GnuPG's status output for the decrypt operation. * * This class is also responsible for parsing error status and throwing a * meaningful exception in the event that decryption fails. @@ -64,7 +64,7 @@ require_once 'Crypt/GPG/Exceptions.php'; * @category Encryption * @package Crypt_GPG * @author Michael Gauthier - * @copyright 2008 silverorange + * @copyright 2008-2013 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ @@ -293,8 +293,10 @@ class Crypt_GPG_DecryptStatusHandler throw new Crypt_GPG_KeyNotFoundException( 'Cannot decrypt data. No suitable private key is in the ' . 'keyring. Import a suitable private key before trying to ' . - 'decrypt this data.', $code, $keyId); - + 'decrypt this data.', + $code, + $keyId + ); case Crypt_GPG::ERROR_BAD_PASSPHRASE: $badPassphrases = array_diff_key( $this->badPassphrases, @@ -316,17 +318,23 @@ class Crypt_GPG_DecryptStatusHandler implode('", "', $badPassphrases) . '".'; } - throw new Crypt_GPG_BadPassphraseException($message, $code, - $badPassphrases, $missingPassphrases); - + throw new Crypt_GPG_BadPassphraseException( + $message, + $code, + $badPassphrases, + $missingPassphrases + ); case Crypt_GPG::ERROR_NO_DATA: throw new Crypt_GPG_NoDataException( 'Cannot decrypt data. No PGP encrypted data was found in '. - 'the provided data.', $code); - + 'the provided data.', + $code + ); default: throw new Crypt_GPG_Exception( - 'Unknown error decrypting data.', $code); + 'Unknown error decrypting data.', + $code + ); } } diff --git a/program/lib/Crypt/GPG/Engine.php b/program/lib/Crypt/GPG/Engine.php index 081be8e21..601541443 100644 --- a/program/lib/Crypt/GPG/Engine.php +++ b/program/lib/Crypt/GPG/Engine.php @@ -30,9 +30,9 @@ * @package Crypt_GPG * @author Nathan Fredrickson * @author Michael Gauthier - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: Engine.php 302822 2010-08-26 17:30:57Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ */ @@ -47,6 +47,16 @@ require_once 'Crypt/GPG.php'; */ require_once 'Crypt/GPG/Exceptions.php'; +/** + * Byte string operations. + */ +require_once 'Crypt/GPG/ByteUtils.php'; + +/** + * Process control methods. + */ +require_once 'Crypt/GPG/ProcessControl.php'; + /** * Standard PEAR exception is used if GPG binary is not found. */ @@ -70,7 +80,7 @@ require_once 'PEAR/Exception.php'; * @package Crypt_GPG * @author Nathan Fredrickson * @author Michael Gauthier - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ @@ -162,6 +172,17 @@ class Crypt_GPG_Engine */ private $_binary = ''; + /** + * Location of GnuPG agent binary + * + * Only used for GnuPG 2.x + * + * @var string + * @see Crypt_GPG_Engine::__construct() + * @see Crypt_GPG_Engine::_getAgent() + */ + private $_agent = ''; + /** * Directory containing the GPG key files * @@ -227,6 +248,15 @@ class Crypt_GPG_Engine */ private $_pipes = array(); + /** + * Array of pipes used for communication with the gpg-agent binary + * + * This is an array of file descriptor resources. + * + * @var array + */ + private $_agentPipes = array(); + /** * Array of currently opened pipes * @@ -247,6 +277,20 @@ class Crypt_GPG_Engine */ private $_process = null; + /** + * A handle for the gpg-agent process + * + * @var resource + */ + private $_agentProcess = null; + + /** + * GPG agent daemon socket and PID for running gpg-agent + * + * @var string + */ + private $_agentInfo = null; + /** * Whether or not the operating system is Darwin (OS X) * @@ -367,18 +411,6 @@ class Crypt_GPG_Engine */ private $_version = ''; - /** - * Cached value indicating whether or not mbstring function overloading is - * on for strlen - * - * This is cached for optimal performance inside the I/O loop. - * - * @var boolean - * @see Crypt_GPG_Engine::_byteLength() - * @see Crypt_GPG_Engine::_byteSubstring() - */ - private static $_mbStringOverload = null; - // }}} // {{{ __construct() @@ -432,6 +464,14 @@ class Crypt_GPG_Engine * operating system. The option * gpgBinary is a * deprecated alias for this option. + * - string agent - the location of the GnuPG agent + * binary. The gpg-agent is only + * used for GnuPG 2.x. If not + * specified, the engine attempts + * to auto-detect the gpg-agent + * binary location using a list of + * know default locations for the + * current operating system. * - boolean debug - whether or not to use debug mode. * When debug mode is on, all * communication to and from the GPG @@ -457,24 +497,38 @@ class Crypt_GPG_Engine * @throws PEAR_Exception if the provided binary is invalid, or * if no binary is provided and no suitable binary could * be found. + * + * @throws PEAR_Exception if the provided agent is invalid, or + * if no agent is provided and no suitable gpg-agent + * cound be found. */ public function __construct(array $options = array()) { $this->_isDarwin = (strncmp(strtoupper(PHP_OS), 'DARWIN', 6) === 0); - // populate mbstring overloading cache if not set - if (self::$_mbStringOverload === null) { - self::$_mbStringOverload = (extension_loaded('mbstring') - && (ini_get('mbstring.func_overload') & 0x02) === 0x02); - } - // get homedir if (array_key_exists('homedir', $options)) { $this->_homedir = (string)$options['homedir']; } else { - // note: this requires the package OS dep exclude 'windows' - $info = posix_getpwuid(posix_getuid()); - $this->_homedir = $info['dir'].'/.gnupg'; + if (extension_loaded('posix')) { + // note: this requires the package OS dep exclude 'windows' + $info = posix_getpwuid(posix_getuid()); + $this->_homedir = $info['dir'].'/.gnupg'; + } else { + if (isset($_SERVER['HOME'])) { + $this->_homedir = $_SERVER['HOME']; + } else { + $this->_homedir = getenv('HOME'); + } + } + + if ($this->_homedir === false) { + throw new Crypt_GPG_FileException( + 'Could not locate homedir. Please specify the homedir ' . + 'to use with the \'homedir\' option when instantiating ' . + 'the Crypt_GPG object.' + ); + } } // attempt to create homedir if it does not exist @@ -484,16 +538,40 @@ class Crypt_GPG_Engine // with 0777, homedir is set to 0700. chmod($this->_homedir, 0700); } else { - throw new Crypt_GPG_FileException('The \'homedir\' "' . - $this->_homedir . '" is not readable or does not exist '. - 'and cannot be created. This can happen if \'homedir\' '. - 'is not specified in the Crypt_GPG options, Crypt_GPG is '. - 'run as the web user, and the web user has no home '. - 'directory.', - 0, $this->_homedir); + throw new Crypt_GPG_FileException( + 'The \'homedir\' "' . $this->_homedir . '" is not ' . + 'readable or does not exist and cannot be created. This ' . + 'can happen if \'homedir\' is not specified in the ' . + 'Crypt_GPG options, Crypt_GPG is run as the web user, ' . + 'and the web user has no home directory.', + 0, + $this->_homedir + ); } } + // check homedir permissions (See Bug #19833) + if (!is_executable($this->_homedir)) { + throw new Crypt_GPG_FileException( + 'The \'homedir\' "' . $this->_homedir . '" is not enterable ' . + 'by the current user. Please check the permissions on your ' . + 'homedir and make sure the current user can both enter and ' . + 'write to the directory.', + 0, + $this->_homedir + ); + } + if (!is_writeable($this->_homedir)) { + throw new Crypt_GPG_FileException( + 'The \'homedir\' "' . $this->_homedir . '" is not writable ' . + 'by the current user. Please check the permissions on your ' . + 'homedir and make sure the current user can both enter and ' . + 'write to the directory.', + 0, + $this->_homedir + ); + } + // get binary if (array_key_exists('binary', $options)) { $this->_binary = (string)$options['binary']; @@ -505,9 +583,26 @@ class Crypt_GPG_Engine } if ($this->_binary == '' || !is_executable($this->_binary)) { - throw new PEAR_Exception('GPG binary not found. If you are sure '. - 'the GPG binary is installed, please specify the location of '. - 'the GPG binary using the \'binary\' driver option.'); + throw new PEAR_Exception( + 'GPG binary not found. If you are sure the GPG binary is ' . + 'installed, please specify the location of the GPG binary ' . + 'using the \'binary\' driver option.' + ); + } + + // get agent + if (array_key_exists('agent', $options)) { + $this->_agent = (string)$options['agent']; + } else { + $this->_agent = $this->_getAgent(); + } + + if ($this->_agent == '' || !is_executable($this->_agent)) { + throw new PEAR_Exception( + 'gpg-agent binary not found. If you are sure the gpg-agent ' . + 'is installed, please specify the location of the gpg-agent ' . + 'binary using the \'agent\' driver option.' + ); } /* @@ -891,7 +986,7 @@ class Crypt_GPG_Engine } $matches = array(); - $expression = '/gpg \(GnuPG\) (\S+)/'; + $expression = '#gpg \(GnuPG[A-Za-z0-9/]*?\) (\S+)#'; if (preg_match($expression, $info, $matches) === 1) { $this->_version = $matches[1]; @@ -1114,6 +1209,9 @@ class Crypt_GPG_Engine $fdCommand = $this->_pipes[self::FD_COMMAND]; $fdMessage = $this->_pipes[self::FD_MESSAGE]; + // select loop delay in milliseconds + $delay = 0; + while (true) { $inputStreams = array(); @@ -1166,15 +1264,15 @@ class Crypt_GPG_Engine $outputStreams[] = $this->_output; } - if ($this->_commandBuffer != '') { + if ($this->_commandBuffer != '' && is_resource($fdCommand)) { $outputStreams[] = $fdCommand; } - if ($messageBuffer != '') { + if ($messageBuffer != '' && is_resource($fdMessage)) { $outputStreams[] = $fdMessage; } - if ($inputBuffer != '') { + if ($inputBuffer != '' && is_resource($fdInput)) { $outputStreams[] = $fdInput; } @@ -1209,33 +1307,41 @@ class Crypt_GPG_Engine } // write input (to GPG) - if (in_array($fdInput, $outputStreams)) { + if (in_array($fdInput, $outputStreams, true)) { $this->_debug('GPG is ready for input'); - $chunk = self::_byteSubstring( + $chunk = Crypt_GPG_ByteUtils::substr( $inputBuffer, 0, self::CHUNK_SIZE ); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $this->_debug( '=> about to write ' . $length . ' bytes to GPG input' ); $length = fwrite($fdInput, $chunk, $length); - - $this->_debug('=> wrote ' . $length . ' bytes'); - - $inputBuffer = self::_byteSubstring( - $inputBuffer, - $length - ); + if ($length === 0) { + // If we wrote 0 bytes it was either EAGAIN or EPIPE. Since + // the pipe was seleted for writing, we assume it was EPIPE. + // There's no way to get the actual erorr code in PHP. See + // PHP Bug #39598. https://bugs.php.net/bug.php?id=39598 + $this->_debug('=> broken pipe on GPG input'); + $this->_debug('=> closing pipe GPG input'); + $this->_closePipe(self::FD_INPUT); + } else { + $this->_debug('=> wrote ' . $length . ' bytes'); + $inputBuffer = Crypt_GPG_ByteUtils::substr( + $inputBuffer, + $length + ); + } } // read input (from PHP stream) - if (in_array($this->_input, $inputStreams)) { + if (in_array($this->_input, $inputStreams, true)) { $this->_debug('input stream is ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1243,36 +1349,48 @@ class Crypt_GPG_Engine ); $chunk = fread($this->_input, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $inputBuffer .= $chunk; $this->_debug('=> read ' . $length . ' bytes'); } // write message (to GPG) - if (in_array($fdMessage, $outputStreams)) { + if (in_array($fdMessage, $outputStreams, true)) { $this->_debug('GPG is ready for message data'); - $chunk = self::_byteSubstring( + $chunk = Crypt_GPG_ByteUtils::substr( $messageBuffer, 0, self::CHUNK_SIZE ); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $this->_debug( '=> about to write ' . $length . ' bytes to GPG message' ); $length = fwrite($fdMessage, $chunk, $length); - $this->_debug('=> wrote ' . $length . ' bytes'); - - $messageBuffer = self::_byteSubstring($messageBuffer, $length); + if ($length === 0) { + // If we wrote 0 bytes it was either EAGAIN or EPIPE. Since + // the pipe was seleted for writing, we assume it was EPIPE. + // There's no way to get the actual erorr code in PHP. See + // PHP Bug #39598. https://bugs.php.net/bug.php?id=39598 + $this->_debug('=> broken pipe on GPG message'); + $this->_debug('=> closing pipe GPG message'); + $this->_closePipe(self::FD_MESSAGE); + } else { + $this->_debug('=> wrote ' . $length . ' bytes'); + $messageBuffer = Crypt_GPG_ByteUtils::substr( + $messageBuffer, + $length + ); + } } // read message (from PHP stream) - if (in_array($this->_message, $inputStreams)) { + if (in_array($this->_message, $inputStreams, true)) { $this->_debug('message stream is ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1280,14 +1398,14 @@ class Crypt_GPG_Engine ); $chunk = fread($this->_message, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $messageBuffer .= $chunk; $this->_debug('=> read ' . $length . ' bytes'); } // read output (from GPG) - if (in_array($fdOutput, $inputStreams)) { + if (in_array($fdOutput, $inputStreams, true)) { $this->_debug('GPG output stream ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1295,23 +1413,23 @@ class Crypt_GPG_Engine ); $chunk = fread($fdOutput, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $outputBuffer .= $chunk; $this->_debug('=> read ' . $length . ' bytes'); } // write output (to PHP stream) - if (in_array($this->_output, $outputStreams)) { + if (in_array($this->_output, $outputStreams, true)) { $this->_debug('output stream is ready for data'); - $chunk = self::_byteSubstring( + $chunk = Crypt_GPG_ByteUtils::substr( $outputBuffer, 0, self::CHUNK_SIZE ); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $this->_debug( '=> about to write ' . $length . ' bytes to output stream' @@ -1321,11 +1439,14 @@ class Crypt_GPG_Engine $this->_debug('=> wrote ' . $length . ' bytes'); - $outputBuffer = self::_byteSubstring($outputBuffer, $length); + $outputBuffer = Crypt_GPG_ByteUtils::substr( + $outputBuffer, + $length + ); } // read error (from GPG) - if (in_array($fdError, $inputStreams)) { + if (in_array($fdError, $inputStreams, true)) { $this->_debug('GPG error stream ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1333,14 +1454,14 @@ class Crypt_GPG_Engine ); $chunk = fread($fdError, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $errorBuffer .= $chunk; $this->_debug('=> read ' . $length . ' bytes'); // pass lines to error handlers while (($pos = strpos($errorBuffer, PHP_EOL)) !== false) { - $line = self::_byteSubstring($errorBuffer, 0, $pos); + $line = Crypt_GPG_ByteUtils::substr($errorBuffer, 0, $pos); foreach ($this->_errorHandlers as $handler) { array_unshift($handler['args'], $line); call_user_func_array( @@ -1350,15 +1471,15 @@ class Crypt_GPG_Engine array_shift($handler['args']); } - $errorBuffer = self::_byteSubString( + $errorBuffer = Crypt_GPG_ByteUtils::substr( $errorBuffer, - $pos + self::_byteLength(PHP_EOL) + $pos + Crypt_GPG_ByteUtils::strlen(PHP_EOL) ); } } // read status (from GPG) - if (in_array($fdStatus, $inputStreams)) { + if (in_array($fdStatus, $inputStreams, true)) { $this->_debug('GPG status stream ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1366,17 +1487,17 @@ class Crypt_GPG_Engine ); $chunk = fread($fdStatus, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $statusBuffer .= $chunk; $this->_debug('=> read ' . $length . ' bytes'); // pass lines to status handlers while (($pos = strpos($statusBuffer, PHP_EOL)) !== false) { - $line = self::_byteSubstring($statusBuffer, 0, $pos); + $line = Crypt_GPG_ByteUtils::substr($statusBuffer, 0, $pos); // only pass lines beginning with magic prefix - if (self::_byteSubstring($line, 0, 9) == '[GNUPG:] ') { - $line = self::_byteSubstring($line, 9); + if (Crypt_GPG_ByteUtils::substr($line, 0, 9) == '[GNUPG:] ') { + $line = Crypt_GPG_ByteUtils::substr($line, 9); foreach ($this->_statusHandlers as $handler) { array_unshift($handler['args'], $line); call_user_func_array( @@ -1387,38 +1508,60 @@ class Crypt_GPG_Engine array_shift($handler['args']); } } - $statusBuffer = self::_byteSubString( + $statusBuffer = Crypt_GPG_ByteUtils::substr( $statusBuffer, - $pos + self::_byteLength(PHP_EOL) + $pos + Crypt_GPG_ByteUtils::strlen(PHP_EOL) ); } } // write command (to GPG) - if (in_array($fdCommand, $outputStreams)) { + if (in_array($fdCommand, $outputStreams, true)) { $this->_debug('GPG is ready for command data'); // send commands - $chunk = self::_byteSubstring( + $chunk = Crypt_GPG_ByteUtils::substr( $this->_commandBuffer, 0, self::CHUNK_SIZE ); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $this->_debug( '=> about to write ' . $length . ' bytes to GPG command' ); $length = fwrite($fdCommand, $chunk, $length); + if ($length === 0) { + // If we wrote 0 bytes it was either EAGAIN or EPIPE. Since + // the pipe was seleted for writing, we assume it was EPIPE. + // There's no way to get the actual erorr code in PHP. See + // PHP Bug #39598. https://bugs.php.net/bug.php?id=39598 + $this->_debug('=> broken pipe on GPG command'); + $this->_debug('=> closing pipe GPG command'); + $this->_closePipe(self::FD_COMMAND); + } else { + $this->_debug('=> wrote ' . $length); + $this->_commandBuffer = Crypt_GPG_ByteUtils::substr( + $this->_commandBuffer, + $length + ); + } + } - $this->_debug('=> wrote ' . $length); + if (count($outputStreams) === 0 || count($inputStreams) === 0) { + // we have an I/O imbalance, increase the select loop delay + // to smooth things out + $delay += 10; + } else { + // things are running smoothly, decrease the delay + $delay -= 8; + $delay = max(0, $delay); + } - $this->_commandBuffer = self::_byteSubstring( - $this->_commandBuffer, - $length - ); + if ($delay > 0) { + usleep($delay); } } // end loop while streams are open @@ -1449,12 +1592,83 @@ class Crypt_GPG_Engine { $version = $this->getVersion(); + // Binary operations will not work on Windows with PHP < 5.2.6. This is + // in case stream_select() ever works on Windows. + $rb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'r' : 'rb'; + $wb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'w' : 'wb'; + $env = $_ENV; // Newer versions of GnuPG return localized results. Crypt_GPG only // works with English, so set the locale to 'C' for the subprocess. $env['LC_ALL'] = 'C'; + // If using GnuPG 2.x start the gpg-agent + if (version_compare($version, '2.0.0', 'ge')) { + $agentCommandLine = $this->_agent; + + $agentArguments = array( + '--options /dev/null', // ignore any saved options + '--csh', // output is easier to parse + '--keep-display', // prevent passing --display to pinentry + '--no-grab', + '--ignore-cache-for-signing', + '--pinentry-touch-file /dev/null', + '--disable-scdaemon', + '--no-use-standard-socket', + '--pinentry-program ' . escapeshellarg($this->_getPinEntry()) + ); + + if ($this->_homedir) { + $agentArguments[] = '--homedir ' . + escapeshellarg($this->_homedir); + } + + + $agentCommandLine .= ' ' . implode(' ', $agentArguments) + . ' --daemon'; + + $agentDescriptorSpec = array( + self::FD_INPUT => array('pipe', $rb), // stdin + self::FD_OUTPUT => array('pipe', $wb), // stdout + self::FD_ERROR => array('pipe', $wb) // stderr + ); + + $this->_debug('OPENING GPG-AGENT SUBPROCESS WITH THE FOLLOWING COMMAND:'); + $this->_debug($agentCommandLine); + + $this->_agentProcess = proc_open( + $agentCommandLine, + $agentDescriptorSpec, + $this->_agentPipes, + null, + $env, + array('binary_pipes' => true) + ); + + if (!is_resource($this->_agentProcess)) { + throw new Crypt_GPG_OpenSubprocessException( + 'Unable to open gpg-agent subprocess.', + 0, + $agentCommandLine + ); + } + + // Get GPG_AGENT_INFO and set environment variable for gpg process. + // This is a blocking read, but is only 1 line. + $agentInfo = fread( + $this->_agentPipes[self::FD_OUTPUT], + self::CHUNK_SIZE + ); + + $agentInfo = explode(' ', $agentInfo, 3); + $this->_agentInfo = $agentInfo[2]; + $env['GPG_AGENT_INFO'] = $this->_agentInfo; + + // gpg-agent daemon is started, we can close the launching process + $this->_closeAgentLaunchProcess(); + } + $commandLine = $this->_binary; $defaultArguments = array( @@ -1511,11 +1725,6 @@ class Crypt_GPG_Engine $commandLine .= ' ' . implode(' ', $arguments) . ' ' . $this->_operation; - // Binary operations will not work on Windows with PHP < 5.2.6. This is - // in case stream_select() ever works on Windows. - $rb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'r' : 'rb'; - $wb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'w' : 'wb'; - $descriptorSpec = array( self::FD_INPUT => array('pipe', $rb), // stdin self::FD_OUTPUT => array('pipe', $wb), // stdout @@ -1525,7 +1734,7 @@ class Crypt_GPG_Engine self::FD_MESSAGE => array('pipe', $rb) // message ); - $this->_debug('OPENING SUBPROCESS WITH THE FOLLOWING COMMAND:'); + $this->_debug('OPENING GPG SUBPROCESS WITH THE FOLLOWING COMMAND:'); $this->_debug($commandLine); $this->_process = proc_open( @@ -1542,6 +1751,11 @@ class Crypt_GPG_Engine 'Unable to open GPG subprocess.', 0, $commandLine); } + // Set streams as non-blocking. See Bug #18618. + foreach ($this->_pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + $this->_openPipes = $this->_pipes; $this->_errorCode = Crypt_GPG::ERROR_NONE; } @@ -1562,8 +1776,11 @@ class Crypt_GPG_Engine */ private function _closeSubprocess() { + // clear PINs from environment if they were set + $_ENV['PINENTRY_USER_DATA'] = null; + if (is_resource($this->_process)) { - $this->_debug('CLOSING SUBPROCESS'); + $this->_debug('CLOSING GPG SUBPROCESS'); // close remaining open pipes foreach (array_keys($this->_openPipes) as $pipeNumber) { @@ -1590,9 +1807,55 @@ class Crypt_GPG_Engine $this->_process = null; $this->_pipes = array(); } + + $this->_closeAgentLaunchProcess(); + + if ($this->_agentInfo !== null) { + $this->_debug('STOPPING GPG-AGENT DAEMON'); + + $parts = explode(':', $this->_agentInfo, 3); + $pid = $parts[1]; + $process = new Crypt_GPG_ProcessControl($pid); + + // terminate agent daemon + $process->terminate(); + + while ($process->isRunning()) { + usleep(10000); // 10 ms + $process->terminate(); + } + + $this->_agentInfo = null; + + $this->_debug('GPG-AGENT DAEMON STOPPED'); + } } // }}} + // {{ _closeAgentLaunchProcess() + + private function _closeAgentLaunchProcess() + { + if (is_resource($this->_agentProcess)) { + $this->_debug('CLOSING GPG-AGENT LAUNCH PROCESS'); + + // close agent pipes + foreach ($this->_agentPipes as $pipe) { + fflush($pipe); + fclose($pipe); + } + + // close agent launching process + proc_close($this->_agentProcess); + + $this->_agentProcess = null; + $this->_agentPipes = array(); + + $this->_debug('GPG-AGENT LAUNCH PROCESS CLOSED'); + } + } + + // }} // {{{ _closePipe() /** @@ -1658,6 +1921,55 @@ class Crypt_GPG_Engine } // }}} + // {{ _getAgent() + + private function _getAgent() + { + $agent = ''; + + if ($this->_isDarwin) { + $agentFiles = array( + '/opt/local/bin/gpg-agent', // MacPorts + '/usr/local/bin/gpg-agent', // Mac GPG + '/sw/bin/gpg-agent', // Fink + '/usr/bin/gpg-agent' + ); + } else { + $agentFiles = array( + '/usr/bin/gpg-agent', + '/usr/local/bin/gpg-agent' + ); + } + + foreach ($agentFiles as $agentFile) { + if (is_executable($agentFile)) { + $agent = $agentFile; + break; + } + } + + return $agent; + } + + // }} + // {{ _getPinEntry() + + private function _getPinEntry() + { + // Check if we're running directly from git or if we're using a + // PEAR-packaged version + $pinEntry = '@bin-dir@' . DIRECTORY_SEPARATOR . 'crypt-gpg-pinentry'; + + if ($pinEntry[0] === '@') { + $pinEntry = dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'scripts' + . DIRECTORY_SEPARATOR . 'crypt-gpg-pinentry'; + } + + return $pinEntry; + } + + // }} // {{{ _debug() /** @@ -1672,7 +1984,7 @@ class Crypt_GPG_Engine private function _debug($text) { if ($this->_debug) { - if (array_key_exists('SHELL', $_ENV)) { + if (php_sapi_name() === 'cli') { foreach (explode(PHP_EOL, $text) as $line) { echo "Crypt_GPG DEBUG: ", $line, PHP_EOL; } @@ -1686,70 +1998,6 @@ class Crypt_GPG_Engine } } - // }}} - // {{{ _byteLength() - - /** - * Gets the length of a string in bytes even if mbstring function - * overloading is turned on - * - * This is used for stream-based communication with the GPG subprocess. - * - * @param string $string the string for which to get the length. - * - * @return integer the length of the string in bytes. - * - * @see Crypt_GPG_Engine::$_mbStringOverload - */ - private static function _byteLength($string) - { - if (self::$_mbStringOverload) { - return mb_strlen($string, '8bit'); - } - - return strlen((binary)$string); - } - - // }}} - // {{{ _byteSubstring() - - /** - * Gets the substring of a string in bytes even if mbstring function - * overloading is turned on - * - * This is used for stream-based communication with the GPG subprocess. - * - * @param string $string the input string. - * @param integer $start the starting point at which to get the substring. - * @param integer $length optional. The length of the substring. - * - * @return string the extracted part of the string. Unlike the default PHP - * substr() function, the returned value is - * always a string and never false. - * - * @see Crypt_GPG_Engine::$_mbStringOverload - */ - private static function _byteSubstring($string, $start, $length = null) - { - if (self::$_mbStringOverload) { - if ($length === null) { - return mb_substr( - $string, - $start, - self::_byteLength($string) - $start, '8bit' - ); - } - - return mb_substr($string, $start, $length, '8bit'); - } - - if ($length === null) { - return (string)substr((binary)$string, $start); - } - - return (string)substr((binary)$string, $start, $length); - } - // }}} } diff --git a/program/lib/Crypt/GPG/Exceptions.php b/program/lib/Crypt/GPG/Exceptions.php index 744acf5d4..0ca917db6 100644 --- a/program/lib/Crypt/GPG/Exceptions.php +++ b/program/lib/Crypt/GPG/Exceptions.php @@ -32,9 +32,9 @@ * @package Crypt_GPG * @author Nathan Fredrickson * @author Michael Gauthier - * @copyright 2005 silverorange + * @copyright 2005-2011 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: Exceptions.php 273745 2009-01-18 05:24:25Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ @@ -468,6 +468,131 @@ class Crypt_GPG_DeletePrivateKeyException extends Crypt_GPG_Exception // }}} } +// }}} +// {{{ class Crypt_GPG_KeyNotCreatedException + +/** + * An exception thrown when an attempt is made to generate a key and the + * attempt fails + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier + * @copyright 2011 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + */ +class Crypt_GPG_KeyNotCreatedException extends Crypt_GPG_Exception +{ +} + +// }}} +// {{{ class Crypt_GPG_InvalidKeyParamsException + +/** + * An exception thrown when an attempt is made to generate a key and the + * key parameters set on the key generator are invalid + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier + * @copyright 2011 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + */ +class Crypt_GPG_InvalidKeyParamsException extends Crypt_GPG_Exception +{ + // {{{ private class properties + + /** + * The key algorithm + * + * @var integer + */ + private $_algorithm = 0; + + /** + * The key size + * + * @var integer + */ + private $_size = 0; + + /** + * The key usage + * + * @var integer + */ + private $_usage = 0; + + // }}} + // {{{ __construct() + + /** + * Creates a new Crypt_GPG_InvalidKeyParamsException + * + * @param string $message an error message. + * @param integer $code a user defined error code. + * @param string $algorithm the key algorithm. + * @param string $size the key size. + * @param string $usage the key usage. + */ + public function __construct( + $message, + $code = 0, + $algorithm = 0, + $size = 0, + $usage = 0 + ) { + parent::__construct($message, $code); + + $this->_algorithm = $algorithm; + $this->_size = $size; + $this->_usage = $usage; + } + + // }}} + // {{{ getAlgorithm() + + /** + * Gets the key algorithm + * + * @return integer the key algorithm. + */ + public function getAlgorithm() + { + return $this->_algorithm; + } + + // }}} + // {{{ getSize() + + /** + * Gets the key size + * + * @return integer the key size. + */ + public function getSize() + { + return $this->_size; + } + + // }}} + // {{{ getUsage() + + /** + * Gets the key usage + * + * @return integer the key usage. + */ + public function getUsage() + { + return $this->_usage; + } + + // }}} +} + // }}} ?> diff --git a/program/lib/Crypt/GPG/Key.php b/program/lib/Crypt/GPG/Key.php index 67a4b9c7d..6ecb538bc 100644 --- a/program/lib/Crypt/GPG/Key.php +++ b/program/lib/Crypt/GPG/Key.php @@ -28,7 +28,7 @@ * @author Michael Gauthier * @copyright 2008-2010 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: Key.php 295621 2010-03-01 04:18:54Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ diff --git a/program/lib/Crypt/GPG/KeyGenerator.php b/program/lib/Crypt/GPG/KeyGenerator.php new file mode 100644 index 000000000..f59c0ee3a --- /dev/null +++ b/program/lib/Crypt/GPG/KeyGenerator.php @@ -0,0 +1,790 @@ + + * @copyright 2011-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id:$ + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ + +/** + * Base class for GPG methods + */ +require_once 'Crypt/GPGAbstract.php'; + +/** + * Status output handler for key generation + */ +require_once 'Crypt/GPG/KeyGeneratorStatusHandler.php'; + +/** + * Error output handler for key generation + */ +require_once 'Crypt/GPG/KeyGeneratorErrorHandler.php'; + +// {{{ class Crypt_GPG_KeyGenerator + +/** + * GnuPG key generator + * + * This class provides an object oriented interface for generating keys with + * the GNU Privacy Guard (GPG). + * + * Secure key generation requires true random numbers, and as such can be slow. + * If the operating system runs out of entropy, key generation will block until + * more entropy is available. + * + * If quick key generation is important, a hardware entropy generator, or an + * entropy gathering daemon may be installed. For example, administrators of + * Debian systems may want to install the 'randomsound' package. + * + * This class uses the experimental automated key generation support available + * in GnuPG. See doc/DETAILS in the + * {@link http://www.gnupg.org/download/ GPG distribution} for detailed + * information on the key generation format. + * + * @category Encryption + * @package Crypt_GPG + * @author Nathan Fredrickson + * @author Michael Gauthier + * @copyright 2005-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ +class Crypt_GPG_KeyGenerator extends Crypt_GPGAbstract +{ + // {{{ protected properties + + /** + * The expiration date of generated keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setExpirationDate() + */ + protected $expirationDate = 0; + + /** + * The passphrase of generated keys + * + * @var string + * + * @see Crypt_GPG_KeyGenerator::setPassphrase() + */ + protected $passphrase = ''; + + /** + * The algorithm for generated primary keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setKeyParams() + */ + protected $keyAlgorithm = Crypt_GPG_SubKey::ALGORITHM_DSA; + + /** + * The size of generated primary keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setKeyParams() + */ + protected $keySize = 1024; + + /** + * The usages of generated primary keys + * + * This is a bitwise combination of the usage constants in + * {@link Crypt_GPG_SubKey}. + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setKeyParams() + */ + protected $keyUsage = 6; // USAGE_SIGN | USAGE_CERTIFY + + /** + * The algorithm for generated sub-keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setSubKeyParams() + */ + protected $subKeyAlgorithm = Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC; + + /** + * The size of generated sub-keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setSubKeyParams() + */ + protected $subKeySize = 2048; + + /** + * The usages of generated sub-keys + * + * This is a bitwise combination of the usage constants in + * {@link Crypt_GPG_SubKey}. + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setSubKeyParams() + */ + protected $subKeyUsage = Crypt_GPG_SubKey::USAGE_ENCRYPT; + + /** + * The GnuPG status handler to use for key generation + * + * @var Crypt_GPG_KeyGeneratorStatusHandler + * + * @see Crypt_GPG_KeyGenerator::setStatusHandler() + */ + protected $statusHandler = null; + + /** + * The GnuPG error handler to use for key generation + * + * @var Crypt_GPG_KeyGeneratorErrorHandler + * + * @see Crypt_GPG_KeyGenerator::setErrorHandler() + */ + protected $errorHandler = null; + + // }}} + // {{{ __construct() + + /** + * Creates a new GnuPG key generator + * + * Available options are: + * + * - string homedir - the directory where the GPG + * keyring files are stored. If not + * specified, Crypt_GPG uses the + * default of ~/.gnupg. + * - string publicKeyring - the file path of the public + * keyring. Use this if the public + * keyring is not in the homedir, or + * if the keyring is in a directory + * not writable by the process + * invoking GPG (like Apache). Then + * you can specify the path to the + * keyring with this option + * (/foo/bar/pubring.gpg), and specify + * a writable directory (like /tmp) + * using the homedir option. + * - string privateKeyring - the file path of the private + * keyring. Use this if the private + * keyring is not in the homedir, or + * if the keyring is in a directory + * not writable by the process + * invoking GPG (like Apache). Then + * you can specify the path to the + * keyring with this option + * (/foo/bar/secring.gpg), and specify + * a writable directory (like /tmp) + * using the homedir option. + * - string trustDb - the file path of the web-of-trust + * database. Use this if the trust + * database is not in the homedir, or + * if the database is in a directory + * not writable by the process + * invoking GPG (like Apache). Then + * you can specify the path to the + * trust database with this option + * (/foo/bar/trustdb.gpg), and specify + * a writable directory (like /tmp) + * using the homedir option. + * - string binary - the location of the GPG binary. If + * not specified, the driver attempts + * to auto-detect the GPG binary + * location using a list of known + * default locations for the current + * operating system. The option + * gpgBinary is a + * deprecated alias for this option. + * - string agent - the location of the GnuPG agent + * binary. The gpg-agent is only + * used for GnuPG 2.x. If not + * specified, the engine attempts + * to auto-detect the gpg-agent + * binary location using a list of + * know default locations for the + * current operating system. + * - boolean debug - whether or not to use debug mode. + * When debug mode is on, all + * communication to and from the GPG + * subprocess is logged. This can be + * + * @param array $options optional. An array of options used to create the + * GPG object. All options are optional and are + * represented as key-value pairs. + * + * @throws Crypt_GPG_FileException if the homedir does not exist + * and cannot be created. This can happen if homedir is + * not specified, Crypt_GPG is run as the web user, and the web + * user has no home directory. This exception is also thrown if any + * of the options publicKeyring, + * privateKeyring or trustDb options are + * specified but the files do not exist or are are not readable. + * This can happen if the user running the Crypt_GPG process (for + * example, the Apache user) does not have permission to read the + * files. + * + * @throws PEAR_Exception if the provided binary is invalid, or + * if no binary is provided and no suitable binary could + * be found. + * + * @throws PEAR_Exception if the provided agent is invalid, or + * if no agent is provided and no suitable gpg-agent + * cound be found. + */ + public function __construct(array $options = array()) + { + parent::__construct($options); + + $this->statusHandler = new Crypt_GPG_KeyGeneratorStatusHandler(); + $this->errorHandler = new Crypt_GPG_KeyGeneratorErrorHandler(); + } + + // }}} + // {{{ setExpirationDate() + + /** + * Sets the expiration date of generated keys + * + * @param string|integer $date either a string that may be parsed by + * PHP's strtotime() function, or an integer + * timestamp representing the number of seconds + * since the UNIX epoch. This date must be at + * least one date in the future. Keys that + * expire in the past may not be generated. Use + * an expiration date of 0 for keys that do not + * expire. + * + * @throws InvalidArgumentException if the date is not a valid format, or + * if the date is not at least one day in + * the future, or if the date is greater + * than 2038-01-19T03:14:07. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setExpirationDate($date) + { + if (is_int($date) || ctype_digit(strval($date))) { + $expirationDate = intval($date); + } else { + $expirationDate = strtotime($date); + } + + if ($expirationDate === false) { + throw new InvalidArgumentException( + sprintf( + 'Invalid expiration date format: "%s". Please use a ' . + 'format compatible with PHP\'s strtotime().', + $date + ) + ); + } + + if ($expirationDate !== 0 && $expirationDate < time() + 86400) { + throw new InvalidArgumentException( + 'Expiration date must be at least a day in the future.' + ); + } + + // GnuPG suffers from the 2038 bug + if ($expirationDate > 2147483647) { + throw new InvalidArgumentException( + 'Expiration date must not be greater than 2038-01-19T03:14:07.' + ); + } + + $this->expirationDate = $expirationDate; + + return $this; + } + + // }}} + // {{{ setPassphrase() + + /** + * Sets the passphrase of generated keys + * + * @param string $passphrase the passphrase to use for generated keys. Use + * null or an empty string for no passphrase. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setPassphrase($passphrase) + { + $this->passphrase = strval($passphrase); + return $this; + } + + // }}} + // {{{ setKeyParams() + + /** + * Sets the parameters for the primary key of generated key-pairs + * + * @param integer $algorithm the algorithm used by the key. This should be + * one of the Crypt_GPG_SubKey::ALGORITHM_* + * constants. + * @param integer $size optional. The size of the key. Different + * algorithms have different size requirements. + * If not specified, the default size for the + * specified algorithm will be used. If an + * invalid key size is used, GnuPG will do its + * best to round it to a valid size. + * @param integer $usage optional. A bitwise combination of key usages. + * If not specified, the primary key will be used + * only to sign and certify. This is the default + * behavior of GnuPG in interactive mode. Use + * the Crypt_GPG_SubKey::USAGE_* constants here. + * The primary key may be used to certify even + * if the certify usage is not specified. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setKeyParams($algorithm, $size = 0, $usage = 0) + { + $apgorithm = intval($algorithm); + + if ($algorithm === Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC) { + throw new Crypt_GPG_InvalidKeyParamsException( + 'Primary key algorithm must be capable of signing. The ' . + 'Elgamal algorithm can only encrypt.', + 0, + $algorithm, + $size, + $usage + ); + } + + if ($size != 0) { + $size = intval($size); + } + + if ($usage != 0) { + $usage = intval($usage); + } + + $usageEncrypt = Crypt_GPG_SubKey::USAGE_ENCRYPT; + + if ( $algorithm === Crypt_GPG_SubKey::ALGORITHM_DSA + && ($usage & $usageEncrypt) === $usageEncrypt + ) { + throw new Crypt_GPG_InvalidKeyParamsException( + 'The DSA algorithm is not capable of encrypting. Please ' . + 'specify a different algorithm or do not include encryption ' . + 'as a usage for the primary key.', + 0, + $algorithm, + $size, + $usage + ); + } + + $this->keyAlgorithm = $algorithm; + + if ($size != 0) { + $this->keySize = $size; + } + + if ($usage != 0) { + $this->keyUsage = $usage; + } + + return $this; + } + + // }}} + // {{{ setSubKeyParams() + + /** + * Sets the parameters for the sub-key of generated key-pairs + * + * @param integer $algorithm the algorithm used by the key. This should be + * one of the Crypt_GPG_SubKey::ALGORITHM_* + * constants. + * @param integer $size optional. The size of the key. Different + * algorithms have different size requirements. + * If not specified, the default size for the + * specified algorithm will be used. If an + * invalid key size is used, GnuPG will do its + * best to round it to a valid size. + * @param integer $usage optional. A bitwise combination of key usages. + * If not specified, the sub-key will be used + * only to encrypt. This is the default behavior + * of GnuPG in interactive mode. Use the + * Crypt_GPG_SubKey::USAGE_* constants here. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setSubKeyParams($algorithm, $size = '', $usage = 0) + { + $apgorithm = intval($algorithm); + + if ($size != 0) { + $size = intval($size); + } + + if ($usage != 0) { + $usage = intval($usage); + } + + $usageSign = Crypt_GPG_SubKey::USAGE_SIGN; + + if ( $algorithm === Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC + && ($usage & $usageSign) === $usageSign + ) { + throw new Crypt_GPG_InvalidKeyParamsException( + 'The Elgamal algorithm is not capable of signing. Please ' . + 'specify a different algorithm or do not include signing ' . + 'as a usage for the sub-key.', + 0, + $algorithm, + $size, + $usage + ); + } + + $usageEncrypt = Crypt_GPG_SubKey::USAGE_ENCRYPT; + + if ( $algorithm === Crypt_GPG_SubKey::ALGORITHM_DSA + && ($usage & $usageEncrypt) === $usageEncrypt + ) { + throw new Crypt_GPG_InvalidKeyParamsException( + 'The DSA algorithm is not capable of encrypting. Please ' . + 'specify a different algorithm or do not include encryption ' . + 'as a usage for the sub-key.', + 0, + $algorithm, + $size, + $usage + ); + } + + $this->subKeyAlgorithm = $algorithm; + + if ($size != 0) { + $this->subKeySize = $size; + } + + if ($usage != 0) { + $this->subKeyUsage = $usage; + } + + return $this; + } + + // }}} + // {{{ setStatusHandler() + + /** + * Sets the status handler to use for key generation + * + * Normally this method does not need to be used. It provides a means for + * dependency injection. + * + * @param Crypt_GPG_KeyStatusHandler $handler the key status handler to + * use. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setStatusHandler( + Crypt_GPG_KeyGeneratorStatusHandler $handler + ) { + $this->statusHandler = $handler; + return $this; + } + + // }}} + // {{{ setErrorHandler() + + /** + * Sets the error handler to use for key generation + * + * Normally this method does not need to be used. It provides a means for + * dependency injection. + * + * @param Crypt_GPG_KeyErrorHandler $handler the key error handler to + * use. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setErrorHandler( + Crypt_GPG_KeyGeneratorErrorHandler $handler + ) { + $this->errorHandler = $handler; + return $this; + } + + // }}} + // {{{ generateKey() + + /** + * Generates a new key-pair in the current keyring + * + * Secure key generation requires true random numbers, and as such can be + * solw. If the operating system runs out of entropy, key generation will + * block until more entropy is available. + * + * If quick key generation is important, a hardware entropy generator, or + * an entropy gathering daemon may be installed. For example, + * administrators of Debian systems may want to install the 'randomsound' + * package. + * + * @param string|Crypt_GPG_UserId $name either a {@link Crypt_GPG_UserId} + * object, or a string containing + * the name of the user id. + * @param string $email optional. If $name is + * specified as a string, this is + * the email address of the user id. + * @param string $comment optional. If $name is + * specified as a string, this is + * the comment of the user id. + * + * @return Crypt_GPG_Key the newly generated key. + * + * @throws Crypt_GPG_KeyNotCreatedException if the key parameters are + * incorrect, if an unknown error occurs during key generation, or + * if the newly generated key is not found in the keyring. + * + * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs. + * Use the debug option and file a bug report if these + * exceptions occur. + */ + public function generateKey($name, $email = '', $comment = '') + { + $handle = uniqid('key', true); + + $userId = $this->getUserId($name, $email, $comment); + + $keyParams = array( + 'Key-Type' => $this->keyAlgorithm, + 'Key-Length' => $this->keySize, + 'Key-Usage' => $this->getUsage($this->keyUsage), + 'Subkey-Type' => $this->subKeyAlgorithm, + 'Subkey-Length' => $this->subKeySize, + 'Subkey-Usage' => $this->getUsage($this->subKeyUsage), + 'Name-Real' => $userId->getName(), + 'Handle' => $handle, + ); + + if ($this->expirationDate != 0) { + // GnuPG only accepts granularity of days + $expirationDate = date('Y-m-d', $this->expirationDate); + $keyParams['Expire-Date'] = $expirationDate; + } + + if ($this->passphrase != '') { + $keyParams['Passphrase'] = $this->passphrase; + } + + if ($userId->getEmail() != '') { + $keyParams['Name-Email'] = $userId->getEmail(); + } + + if ($userId->getComment() != '') { + $keyParams['Name-Comment'] = $userId->getComment(); + } + + + $keyParamsFormatted = array(); + foreach ($keyParams as $name => $value) { + $keyParamsFormatted[] = $name . ': ' . $value; + } + + $input = implode("\n", $keyParamsFormatted) . "\n%commit\n"; + + $statusHandler = clone $this->statusHandler; + $statusHandler->setHandle($handle); + + $errorHandler = clone $this->errorHandler; + + $this->engine->reset(); + $this->engine->addStatusHandler(array($statusHandler, 'handle')); + $this->engine->addErrorHandler(array($errorHandler, 'handle')); + $this->engine->setInput($input); + $this->engine->setOutput($output); + $this->engine->setOperation('--gen-key', array('--batch')); + $this->engine->run(); + + $code = $errorHandler->getErrorCode(); + switch ($code) { + case self::ERROR_BAD_KEY_PARAMS: + switch ($errorHandler->getLineNumber()) { + case 1: + throw new Crypt_GPG_InvalidKeyParamsException( + 'Invalid primary key algorithm specified.', + 0, + $this->keyAlgorithm, + $this->keySize, + $this->keyUsage + ); + case 4: + throw new Crypt_GPG_InvalidKeyParamsException( + 'Invalid sub-key algorithm specified.', + 0, + $this->subKeyAlgorithm, + $this->subKeySize, + $this->subKeyUsage + ); + default: + throw new Crypt_GPG_InvalidKeyParamsException( + 'Invalid key algorithm specified.' + ); + } + } + + $code = $this->engine->getErrorCode(); + + switch ($code) { + case self::ERROR_NONE: + break; + default: + throw new Crypt_GPG_Exception( + 'Unknown error generating key-pair. Please use the \'debug\' ' . + 'option when creating the Crypt_GPG object, and file a bug ' . + 'report at ' . self::BUG_URI, + $code + ); + } + + $code = $statusHandler->getErrorCode(); + + switch ($code) { + case self::ERROR_NONE: + break; + case self::ERROR_KEY_NOT_CREATED: + throw new Crypt_GPG_KeyNotCreatedException( + 'Unable to create new key-pair. Invalid key parameters. ' . + 'Make sure the specified key algorithms and sizes are ' . + 'correct.', + $code + ); + } + + $fingerprint = $statusHandler->getKeyFingerprint(); + $keys = $this->_getKeys($fingerprint); + + if (count($keys) === 0) { + throw new Crypt_GPG_KeyNotCreatedException( + sprintf( + 'Newly created key "%s" not found in keyring.', + $fingerprint + ) + ); + } + + return $keys[0]; + } + + // }}} + // {{{ getUsage() + + /** + * Builds a GnuPG key usage string suitable for key generation + * + * See doc/DETAILS in the + * {@link http://www.gnupg.org/download/ GPG distribution} for detailed + * information on the key usage format. + * + * @param integer $usage a bitwise combination of the key usages. This is + * a combination of the Crypt_GPG_SubKey::USAGE_* + * constants. + * + * @return string the key usage string. + */ + protected function getUsage($usage) + { + $map = array( + Crypt_GPG_SubKey::USAGE_ENCRYPT => 'encrypt', + Crypt_GPG_SubKey::USAGE_SIGN => 'sign', + Crypt_GPG_SubKey::USAGE_CERTIFY => 'cert', + Crypt_GPG_SubKey::USAGE_AUTHENTICATION => 'auth', + ); + + // cert is always used for primary keys and does not need to be + // specified + $usage &= ~Crypt_GPG_SubKey::USAGE_CERTIFY; + + $usageArray = array(); + + foreach ($map as $key => $value) { + if (($usage & $key) === $key) { + $usageArray[] = $value; + } + } + + return implode(',', $usageArray); + } + + // }}} + // {{{ getUserId() + + /** + * Gets a user id object from parameters + * + * @param string|Crypt_GPG_UserId $name either a {@link Crypt_GPG_UserId} + * object, or a string containing + * the name of the user id. + * @param string $email optional. If $name is + * specified as a string, this is + * the email address of the user id. + * @param string $comment optional. If $name is + * specified as a string, this is + * the comment of the user id. + * + * @return Crypt_GPG_UserId a user id object for the specified parameters. + */ + protected function getUserId($name, $email = '', $comment = '') + { + if ($name instanceof Crypt_GPG_UserId) { + $userId = $name; + } else { + $userId = new Crypt_GPG_UserId(); + $userId->setName($name)->setEmail($email)->setComment($comment); + } + + return $userId; + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Crypt/GPG/KeyGeneratorErrorHandler.php b/program/lib/Crypt/GPG/KeyGeneratorErrorHandler.php new file mode 100644 index 000000000..ad9ebf395 --- /dev/null +++ b/program/lib/Crypt/GPG/KeyGeneratorErrorHandler.php @@ -0,0 +1,121 @@ + + * @copyright 2011-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id:$ + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ + +/** + * Error line handler for the key generation operation + * + * This class is used internally by Crypt_GPG and does not need be used + * directly. See the {@link Crypt_GPG} class for end-user API. + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier + * @copyright 2011-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ +class Crypt_GPG_KeyGeneratorErrorHandler +{ + // {{{ protected properties + + /** + * Error code (if any) caused by key generation + * + * @var integer + */ + protected $errorCode = Crypt_GPG::ERROR_NONE; + + /** + * Line number at which the error occurred + * + * @var integer + */ + protected $lineNumber = null; + + // }}} + // {{{ handle() + + /** + * Handles an error line + * + * @param string $line the error line to handle. + * + * @return void + */ + public function handle($line) + { + $matches = array(); + $pattern = '/:([0-9]+): invalid algorithm$/'; + if (preg_match($pattern, $line, $matches) === 1) { + $this->errorCode = Crypt_GPG::ERROR_BAD_KEY_PARAMS; + $this->lineNumber = intval($matches[1]); + } + } + + // }}} + // {{{ getErrorCode() + + /** + * Gets the error code resulting from key gneration + * + * @return integer the error code resulting from key generation. + */ + public function getErrorCode() + { + return $this->errorCode; + } + + // }}} + // {{{ getLineNumber() + + /** + * Gets the line number at which the error occurred + * + * @return integer the line number at which the error occurred. Null if + * no error occurred. + */ + public function getLineNumber() + { + return $this->lineNumber; + } + + // }}} +} + +?> diff --git a/program/lib/Crypt/GPG/KeyGeneratorStatusHandler.php b/program/lib/Crypt/GPG/KeyGeneratorStatusHandler.php new file mode 100644 index 000000000..8b4c85c7a --- /dev/null +++ b/program/lib/Crypt/GPG/KeyGeneratorStatusHandler.php @@ -0,0 +1,173 @@ + + * @copyright 2011-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id:$ + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ + +/** + * Status line handler for the key generation operation + * + * This class is used internally by Crypt_GPG and does not need be used + * directly. See the {@link Crypt_GPG} class for end-user API. + * + * This class is responsible for parsing the final key fingerprint from the + * status output and for updating the key generation progress file. See + * doc/DETAILS in the + * {@link http://www.gnupg.org/download/ GPG distribution} for detailed + * information on GPG's status output for the batch key generation operation. + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier + * @copyright 2011-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ +class Crypt_GPG_KeyGeneratorStatusHandler +{ + // {{{ protected properties + + /** + * The key fingerprint + * + * Ths key fingerprint is emitted by GPG after the key generation is + * complete. + * + * @var string + */ + protected $keyFingerprint = ''; + + /** + * The unique key handle used by this handler + * + * The key handle is used to track GPG status output for a particular key + * before the key has its own identifier. + * + * @var string + * + * @see Crypt_GPG_KeyGeneratorStatusHandler::setHandle() + */ + protected $handle = ''; + + /** + * Error code (if any) caused by key generation + * + * @var integer + */ + protected $errorCode = Crypt_GPG::ERROR_NONE; + + // }}} + // {{{ setHandle() + + /** + * Sets the unique key handle used by this handler + * + * The key handle is used to track GPG status output for a particular key + * before the key has its own identifier. + * + * @param string $handle the key handle this status handle will use. + * + * @return Crypt_GPG_KeyGeneratorStatusHandler the current object, for + * fluent interface. + */ + public function setHandle($handle) + { + $this->handle = strval($handle); + return $this; + } + + // }}} + // {{{ handle() + + /** + * Handles a status line + * + * @param string $line the status line to handle. + * + * @return void + */ + public function handle($line) + { + $tokens = explode(' ', $line); + switch ($tokens[0]) { + case 'KEY_CREATED': + if ($tokens[3] == $this->handle) { + $this->keyFingerprint = $tokens[2]; + } + break; + + case 'KEY_NOT_CREATED': + if ($tokens[1] == $this->handle) { + $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_CREATED; + } + break; + + case 'PROGRESS': + // todo: at some point, support reporting status async + break; + } + } + + // }}} + // {{{ getKeyFingerprint() + + /** + * Gets the key fingerprint parsed by this handler + * + * @return array the key fingerprint parsed by this handler. + */ + public function getKeyFingerprint() + { + return $this->keyFingerprint; + } + + // }}} + // {{{ getErrorCode() + + /** + * Gets the error code resulting from key gneration + * + * @return integer the error code resulting from key generation. + */ + public function getErrorCode() + { + return $this->errorCode; + } + + // }}} +} + +?> diff --git a/program/lib/Crypt/GPG/PinEntry.php b/program/lib/Crypt/GPG/PinEntry.php new file mode 100644 index 000000000..c09703617 --- /dev/null +++ b/program/lib/Crypt/GPG/PinEntry.php @@ -0,0 +1,875 @@ + + * @copyright 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id$ + * @link http://pear.php.net/package/Crypt_GPG + */ + +/** + * CLI user-interface and parser. + */ +require_once 'Console/CommandLine.php'; + +// {{{ class Crypt_GPG_PinEntry + +/** + * A command-line dummy pinentry program for use with gpg-agent and Crypt_GPG + * + * This pinentry receives passphrases through en environment variable and + * automatically enters the PIN in response to gpg-agent requests. No user- + * interaction required. + * + * Thie pinentry can be run independently for testing and debugging with the + * following syntax: + * + *
+ * Usage:
+ *   crypt-gpg-pinentry [options]
+ *
+ * Options:
+ *   -l log, --log=log  Optional location to log pinentry activity.
+ *   -v, --verbose      Sets verbosity level. Use multiples for more detail
+ *                      (e.g. "-vv").
+ *   -h, --help         show this help message and exit
+ *   --version          show the program version and exit
+ * 
+ * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier + * @copyright 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + * @see Crypt_GPG::getKeys() + */ +class Crypt_GPG_PinEntry +{ + // {{{ class constants + + /** + * Verbosity level for showing no output. + */ + const VERBOSITY_NONE = 0; + + /** + * Verbosity level for showing error output. + */ + const VERBOSITY_ERRORS = 1; + + /** + * Verbosity level for showing all output, including Assuan protocol + * messages. + */ + const VERBOSITY_ALL = 2; + + /** + * Length of buffer for reading lines from the Assuan server. + * + * PHP reads 8192 bytes. If this is set to less than 8192, PHP reads 8192 + * and buffers the rest so we might as well just read 8192. + * + * Using values other than 8192 also triggers PHP bugs. + * + * @see http://bugs.php.net/bug.php?id=35224 + */ + const CHUNK_SIZE = 8192; + + // }}} + // {{{ protected properties + + /** + * File handle for the input stream + * + * @var resource + */ + protected $stdin = null; + + /** + * File handle for the output stream + * + * @var resource + */ + protected $stdout = null; + + /** + * File handle for the log file if a log file is used + * + * @var resource + */ + protected $logFile = null; + + /** + * Whether or not this pinentry is finished and is exiting + * + * @var boolean + */ + protected $moribund = false; + + /** + * Verbosity level + * + * One of: + * - {@link Crypt_GPG_PinEntry::VERBOSITY_NONE}, + * - {@link Crypt_GPG_PinEntry::VERBOSITY_ERRORS}, or + * - {@link Crypt_GPG_PinEntry::VERBOSITY_ALL} + * + * @var integer + */ + protected $verbosity = self::VERBOSITY_NONE; + + /** + * The command-line interface parser for this pinentry + * + * @var Console_CommandLine + * + * @see Crypt_GPG_PinEntry::getParser() + */ + protected $parser = null; + + /** + * PINs to be entered by this pinentry + * + * An indexed array of associative arrays in the form: + * + * $keyId, + * 'passphrase' => $passphrase + * ), + * ... + * ); + * ?> + * + * + * This array is parsed from the environment variable + * PINENTRY_USER_DATA. + * + * @var array + * + * @see Crypt_GPG_PinEntry::initPinsFromENV() + */ + protected $pins = array(); + + /** + * PINs that have been tried for the current PIN + * + * This is an associative array indexed by the key identifier with + * values being the same as elements in the {@link Crypt_GPG_PinEntry::$pins} + * array. + * + * @var array + */ + protected $triedPins = array(); + + /** + * The PIN currently being requested by the Assuan server + * + * If set, this is an associative array in the form: + * + * $shortKeyId, + * 'userId' => $userIdString + * ); + * ?> + * + * + * @var array|null + */ + protected $currentPin = null; + + // }}} + // {{{ __invoke() + + /** + * Runs this pinentry + * + * @return void + */ + public function __invoke() + { + $this->parser = $this->getCommandLineParser(); + + try { + $result = $this->parser->parse(); + + $this->setVerbosity($result->options['verbose']); + $this->setLogFilename($result->options['log']); + + $this->connect(); + $this->initPinsFromENV(); + + while (($line = fgets($this->stdin, self::CHUNK_SIZE)) !== false) { + $this->parseCommand(mb_substr($line, 0, -1, '8bit')); + if ($this->moribund) { + break; + } + } + + $this->disconnect(); + + } catch (Console_CommandLineException $e) { + $this->log($e->getMessage() . PHP_EOL, slf::VERBOSITY_ERRORS); + exit(1); + } catch (Exception $e) { + $this->log($e->getMessage() . PHP_EOL, self::VERBOSITY_ERRORS); + $this->log($e->getTraceAsString() . PHP_EOL, self::VERBOSITY_ERRORS); + exit(1); + } + } + + // }}} + // {{{ setVerbosity() + + /** + * Sets the verbosity of logging for this pinentry + * + * Verbosity levels are: + * + * - {@link Crypt_GPG_PinEntry::VERBOSITY_NONE} - no logging. + * - {@link Crypt_GPG_PinEntry::VERBOSITY_ERRORS} - log errors only. + * - {@link Crypt_GPG_PinEntry::VERBOSITY_ALL} - log everything, including + * the assuan protocol. + * + * @param integer $verbosity the level of verbosity of this pinentry. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + public function setVerbosity($verbosity) + { + $this->verbosity = (integer)$verbosity; + return $this; + } + + // }}} + // {{{ setLogFilename() + + /** + * Sets the log file location + * + * @param string $filename the new log filename to use. If an empty string + * is used, file-based logging is disabled. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + public function setLogFilename($filename) + { + if (is_resource($this->logFile)) { + fflush($this->logFile); + fclose($this->logFile); + $this->logFile = null; + } + + if ($filename != '') { + if (($this->logFile = fopen($filename, 'w')) === false) { + $this->log( + 'Unable to open log file "' . $filename . '" ' + . 'for writing.' . PHP_EOL, + self::VERBOSITY_ERRORS + ); + exit(1); + } else { + stream_set_write_buffer($this->logFile, 0); + } + } + + return $this; + } + + // }}} + // {{{ getUIXML() + + /** + * Gets the CLI user-interface definition for this pinentry + * + * Detects whether or not this package is PEAR-installed and appropriately + * locates the XML UI definition. + * + * @return string the location of the CLI user-interface definition XML. + */ + protected function getUIXML() + { + $dir = '@data-dir@' . DIRECTORY_SEPARATOR + . '@package-name@' . DIRECTORY_SEPARATOR . 'data'; + + // Check if we're running directly from a git checkout or if we're + // running from a PEAR-packaged version. + if ($dir[0] == '@') { + $dir = dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data'; + } + + return $dir . DIRECTORY_SEPARATOR . 'pinentry-cli.xml'; + } + + // }}} + // {{{ getCommandLineParser() + + /** + * Gets the CLI parser for this pinentry + * + * @return Console_CommandLine the CLI parser for this pinentry. + */ + protected function getCommandLineParser() + { + return Console_CommandLine::fromXmlFile($this->getUIXML()); + } + + // }}} + // {{{ log() + + /** + * Logs a message at the specified verbosity level + * + * If a log file is used, the message is written to the log. Otherwise, + * the message is sent to STDERR. + * + * @param string $data the message to log. + * @param integer $level the verbosity level above which the message should + * be logged. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function log($data, $level) + { + if ($this->verbosity >= $level) { + if (is_resource($this->logFile)) { + fwrite($this->logFile, $data); + fflush($this->logFile); + } else { + $this->parser->outputter->stderr($data); + } + } + + return $this; + } + + // }}} + // {{{ connect() + + /** + * Connects this pinentry to the assuan server + * + * Opens I/O streams and sends initial handshake. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function connect() + { + // Binary operations will not work on Windows with PHP < 5.2.6. + $rb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'r' : 'rb'; + $wb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'w' : 'wb'; + + $this->stdin = fopen('php://stdin', $rb); + $this->stdout = fopen('php://stdout', $wb); + + if (function_exists('stream_set_read_buffer')) { + stream_set_read_buffer($this->stdin, 0); + } + stream_set_write_buffer($this->stdout, 0); + + // initial handshake + $this->send($this->getOK('Crypt_GPG pinentry ready and waiting')); + + return $this; + } + + // }}} + // {{{ parseCommand() + + /** + * Parses an assuan command and performs the appropriate action + * + * Documentation of the assuan commands for pinentry is limited to + * non-existent. Most of these commands were taken from the C source code + * to gpg-agent and pinentry. + * + * Additional context was provided by using strace -f when calling the + * gpg-agent. + * + * @param string $line the assuan command line to parse + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function parseCommand($line) + { + $this->log('<- ' . $line . PHP_EOL, self::VERBOSITY_ALL); + + $parts = explode(' ', $line, 2); + + $command = $parts[0]; + + if (count($parts) === 2) { + $data = $parts[1]; + } else { + $data = null; + } + + switch ($command) { + case 'SETDESC': + return $this->sendSetDescription($data); + + case 'SETPROMPT': + case 'SETERROR': + case 'SETOK': + case 'SETNOTOK': + case 'SETCANCEL': + case 'SETQUALITYBAR': + case 'SETQUALITYBAR_TT': + case 'OPTION': + return $this->sendNotImplementedOK(); + + case 'MESSAGE': + return $this->sendMessage(); + + case 'CONFIRM': + return $this->sendConfirm(); + + case 'GETINFO': + return $this->sendGetInfo($data); + + case 'GETPIN': + return $this->sendGetPin($data); + + case 'RESET': + return $this->sendReset(); + + case 'BYE': + return $this->sendBye(); + } + } + + // }}} + // {{{ initPinsFromENV() + + /** + * Initializes the PINs to be entered by this pinentry from the environment + * variable PINENTRY_USER_DATA + * + * The PINs are parsed from a JSON-encoded string. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function initPinsFromENV() + { + if (($userData = getenv('PINENTRY_USER_DATA')) !== false) { + $pins = json_decode($userData, true); + if ($pins === null) { + $this->log( + '-- failed to parse user data' . PHP_EOL, + self::VERBOSITY_ERRORS + ); + } else { + $this->pins = $pins; + $this->log( + '-- got user data [not showing passphrases]' . PHP_EOL, + self::VERBOSITY_ALL + ); + } + } + + return $this; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects this pinentry from the Assuan server + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function disconnect() + { + $this->log('-- disconnecting' . PHP_EOL, self::VERBOSITY_ALL); + + fflush($this->stdout); + fclose($this->stdout); + fclose($this->stdin); + + $this->stdin = null; + $this->stdout = null; + + $this->log('-- disconnected' . PHP_EOL, self::VERBOSITY_ALL); + + if (is_resource($this->logFile)) { + fflush($this->logFile); + fclose($this->logFile); + $this->logFile = null; + } + + return $this; + } + + // }}} + // {{{ sendNotImplementedOK() + + /** + * Sends an OK response for a not implemented feature + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendNotImplementedOK() + { + return $this->send($this->getOK()); + } + + // }}} + // {{{ sendSetDescription() + + /** + * Parses the currently requested key identifier and user identifier from + * the description passed to this pinentry + * + * @param string $text the raw description sent from gpg-agent. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendSetDescription($text) + { + $text = rawurldecode($text); + $matches = array(); + // TODO: handle user id with quotation marks + $exp = '/\n"(.+)"\n.*\sID ([A-Z0-9]+),\n/mu'; + if (preg_match($exp, $text, $matches) === 1) { + $userId = $matches[1]; + $keyId = $matches[2]; + + // only reset tried pins for new requested pin + if ( $this->currentPin === null + || $this->currentPin['keyId'] !== $keyId + ) { + $this->currentPin = array( + 'userId' => $userId, + 'keyId' => $keyId + ); + $this->triedPins = array(); + $this->log( + '-- looking for PIN for ' . $keyId . PHP_EOL, + self::VERBOSITY_ALL + ); + } + } + + return $this->send($this->getOK()); + } + + // }}} + // {{{ sendConfirm() + + /** + * Tells the assuan server the PIN entry was confirmed (not cancelled) + * by pressing the fake 'close' button + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendConfirm() + { + return $this->sendButtonInfo('close'); + } + + // }}} + // {{{ sendMessage() + + /** + * Tells the assuan server that any requested pop-up messages were confirmed + * by pressing the fake 'close' button + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendMessage() + { + return $this->sendButtonInfo('close'); + } + + // }}} + // {{{ sendButtonInfo() + + /** + * Sends information about pressed buttons to the assuan server + * + * This is used to fake a user-interface for this pinentry. + * + * @param string $text the button status to send. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendButtonInfo($text) + { + return $this->send('BUTTON_INFO ' . $text . "\n"); + } + + // }}} + // {{{ sendGetPin() + + /** + * Sends the PIN value for the currently requested key + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendGetPin() + { + $foundPin = ''; + + if (is_array($this->currentPin)) { + $keyIdLength = mb_strlen($this->currentPin['keyId'], '8bit'); + + // search for the pin + foreach ($this->pins as $pin) { + // only check pins we haven't tried + if (!isset($this->triedPins[$pin['keyId']])) { + + // get last X characters of key identifier to compare + $keyId = mb_substr( + $pin['keyId'], + -$keyIdLength, + mb_strlen($pin['keyId'], '8bit'), + '8bit' + ); + + if ($keyId === $this->currentPin['keyId']) { + $foundPin = $pin['passphrase']; + $this->triedPins[$pin['keyId']] = $pin; + break; + } + } + } + } + + return $this + ->send($this->getData($foundPin)) + ->send($this->getOK()); + } + + // }}} + // {{{ sendGetInfo() + + /** + * Sends information about this pinentry + * + * @param string $data the information requested by the assuan server. + * Currently only 'pid' is supported. Other requests + * return no information. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendGetInfo($data) + { + $parts = explode(' ', $data, 2); + $command = reset($parts); + + switch ($command) { + case 'pid': + return $this->sendGetInfoPID(); + default: + return $this->send($this->getOK()); + } + + return $this; + } + // }}} + // {{{ sendGetInfoPID() + + /** + * Sends the PID of this pinentry to the assuan server + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendGetInfoPID() + { + return $this + ->send($this->getData(getmypid())) + ->send($this->getOK()); + } + + // }}} + // {{{ sendBye() + + /** + * Flags this pinentry for disconnection and sends an OK response + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendBye() + { + $return = $this->send($this->getOK('closing connection')); + $this->moribund = true; + return $return; + } + + // }}} + // {{{ sendReset() + + /** + * Resets this pinentry and sends an OK response + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendReset() + { + $this->currentPin = null; + $this->triedPins = array(); + return $this->send($this->getOK()); + } + + // }}} + // {{{ getOK() + + /** + * Gets an OK response to send to the assuan server + * + * @param string $data an optional message to include with the OK response. + * + * @return string the OK response. + */ + protected function getOK($data = null) + { + $return = 'OK'; + + if ($data) { + $return .= ' ' . $data; + } + + return $return . "\n"; + } + + // }}} + // {{{ getData() + + /** + * Gets data ready to send to the assuan server + * + * Data is appropriately escaped and long lines are wrapped. + * + * @param string $data the data to send to the assuan server. + * + * @return string the properly escaped, formatted data. + * + * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html + */ + protected function getData($data) + { + // Escape data. Only %, \n and \r need to be escaped but other + // values are allowed to be escaped. See + // http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html + $data = rawurlencode($data); + $data = $this->getWordWrappedData($data, 'D'); + return $data; + } + + // }}} + // {{{ getComment() + + /** + * Gets a comment ready to send to the assuan server + * + * @param string $data the comment to send to the assuan server. + * + * @return string the properly formatted comment. + * + * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html + */ + protected function getComment($data) + { + return $this->getWordWrappedData($data, '#'); + } + + // }}} + // {{{ getWordWrappedData() + + /** + * Wraps strings at 1,000 bytes without splitting UTF-8 multibyte + * characters + * + * Each line is prepended with the specified line prefix. Wrapped lines + * are automatically appended with \ characters. + * + * Protocol strings are UTF-8 but maximum line length is 1,000 bytes. + * mb_strcut() is used so we can limit line length by bytes + * and not split characters across multiple lines. + * + * @param string $data the data to wrap. + * @param string $prefix a single character to use as the line prefix. For + * example, 'D' or '#'. + * + * @return string the word-wrapped, prefixed string. + * + * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html + */ + protected function getWordWrappedData($data, $prefix) + { + $lines = array(); + + do { + if (mb_strlen($data, '8bit') > 997) { + $line = $prefix . ' ' . mb_strcut($data, 0, 996, 'utf-8') . "\\\n"; + $lines[] = $line; + $lineLength = mb_strlen($line, '8bit') - 1; + $dataLength = mb_substr($data, '8bit'); + $data = mb_substr( + $data, + $lineLength, + $dataLength - $lineLength, + '8bit' + ); + } else { + $lines[] = $prefix . ' ' . $data . "\n"; + $data = ''; + } + } while ($data != ''); + + return implode('', $lines); + } + + // }}} + // {{{ send() + + /** + * Sends raw data to the assuan server + * + * @param string $data the data to send. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function send($data) + { + $this->log('-> ' . $data, self::VERBOSITY_ALL); + fwrite($this->stdout, $data); + fflush($this->stdout); + return $this; + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Crypt/GPG/ProcessControl.php b/program/lib/Crypt/GPG/ProcessControl.php new file mode 100644 index 000000000..d6dae0325 --- /dev/null +++ b/program/lib/Crypt/GPG/ProcessControl.php @@ -0,0 +1,150 @@ + + * @copyright 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id$ + * @link http://pear.php.net/package/Crypt_GPG + */ + +// {{{ class Crypt_GPG_ProcessControl + +/** + * A class for monitoring and terminating processes by PID + * + * This is used to safely terminate the gpg-agent for GnuPG 2.x. This class + * is limited in its abilities and can only check if a PID is running and + * send a PID SIGTERM. + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier + * @copyright 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + */ +class Crypt_GPG_ProcessControl +{ + // {{{ protected properties + + /** + * The PID (process identifier) being monitored + * + * @var integer + */ + protected $pid; + + // }}} + // {{{ __construct() + + /** + * Creates a new process controller from the given PID (process identifier) + * + * @param integer $pid the PID (process identifier). + */ + public function __construct($pid) + { + $this->pid = $pid; + } + + // }}} + // {{{ public function getPid() + + /** + * Gets the PID (process identifier) being controlled + * + * @return integer the PID being controlled. + */ + public function getPid() + { + return $this->pid; + } + + // }}} + // {{{ isRunning() + + /** + * Checks if the process is running + * + * Uses ps on UNIX-like systems and tasklist on + * Windows. + * + * @return boolean true if the process is running, false if not. + */ + public function isRunning() + { + $running = false; + + if (PHP_OS === 'WINNT') { + $command = 'tasklist /fo csv /nh /fi ' + . escapeshellarg('PID eq ' . $this->pid); + + $result = exec($command); + $parts = explode(',', $result); + $running = (count($parts) > 1 && trim($parts[1], '"') == $this->pid); + } else { + $result = exec('ps -p ' . escapeshellarg($this->pid) . ' -o pid='); + $running = (trim($result) == $this->pid); + } + + return $running; + } + + // }}} + // {{{ terminate() + + /** + * Ends the process gracefully + * + * The signal SIGTERM is sent to the process. The gpg-agent process will + * end gracefully upon receiving the SIGTERM signal. Upon 3 consecutive + * SIGTERM signals the gpg-agent will forcefully shut down. + * + * If the posix extension is available, posix_kill() + * is used. Otherwise kill is used on UNIX-like systems and + * taskkill is used in Windows. + * + * @return void + */ + public function terminate() + { + if (function_exists('posix_kill')) { + posix_kill($this->pid, 15); + } elseif (PHP_OS === 'WINNT') { + exec('taskkill /PID ' . escapeshellarg($this->pid)); + } else { + exec('kill -15 ' . escapeshellarg($this->pid)); + } + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Crypt/GPG/Signature.php b/program/lib/Crypt/GPG/Signature.php index 03ab44c53..1d28a1188 100644 --- a/program/lib/Crypt/GPG/Signature.php +++ b/program/lib/Crypt/GPG/Signature.php @@ -28,9 +28,10 @@ * @category Encryption * @package Crypt_GPG * @author Nathan Fredrickson - * @copyright 2005-2010 silverorange + * @author Michael Gauthier + * @copyright 2005-2013 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: Signature.php 302773 2010-08-25 14:16:28Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ @@ -50,7 +51,7 @@ require_once 'Crypt/GPG/UserId.php'; * @package Crypt_GPG * @author Nathan Fredrickson * @author Michael Gauthier - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 * @link http://pear.php.net/package/Crypt_GPG * @see Crypt_GPG::verify() @@ -159,8 +160,6 @@ class Crypt_GPG_Signature if ($signature->_userId instanceof Crypt_GPG_UserId) { $this->_userId = clone $signature->_userId; - } else { - $this->_userId = $signature->_userId; } } diff --git a/program/lib/Crypt/GPG/SubKey.php b/program/lib/Crypt/GPG/SubKey.php index b6316e99f..59245cac1 100644 --- a/program/lib/Crypt/GPG/SubKey.php +++ b/program/lib/Crypt/GPG/SubKey.php @@ -29,7 +29,7 @@ * @author Nathan Fredrickson * @copyright 2005-2010 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: SubKey.php 302768 2010-08-25 13:45:52Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ @@ -53,7 +53,7 @@ */ class Crypt_GPG_SubKey { - // {{{ class constants + // {{{ algorithm class constants /** * RSA encryption algorithm. @@ -76,6 +76,29 @@ class Crypt_GPG_SubKey */ const ALGORITHM_ELGAMAL_ENC_SGN = 20; + // }}} + // {{{ usage class constants + + /** + * Key can be used to encrypt + */ + const USAGE_ENCRYPT = 1; + + /** + * Key can be used to sign + */ + const USAGE_SIGN = 2; + + /** + * Key can be used to certify other keys + */ + const USAGE_CERTIFY = 4; + + /** + * Key can be used for authentication + */ + const USAGE_AUTHENTICATION = 8; + // }}} // {{{ class properties diff --git a/program/lib/Crypt/GPG/UserId.php b/program/lib/Crypt/GPG/UserId.php index 04435708c..a367bceb3 100644 --- a/program/lib/Crypt/GPG/UserId.php +++ b/program/lib/Crypt/GPG/UserId.php @@ -28,7 +28,7 @@ * @author Michael Gauthier * @copyright 2008-2010 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: UserId.php 295621 2010-03-01 04:18:54Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ diff --git a/program/lib/Crypt/GPG/VerifyStatusHandler.php b/program/lib/Crypt/GPG/VerifyStatusHandler.php index 083bd3012..8904be149 100644 --- a/program/lib/Crypt/GPG/VerifyStatusHandler.php +++ b/program/lib/Crypt/GPG/VerifyStatusHandler.php @@ -31,7 +31,7 @@ * @author Michael Gauthier * @copyright 2008 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 - * @version CVS: $Id: VerifyStatusHandler.php 302908 2010-08-31 03:56:54Z gauthierm $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ */ diff --git a/program/lib/Crypt/GPGAbstract.php b/program/lib/Crypt/GPGAbstract.php new file mode 100644 index 000000000..214133936 --- /dev/null +++ b/program/lib/Crypt/GPGAbstract.php @@ -0,0 +1,508 @@ + + * @author Michael Gauthier + * @copyright 2005-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id: GPG.php 305428 2010-11-17 02:47:56Z gauthierm $ + * @link http://pear.php.net/package/Crypt_GPG + * @link http://pear.php.net/manual/en/package.encryption.crypt-gpg.php + * @link http://www.gnupg.org/ + */ + +/** + * GPG key class + */ +require_once 'Crypt/GPG/Key.php'; + +/** + * GPG sub-key class + */ +require_once 'Crypt/GPG/SubKey.php'; + +/** + * GPG user id class + */ +require_once 'Crypt/GPG/UserId.php'; + +/** + * GPG process and I/O engine class + */ +require_once 'Crypt/GPG/Engine.php'; + +/** + * GPG exception classes + */ +require_once 'Crypt/GPG/Exceptions.php'; + +// {{{ class Crypt_GPGAbstract + +/** + * Base class for implementing a user of {@link Crypt_GPG_Engine} + * + * @category Encryption + * @package Crypt_GPG + * @author Nathan Fredrickson + * @author Michael Gauthier + * @copyright 2005-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ +abstract class Crypt_GPGAbstract +{ + // {{{ class error constants + + /** + * Error code returned when there is no error. + */ + const ERROR_NONE = 0; + + /** + * Error code returned when an unknown or unhandled error occurs. + */ + const ERROR_UNKNOWN = 1; + + /** + * Error code returned when a bad passphrase is used. + */ + const ERROR_BAD_PASSPHRASE = 2; + + /** + * Error code returned when a required passphrase is missing. + */ + const ERROR_MISSING_PASSPHRASE = 3; + + /** + * Error code returned when a key that is already in the keyring is + * imported. + */ + const ERROR_DUPLICATE_KEY = 4; + + /** + * Error code returned the required data is missing for an operation. + * + * This could be missing key data, missing encrypted data or missing + * signature data. + */ + const ERROR_NO_DATA = 5; + + /** + * Error code returned when an unsigned key is used. + */ + const ERROR_UNSIGNED_KEY = 6; + + /** + * Error code returned when a key that is not self-signed is used. + */ + const ERROR_NOT_SELF_SIGNED = 7; + + /** + * Error code returned when a public or private key that is not in the + * keyring is used. + */ + const ERROR_KEY_NOT_FOUND = 8; + + /** + * Error code returned when an attempt to delete public key having a + * private key is made. + */ + const ERROR_DELETE_PRIVATE_KEY = 9; + + /** + * Error code returned when one or more bad signatures are detected. + */ + const ERROR_BAD_SIGNATURE = 10; + + /** + * Error code returned when there is a problem reading GnuPG data files. + */ + const ERROR_FILE_PERMISSIONS = 11; + + /** + * Error code returned when a key could not be created. + */ + const ERROR_KEY_NOT_CREATED = 12; + + /** + * Error code returned when bad key parameters are used during key + * generation. + */ + const ERROR_BAD_KEY_PARAMS = 13; + + // }}} + // {{{ other class constants + + /** + * URI at which package bugs may be reported. + */ + const BUG_URI = 'http://pear.php.net/bugs/report.php?package=Crypt_GPG'; + + // }}} + // {{{ protected class properties + + /** + * Engine used to control the GPG subprocess + * + * @var Crypt_GPG_Engine + * + * @see Crypt_GPGAbstract::setEngine() + */ + protected $engine = null; + + // }}} + // {{{ __construct() + + /** + * Creates a new GPG object + * + * Available options are: + * + * - string homedir - the directory where the GPG + * keyring files are stored. If not + * specified, Crypt_GPG uses the + * default of ~/.gnupg. + * - string publicKeyring - the file path of the public + * keyring. Use this if the public + * keyring is not in the homedir, or + * if the keyring is in a directory + * not writable by the process + * invoking GPG (like Apache). Then + * you can specify the path to the + * keyring with this option + * (/foo/bar/pubring.gpg), and specify + * a writable directory (like /tmp) + * using the homedir option. + * - string privateKeyring - the file path of the private + * keyring. Use this if the private + * keyring is not in the homedir, or + * if the keyring is in a directory + * not writable by the process + * invoking GPG (like Apache). Then + * you can specify the path to the + * keyring with this option + * (/foo/bar/secring.gpg), and specify + * a writable directory (like /tmp) + * using the homedir option. + * - string trustDb - the file path of the web-of-trust + * database. Use this if the trust + * database is not in the homedir, or + * if the database is in a directory + * not writable by the process + * invoking GPG (like Apache). Then + * you can specify the path to the + * trust database with this option + * (/foo/bar/trustdb.gpg), and specify + * a writable directory (like /tmp) + * using the homedir option. + * - string binary - the location of the GPG binary. If + * not specified, the driver attempts + * to auto-detect the GPG binary + * location using a list of known + * default locations for the current + * operating system. The option + * gpgBinary is a + * deprecated alias for this option. + * - string agent - the location of the GnuPG agent + * binary. The gpg-agent is only + * used for GnuPG 2.x. If not + * specified, the engine attempts + * to auto-detect the gpg-agent + * binary location using a list of + * know default locations for the + * current operating system. + * - boolean debug - whether or not to use debug mode. + * When debug mode is on, all + * communication to and from the GPG + * subprocess is logged. This can be + * + * @param array $options optional. An array of options used to create the + * GPG object. All options are optional and are + * represented as key-value pairs. + * + * @throws Crypt_GPG_FileException if the homedir does not exist + * and cannot be created. This can happen if homedir is + * not specified, Crypt_GPG is run as the web user, and the web + * user has no home directory. This exception is also thrown if any + * of the options publicKeyring, + * privateKeyring or trustDb options are + * specified but the files do not exist or are are not readable. + * This can happen if the user running the Crypt_GPG process (for + * example, the Apache user) does not have permission to read the + * files. + * + * @throws PEAR_Exception if the provided binary is invalid, or + * if no binary is provided and no suitable binary could + * be found. + * + * @throws PEAR_Exception if the provided agent is invalid, or + * if no agent is provided and no suitable gpg-agent + * cound be found. + */ + public function __construct(array $options = array()) + { + $this->setEngine(new Crypt_GPG_Engine($options)); + } + + // }}} + // {{{ setEngine() + + /** + * Sets the I/O engine to use for GnuPG operations + * + * Normally this method does not need to be used. It provides a means for + * dependency injection. + * + * @param Crypt_GPG_Engine $engine the engine to use. + * + * @return Crypt_GPGAbstract the current object, for fluent interface. + */ + public function setEngine(Crypt_GPG_Engine $engine) + { + $this->engine = $engine; + return $this; + } + + // }}} + // {{{ _getKeys() + + /** + * Gets the available keys in the keyring + * + * Calls GPG with the --list-keys command and grabs keys. See + * the first section of doc/DETAILS in the + * {@link http://www.gnupg.org/download/ GPG package} for a detailed + * description of how the GPG command output is parsed. + * + * @param string $keyId optional. Only keys with that match the specified + * pattern are returned. The pattern may be part of + * a user id, a key id or a key fingerprint. If not + * specified, all keys are returned. + * + * @return array an array of {@link Crypt_GPG_Key} objects. If no keys + * match the specified $keyId an empty array is + * returned. + * + * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs. + * Use the debug option and file a bug report if these + * exceptions occur. + * + * @see Crypt_GPG_Key + */ + protected function _getKeys($keyId = '') + { + // get private key fingerprints + if ($keyId == '') { + $operation = '--list-secret-keys'; + } else { + $operation = '--list-secret-keys ' . escapeshellarg($keyId); + } + + // According to The file 'doc/DETAILS' in the GnuPG distribution, using + // double '--with-fingerprint' also prints the fingerprint for subkeys. + $arguments = array( + '--with-colons', + '--with-fingerprint', + '--with-fingerprint', + '--fixed-list-mode' + ); + + $output = ''; + + $this->engine->reset(); + $this->engine->setOutput($output); + $this->engine->setOperation($operation, $arguments); + $this->engine->run(); + + $code = $this->engine->getErrorCode(); + + switch ($code) { + case self::ERROR_NONE: + case self::ERROR_KEY_NOT_FOUND: + // ignore not found key errors + break; + case self::ERROR_FILE_PERMISSIONS: + $filename = $this->engine->getErrorFilename(); + if ($filename) { + throw new Crypt_GPG_FileException( + sprintf( + 'Error reading GnuPG data file \'%s\'. Check to make ' . + 'sure it is readable by the current user.', + $filename + ), + $code, + $filename + ); + } + throw new Crypt_GPG_FileException( + 'Error reading GnuPG data file. Check to make GnuPG data ' . + 'files are readable by the current user.', + $code + ); + default: + throw new Crypt_GPG_Exception( + 'Unknown error getting keys. Please use the \'debug\' option ' . + 'when creating the Crypt_GPG object, and file a bug report ' . + 'at ' . self::BUG_URI, + $code + ); + } + + $privateKeyFingerprints = array(); + + $lines = explode(PHP_EOL, $output); + foreach ($lines as $line) { + $lineExp = explode(':', $line); + if ($lineExp[0] == 'fpr') { + $privateKeyFingerprints[] = $lineExp[9]; + } + } + + // get public keys + if ($keyId == '') { + $operation = '--list-public-keys'; + } else { + $operation = '--list-public-keys ' . escapeshellarg($keyId); + } + + $output = ''; + + $this->engine->reset(); + $this->engine->setOutput($output); + $this->engine->setOperation($operation, $arguments); + $this->engine->run(); + + $code = $this->engine->getErrorCode(); + + switch ($code) { + case self::ERROR_NONE: + case self::ERROR_KEY_NOT_FOUND: + // ignore not found key errors + break; + case self::ERROR_FILE_PERMISSIONS: + $filename = $this->engine->getErrorFilename(); + if ($filename) { + throw new Crypt_GPG_FileException( + sprintf( + 'Error reading GnuPG data file \'%s\'. Check to make ' . + 'sure it is readable by the current user.', + $filename + ), + $code, + $filename + ); + } + throw new Crypt_GPG_FileException( + 'Error reading GnuPG data file. Check to make GnuPG data ' . + 'files are readable by the current user.', + $code + ); + default: + throw new Crypt_GPG_Exception( + 'Unknown error getting keys. Please use the \'debug\' option ' . + 'when creating the Crypt_GPG object, and file a bug report ' . + 'at ' . self::BUG_URI, + $code + ); + } + + $keys = array(); + + $key = null; // current key + $subKey = null; // current sub-key + + $lines = explode(PHP_EOL, $output); + foreach ($lines as $line) { + $lineExp = explode(':', $line); + + if ($lineExp[0] == 'pub') { + + // new primary key means last key should be added to the array + if ($key !== null) { + $keys[] = $key; + } + + $key = new Crypt_GPG_Key(); + + $subKey = Crypt_GPG_SubKey::parse($line); + $key->addSubKey($subKey); + + } elseif ($lineExp[0] == 'sub') { + + $subKey = Crypt_GPG_SubKey::parse($line); + $key->addSubKey($subKey); + + } elseif ($lineExp[0] == 'fpr') { + + $fingerprint = $lineExp[9]; + + // set current sub-key fingerprint + $subKey->setFingerprint($fingerprint); + + // if private key exists, set has private to true + if (in_array($fingerprint, $privateKeyFingerprints)) { + $subKey->setHasPrivate(true); + } + + } elseif ($lineExp[0] == 'uid') { + + $string = stripcslashes($lineExp[9]); // as per documentation + $userId = new Crypt_GPG_UserId($string); + + if ($lineExp[1] == 'r') { + $userId->setRevoked(true); + } + + $key->addUserId($userId); + + } + } + + // add last key + if ($key !== null) { + $keys[] = $key; + } + + return $keys; + } + + // }}} +} + +// }}} + +?> -- cgit v1.2.3