* @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; } // }}} } // }}} ?>