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