* * @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/ */ /** * Base class for GPG methods */ require_once 'Crypt/GPGAbstract.php'; /** * 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 { /** * 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; /** * Creates a new GnuPG key generator * * @param array $options An array of options used to create the object. * All options are optional and are represented as key-value * pairs. See Crypt_GPGAbstract::__construct() for more info. * * @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 * could be found. */ public function __construct(array $options = array()) { parent::__construct($options); } /** * 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; } /** * 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; } /** * 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) { $algorithm = 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; } /** * 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) { $algorithm = 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; } /** * 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), 'Handle' => $handle, ); if ($this->expirationDate != 0) { // GnuPG only accepts granularity of days $expirationDate = date('Y-m-d', $this->expirationDate); $keyParams['Expire-Date'] = $expirationDate; } if (strlen($this->passphrase)) { $keyParams['Passphrase'] = $this->passphrase; } $name = $userId->getName(); $email = $userId->getEmail(); $comment = $userId->getComment(); if (strlen($name) > 0) { $keyParams['Name-Real'] = $name; } if (strlen($email) > 0) { $keyParams['Name-Email'] = $email; } if (strlen($comment) > 0) { $keyParams['Name-Comment'] = $comment; } $keyParamsFormatted = array(); foreach ($keyParams as $name => $value) { $keyParamsFormatted[] = $name . ': ' . $value; } // This is required in GnuPG 2.1 if (!strlen($this->passphrase)) { $keyParamsFormatted[] = '%no-protection'; } $input = implode("\n", $keyParamsFormatted) . "\n%commit\n"; $this->engine->reset(); $this->engine->setProcessData('Handle', $handle); $this->engine->setInput($input); $this->engine->setOutput($output); $this->engine->setOperation('--gen-key', array('--batch')); try { $this->engine->run(); } catch (Crypt_GPG_InvalidKeyParamsException $e) { switch ($this->engine->getProcessData('LineNumber')) { 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 $e; } } $fingerprint = $this->engine->getProcessData('KeyCreated'); $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]; } /** * 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); } /** * 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; } }