<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Crypt_GPG is a package to use GPG from PHP
 *
 * This file contains an object that handles GnuPG key generation.
 *
 * LICENSE:
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see
 * <http://www.gnu.org/licenses/>
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Michael Gauthier <mike@silverorange.com>
 * @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 <b>doc/DETAILS</b> 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 <nathan@silverorange.com>
 * @author    Michael Gauthier <mike@silverorange.com>
 * @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 int
     *
     * @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 int
     *
     * @see Crypt_GPG_KeyGenerator::setKeyParams()
     */
    protected $keyAlgorithm = Crypt_GPG_SubKey::ALGORITHM_DSA;

    /**
     * The size of generated primary keys
     *
     * @var int
     *
     * @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 int
     *
     * @see Crypt_GPG_KeyGenerator::setKeyParams()
     */
    protected $keyUsage = 6; // USAGE_SIGN | USAGE_CERTIFY

    /**
     * The algorithm for generated sub-keys
     *
     * @var int
     *
     * @see Crypt_GPG_KeyGenerator::setSubKeyParams()
     */
    protected $subKeyAlgorithm = Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC;

    /**
     * The size of generated sub-keys
     *
     * @var int
     *
     * @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 int
     *
     * @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 <kbd>homedir</kbd> does not exist
     *         and cannot be created. This can happen if <kbd>homedir</kbd> 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 <kbd>publicKeyring</kbd>,
     *         <kbd>privateKeyring</kbd> or <kbd>trustDb</kbd> 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 <kbd>binary</kbd> is invalid, or
     *         if no <kbd>binary</kbd> is provided and no suitable binary could
     *         be found.
     *
     * @throws PEAR_Exception if the provided <kbd>agent</kbd> is invalid, or
     *         if no <kbd>agent</kbd> is provided and no suitable gpg-agent
     *         could be found.
     */
    public function __construct(array $options = [])
    {
        parent::__construct($options);
    }

    /**
     * Sets the expiration date of generated keys
     *
     * @param string|int $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 int $algorithm the algorithm used by the key. This should be
     *                       one of the Crypt_GPG_SubKey::ALGORITHM_*
     *                       constants.
     * @param int $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 int $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 int $algorithm The algorithm used by the key. This should be
     *                       one of the Crypt_GPG_SubKey::ALGORITHM_*
     *                       constants.
     * @param int $size      Optional 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 int $usage     Optional 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 = 0, $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 <i>$name</i> is
     *                                         specified as a string, this is
     *                                         the email address of the user id.
     * @param string                  $comment optional. If <i>$name</i> 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 <kbd>debug</kbd> 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 = [
            '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 = [];
        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', ['--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 <b>doc/DETAILS</b> in the
     * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
     * information on the key usage format.
     *
     * @param int $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 = [
            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 = [];

        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 <i>$name</i> is
     *                                         specified as a string, this is
     *                                         the email address of the user id.
     * @param string                  $comment optional. If <i>$name</i> 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;
    }
}
