<?php

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

/**
 * Crypt_GPG is a package to use GPG from PHP
 *
 * This file contains handler for status and error pipes of GPG process.
 *
 * PHP version 5
 *
 * 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    Nathan Fredrickson <nathan@silverorange.com>
 * @author    Michael Gauthier <mike@silverorange.com>
 * @author    Aleksander Machniak <alec@alec.pl>
 * @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/
 */

/**
 * GPG exception classes.
 */
require_once 'Crypt/GPG/Exceptions.php';

/**
 * Signature object class definition
 */
require_once 'Crypt/GPG/Signature.php';

// {{{ class Crypt_GPG_ProcessHandler

/**
 * Status/Error handler for GPG process pipes.
 *
 * This class is used internally by Crypt_GPG_Engine and does not need to be used
 * directly. See the {@link Crypt_GPG} class for end-user API.
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Nathan Fredrickson <nathan@silverorange.com>
 * @author    Michael Gauthier <mike@silverorange.com>
 * @author    Aleksander Machniak <alec@alec.pl>
 * @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_ProcessHandler
{
    // {{{ protected class properties

    /**
     * Engine used to control the GPG subprocess
     *
     * @var Crypt_GPG_Engine
     */
    protected $engine;

    /**
     * The error code of the current operation
     *
     * @var integer
     */
    protected $errorCode = Crypt_GPG::ERROR_NONE;

    /**
     * The number of currently needed passphrases
     *
     * If this is not zero when the GPG command is completed, the error code is
     * set to {@link Crypt_GPG::ERROR_MISSING_PASSPHRASE}.
     *
     * @var integer
     */
    protected $needPassphrase = 0;

    /**
     * Some data collected while processing the operation
     * or set for the operation
     *
     * @var array
     * @see self::setData()
     * @see self::getData()
     */
    protected $data = array();

    /**
     * The name of the current operation
     *
     * @var string
     * @see self::setOperation()
     */
    protected $operation = null;

    /**
     * The value of the argument of current operation
     *
     * @var string
     * @see self::setOperation()
     */
    protected $operationArg = null;

    // }}}
    // {{{ __construct()

    /**
     * Creates a new instance
     *
     * @param Crypt_GPG_Engine $engine Engine object
     */
    public function __construct($engine)
    {
        $this->engine = $engine;
    }

    // }}}
    // {{{ setOperation()

    /**
     * Sets the operation that is being performed by the engine.
     *
     * @param string $operation The GPG operation to perform.
     *
     * @return void
     */
    public function setOperation($operation)
    {
        $op    = null;
        $opArg = null;

        // Regexp matching all GPG "operational" arguments
        $regexp = '/--('
            . 'version|import|list-public-keys|list-secret-keys'
            . '|list-keys|delete-key|delete-secret-key|encrypt|sign|clearsign'
            . '|detach-sign|decrypt|verify|export-secret-keys|export|gen-key'
            . ')/';

        if (strpos($operation, ' ') === false) {
            $op = trim($operation, '- ');
        } else if (preg_match($regexp, $operation, $matches, PREG_OFFSET_CAPTURE)) {
            $op      = trim($matches[0][0], '-');
            $op_len  = $matches[0][1] + mb_strlen($op, '8bit') + 3;
            $command = mb_substr($operation, $op_len, null, '8bit');

            // we really need the argument if it is a key ID/fingerprint or email
            // address se we can use simplified regexp to "revert escapeshellarg()"
            if (preg_match('/^[\'"]([a-zA-Z0-9:@._-]+)[\'"]/', $command, $matches)) {
                $opArg = $matches[1];
            }
        }

        $this->operation        = $op;
        $this->operationArg     = $opArg;
        $this->data['Warnings'] = array();
    }

    // }}}
    // {{{ handleStatus()

    /**
     * Handles error values in the status output from GPG
     *
     * This method is responsible for setting the
     * {@link self::$errorCode}. See <b>doc/DETAILS</b> in the
     * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
     * information on GPG's status output.
     *
     * @param string $line the status line to handle.
     *
     * @return void
     */
    public function handleStatus($line)
    {
        $tokens = explode(' ', $line);
        switch ($tokens[0]) {
        case 'NODATA':
            $this->errorCode = Crypt_GPG::ERROR_NO_DATA;
            break;

        case 'DECRYPTION_OKAY':
            // If the message is encrypted, this is the all-clear signal.
            $this->data['DecryptionOkay'] = true;
            $this->errorCode = Crypt_GPG::ERROR_NONE;
            break;

        case 'DELETE_PROBLEM':
            if ($tokens[1] == '1') {
                $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
                break;
            } elseif ($tokens[1] == '2') {
                $this->errorCode = Crypt_GPG::ERROR_DELETE_PRIVATE_KEY;
                break;
            }
            break;

        case 'IMPORT_OK':
            $this->data['Import']['fingerprint'] = $tokens[2];

            if (empty($this->data['Import']['fingerprints'])) {
                $this->data['Import']['fingerprints'] = array($tokens[2]);
            } else if (!in_array($tokens[2], $this->data['Import']['fingerprints'])) {
                $this->data['Import']['fingerprints'][] = $tokens[2];
            }

            break;

        case 'IMPORT_RES':
            $this->data['Import']['public_imported']   = intval($tokens[3]);
            $this->data['Import']['public_unchanged']  = intval($tokens[5]);
            $this->data['Import']['private_imported']  = intval($tokens[11]);
            $this->data['Import']['private_unchanged'] = intval($tokens[12]);
            break;

        case 'NO_PUBKEY':
        case 'NO_SECKEY':
            $this->data['ErrorKeyId'] = $tokens[1];

            if ($this->errorCode != Crypt_GPG::ERROR_MISSING_PASSPHRASE
                && $this->errorCode != Crypt_GPG::ERROR_BAD_PASSPHRASE
            ) {
                $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
            }

            // note: this message is also received if there are multiple
            // recipients and a previous key had a correct passphrase.
            $this->data['MissingKeys'][$tokens[1]] = $tokens[1];

            // @FIXME: remove missing passphrase registered in ENC_TO handler
            //         This is for GnuPG 2.1
            unset($this->data['MissingPassphrases'][$tokens[1]]);
            break;

        case 'KEY_CONSIDERED':
            // In GnuPG 2.1.x exporting/importing a secret key requires passphrase
            // However, no NEED_PASSPRASE is returned, https://bugs.gnupg.org/gnupg/issue2667
            // So, handling KEY_CONSIDERED and GET_HIDDEN is needed.
            if (!array_key_exists('KeyConsidered', $this->data)) {
                $this->data['KeyConsidered'] = $tokens[1];
            }
            break;

        case 'USERID_HINT':
            // remember the user id for pretty exception messages
            // GnuPG 2.1.15 gives me: "USERID_HINT 0000000000000000 [?]"
            $keyId = $tokens[1];
            if (strcspn($keyId, '0')) {
                $username = implode(' ', array_splice($tokens, 2));
                $this->data['BadPassphrases'][$keyId] = $username;
            }
            break;

        case 'ENC_TO':
            // Now we know the message is encrypted. Set flag to check if
            // decryption succeeded.
            $this->data['DecryptionOkay'] = false;

            // this is the new key message
            $this->data['CurrentSubKeyId'] = $keyId = $tokens[1];

            // For some reason in GnuPG 2.1.11 I get only ENC_TO and no
            // NEED_PASSPHRASE/MISSING_PASSPHRASE/USERID_HINT
            // This is not needed for GnuPG 2.1.15
            if (!empty($_ENV['PINENTRY_USER_DATA'])) {
                $passphrases = json_decode($_ENV['PINENTRY_USER_DATA'], true);
            } else {
                $passphrases = array();
            }

            // @TODO: Get user name/email
            $this->data['BadPassphrases'][$keyId] = $keyId;
            if (empty($passphrases) || empty($passphrases[$keyId])) {
                $this->data['MissingPassphrases'][$keyId] = $keyId;
            }
            break;

        case 'GOOD_PASSPHRASE':
            // if we got a good passphrase, remove the key from the list of
            // bad passphrases.
            if (isset($this->data['CurrentSubKeyId'])) {
                unset($this->data['BadPassphrases'][$this->data['CurrentSubKeyId']]);
                unset($this->data['MissingPassphrases'][$this->data['CurrentSubKeyId']]);
            }

            $this->needPassphrase--;
            break;

        case 'BAD_PASSPHRASE':
            $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE;
            break;

        case 'MISSING_PASSPHRASE':
            if (isset($this->data['CurrentSubKeyId'])) {
                $this->data['MissingPassphrases'][$this->data['CurrentSubKeyId']]
                    = $this->data['CurrentSubKeyId'];
            }

            $this->errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE;
            break;

        case 'GET_HIDDEN':
            if ($tokens[1] == 'passphrase.enter' && isset($this->data['KeyConsidered'])) {
                $tokens[1] = $this->data['KeyConsidered'];
            } else {
                break;
            }
            // no break

        case 'NEED_PASSPHRASE':
            $passphrase = $this->getPin($tokens[1]);

            $this->engine->sendCommand($passphrase);

            if ($passphrase === '') {
                $this->needPassphrase++;
            }
            break;

        case 'SIG_CREATED':
            $this->data['SigCreated'] = $line;
            break;

        case 'SIG_ID':
            // note: signature id comes before new signature line and may not
            // exist for some signature types
            $this->data['SignatureId'] = $tokens[1];
            break;

        case 'EXPSIG':
        case 'EXPKEYSIG':
        case 'REVKEYSIG':
        case 'BADSIG':
        case 'ERRSIG':
            $this->errorCode = Crypt_GPG::ERROR_BAD_SIGNATURE;
            // no break
        case 'GOODSIG':
            $signature = new Crypt_GPG_Signature();

            // if there was a signature id, set it on the new signature
            if (!empty($this->data['SignatureId'])) {
                $signature->setId($this->data['SignatureId']);
                $this->data['SignatureId'] = '';
            }

            // Detect whether fingerprint or key id was returned and set
            // signature values appropriately. Key ids are strings of either
            // 16 or 8 hexadecimal characters. Fingerprints are strings of 40
            // hexadecimal characters. The key id is the last 16 characters of
            // the key fingerprint.
            if (mb_strlen($tokens[1], '8bit') > 16) {
                $signature->setKeyFingerprint($tokens[1]);
                $signature->setKeyId(mb_substr($tokens[1], -16, null, '8bit'));
            } else {
                $signature->setKeyId($tokens[1]);
            }

            // get user id string
            if ($tokens[0] != 'ERRSIG') {
                $string = implode(' ', array_splice($tokens, 2));
                $string = rawurldecode($string);

                $signature->setUserId(Crypt_GPG_UserId::parse($string));
            }

            $this->data['Signatures'][] = $signature;
            break;

        case 'VALIDSIG':
            if (empty($this->data['Signatures'])) {
                break;
            }

            $signature = end($this->data['Signatures']);

            $signature->setValid(true);
            $signature->setKeyFingerprint($tokens[1]);

            if (strpos($tokens[3], 'T') === false) {
                $signature->setCreationDate($tokens[3]);
            } else {
                $signature->setCreationDate(strtotime($tokens[3]));
            }

            if (array_key_exists(4, $tokens)) {
                if (strpos($tokens[4], 'T') === false) {
                    $signature->setExpirationDate($tokens[4]);
                } else {
                    $signature->setExpirationDate(strtotime($tokens[4]));
                }
            }

            break;

        case 'KEY_CREATED':
            if (isset($this->data['Handle']) && $tokens[3] == $this->data['Handle']) {
                $this->data['KeyCreated'] = $tokens[2];
            }
            break;

        case 'KEY_NOT_CREATED':
            if (isset($this->data['Handle']) && $tokens[1] == $this->data['Handle']) {
                $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_CREATED;
            }
            break;

        case 'PROGRESS':
            // todo: at some point, support reporting status async
            break;

        // GnuPG 2.1 uses FAILURE and ERROR responses
        case 'FAILURE':
        case 'ERROR':
            $errnum  = (int) $tokens[2];
            $source  = $errnum >> 24;
            $errcode = $errnum & 0xFFFFFF;

            switch ($errcode) {
            case 11: // bad passphrase
            case 87: // bad PIN
                $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE;
                break;

            case 177: // no passphrase
            case 178: // no PIN
                $this->errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE;
                break;

            case 58:
                $this->errorCode = Crypt_GPG::ERROR_NO_DATA;
                break;
            }

            break;
        }
    }

    // }}}
    // {{{ handleError()

    /**
     * Handles error values in the error output from GPG
     *
     * This method is responsible for setting the
     * {@link Crypt_GPG_Engine::$_errorCode}.
     *
     * @param string $line the error line to handle.
     *
     * @return void
     */
    public function handleError($line)
    {
        if (stripos($line, 'gpg: WARNING: ') !== false) {
            $this->data['Warnings'][] = substr($line, 14);
        }

        if ($this->errorCode !== Crypt_GPG::ERROR_NONE) {
            return;
        }

        $pattern = '/no valid OpenPGP data found/';
        if (preg_match($pattern, $line) === 1) {
            $this->errorCode = Crypt_GPG::ERROR_NO_DATA;
            return;
        }

        $pattern = '/No secret key|secret key not available/';
        if (preg_match($pattern, $line) === 1) {
            $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
            return;
        }

        $pattern = '/No public key|public key not found/';
        if (preg_match($pattern, $line) === 1) {
            $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
            return;
        }

        $pattern = '/can\'t (?:access|open) `(.*?)\'/';
        if (preg_match($pattern, $line, $matches) === 1) {
            $this->data['ErrorFilename'] = $matches[1];
            $this->errorCode = Crypt_GPG::ERROR_FILE_PERMISSIONS;
            return;
        }

        // GnuPG 2.1: It should return MISSING_PASSPHRASE, but it does not
        // we have to detect it this way. This happens e.g. on private key import
        $pattern = '/key ([0-9A-F]+).* (Bad|No) passphrase/';
        if (preg_match($pattern, $line, $matches) === 1) {
            $keyId = $matches[1];
            // @TODO: Get user name/email
            if (empty($this->data['BadPassphrases'][$keyId])) {
                $this->data['BadPassphrases'][$keyId] = $keyId;
            }

            if ($matches[2] == 'Bad') {
                $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE;
            } else {
                $this->errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE;
                if (empty($this->data['MissingPassphrases'][$keyId])) {
                    $this->data['MissingPassphrases'][$keyId] = $keyId;
                }
            }

            return;
        }

        if ($this->operation == 'gen-key') {
            $pattern = '/:([0-9]+): invalid algorithm$/';
            if (preg_match($pattern, $line, $matches) === 1) {
                $this->errorCode          = Crypt_GPG::ERROR_BAD_KEY_PARAMS;
                $this->data['LineNumber'] = intval($matches[1]);
            }
        }
    }

    // }}}
    // {{{ throwException()

    /**
     * On error throws exception
     *
     * @param int $exitcode GPG process exit code
     *
     * @return void
     * @throws Crypt_GPG_Exception
     */
    public function throwException($exitcode = 0)
    {
        if ($exitcode > 0 && $this->errorCode === Crypt_GPG::ERROR_NONE) {
            $this->errorCode = $this->setErrorCode($exitcode);
        }

        if ($this->errorCode === Crypt_GPG::ERROR_NONE) {
            return;
        }

        $code = $this->errorCode;
        $note = "Please use the 'debug' option when creating the Crypt_GPG " .
            "object, and file a bug report at " . Crypt_GPG::BUG_URI;

        switch ($this->operation) {
        case 'version':
            throw new Crypt_GPG_Exception(
                'Unknown error getting GnuPG version information. ' . $note,
                $code
            );

        case 'list-secret-keys':
        case 'list-public-keys':
        case 'list-keys':
            switch ($code) {
            case Crypt_GPG::ERROR_KEY_NOT_FOUND:
                // ignore not found key errors
                break;

            case Crypt_GPG::ERROR_FILE_PERMISSIONS:
                if (!empty($this->data['ErrorFilename'])) {
                    throw new Crypt_GPG_FileException(
                        sprintf(
                            'Error reading GnuPG data file \'%s\'. Check to make ' .
                            'sure it is readable by the current user.',
                            $this->data['ErrorFilename']
                        ),
                        $code,
                        $this->data['ErrorFilename']
                    );
                }
                throw new Crypt_GPG_FileException(
                    'Error reading GnuPG data file. Check to make sure that ' .
                    'GnuPG data files are readable by the current user.',
                    $code
                );

            default:
                throw new Crypt_GPG_Exception(
                    'Unknown error getting keys. ' . $note, $code
                );
            }
            break;

        case 'delete-key':
        case 'delete-secret-key':
            switch ($code) {
            case Crypt_GPG::ERROR_KEY_NOT_FOUND:
                throw new Crypt_GPG_KeyNotFoundException(
                    'Key not found: ' . $this->operationArg,
                    $code,
                    $this->operationArg
                );

            case Crypt_GPG::ERROR_DELETE_PRIVATE_KEY:
                throw new Crypt_GPG_DeletePrivateKeyException(
                    'Private key must be deleted before public key can be ' .
                    'deleted.',
                    $code,
                    $this->operationArg
                );

            default:
                throw new Crypt_GPG_Exception(
                    'Unknown error deleting key. ' . $note, $code
                );
            }
            break;

        case 'import':
            switch ($code) {
            case Crypt_GPG::ERROR_NO_DATA:
                throw new Crypt_GPG_NoDataException(
                    'No valid GPG key data found.', $code
                );

            case Crypt_GPG::ERROR_BAD_PASSPHRASE:
            case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
                throw $this->badPassException($code, 'Cannot import private key.');

            default:
                throw new Crypt_GPG_Exception(
                    'Unknown error importing GPG key. ' . $note, $code
                );
            }
            break;

        case 'export':
        case 'export-secret-keys':
            switch ($code) {
            case Crypt_GPG::ERROR_BAD_PASSPHRASE:
            case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
                throw $this->badPassException($code, 'Cannot export private key.');

            default:
                throw new Crypt_GPG_Exception(
                    'Unknown error exporting a key. ' . $note, $code
                );
            }
            break;

        case 'encrypt':
        case 'sign':
        case 'clearsign':
        case 'detach-sign':
            switch ($code) {
            case Crypt_GPG::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,
                    !empty($this->data['ErrorKeyId']) ? $this->data['ErrorKeyId'] : null
                );

            case Crypt_GPG::ERROR_BAD_PASSPHRASE:
                throw new Crypt_GPG_BadPassphraseException(
                    'Cannot sign data. Incorrect passphrase provided.', $code
                );

            case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
                throw new Crypt_GPG_BadPassphraseException(
                    'Cannot sign data. No passphrase provided.', $code
                );

            default:
                throw new Crypt_GPG_Exception(
                    "Unknown error {$this->operation}ing data. $note", $code
                );
            }
            break;

        case 'verify':
            switch ($code) {
            case Crypt_GPG::ERROR_BAD_SIGNATURE:
                // ignore bad signature errors
                break;

            case Crypt_GPG::ERROR_NO_DATA:
                throw new Crypt_GPG_NoDataException(
                    'No valid signature data found.', $code
                );

            case Crypt_GPG::ERROR_KEY_NOT_FOUND:
                throw new Crypt_GPG_KeyNotFoundException(
                    'Public key required for data verification not in keyring.',
                    $code,
                    !empty($this->data['ErrorKeyId']) ? $this->data['ErrorKeyId'] : null
                );

            default:
                throw new Crypt_GPG_Exception(
                    'Unknown error validating signature details. ' . $note,
                    $code
                );
            }
            break;

        case 'decrypt':
            switch ($code) {
            case Crypt_GPG::ERROR_BAD_SIGNATURE:
                // ignore bad signature errors
                break;

            case Crypt_GPG::ERROR_KEY_NOT_FOUND:
                if (!empty($this->data['MissingKeys'])) {
                    $keyId = reset($this->data['MissingKeys']);
                } else {
                    $keyId = '';
                }

                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
                );

            case Crypt_GPG::ERROR_BAD_PASSPHRASE:
            case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
                throw $this->badPassException($code, 'Cannot decrypt data.');

            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
                );

            default:
                throw new Crypt_GPG_Exception(
                    'Unknown error decrypting data.', $code
                );
            }
            break;

        case 'gen-key':
            switch ($code) {
            case Crypt_GPG::ERROR_BAD_KEY_PARAMS:
                throw new Crypt_GPG_InvalidKeyParamsException(
                    'Invalid key algorithm specified.', $code
                );

            default:
                throw new Crypt_GPG_Exception(
                    'Unknown error generating key-pair. ' . $note, $code
                );
            }
        }
    }

    // }}}
    // {{{ throwException()

    /**
     * Check exit code of the GPG operation.
     *
     * @param int $exitcode GPG process exit code
     *
     * @return int Internal error code
     */
    protected function setErrorCode($exitcode)
    {
        if ($this->needPassphrase > 0) {
            return Crypt_GPG::ERROR_MISSING_PASSPHRASE;
        }

        if ($this->operation == 'import') {
            return Crypt_GPG::ERROR_NONE;
        }

        if ($this->operation == 'decrypt' && !empty($this->data['DecryptionOkay'])) {
            if (!empty($this->data['IgnoreVerifyErrors'])) {
                return Crypt_GPG::ERROR_NONE;
            }
            if (!empty($this->data['MissingKeys'])) {
                return Crypt_GPG::ERROR_KEY_NOT_FOUND;
            }
        }

        return Crypt_GPG::ERROR_UNKNOWN;
    }

    // }}}
    // {{{ getData()

    /**
     * Get data from the last process execution.
     *
     * @param string $name Data element name:
     *               - SigCreated: The last SIG_CREATED status.
     *               - KeyConsidered: The last KEY_CONSIDERED status identifier.
     *               - KeyCreated: The KEY_CREATED status (for specified Handle).
     *               - Signatures: Signatures data from verification process.
     *               - LineNumber: Number of the gen-key error line.
     *               - Import: Result of IMPORT_OK/IMPORT_RES
     *               - Warnings: An array of all collected GnuPG warnings
     *
     * @return mixed
     */
    public function getData($name)
    {
        return isset($this->data[$name]) ? $this->data[$name] : null;
    }

    // }}}
    // {{{ setData()

    /**
     * Set data for the process execution.
     *
     * @param string $name  Data element name:
     *               - Handle: The unique key handle used by this handler
     *                         The key handle is used to track GPG status output
     *                         for a particular key on --gen-key command before
     *                         the key has its own identifier.
     *               - IgnoreVerifyErrors: Do not throw exceptions
     *                         when signature verification failes because
     *                         of a missing public key.
     * @param mixed  $value Data element value
     *
     * @return void
     */
    public function setData($name, $value)
    {
        switch ($name) {
        case 'Handle':
            $this->data[$name] = strval($value);
            break;

        case 'IgnoreVerifyErrors':
            $this->data[$name] = (bool) $value;
            break;
        }
    }

    // }}}
    // {{{ setData()

    /**
     * Create Crypt_GPG_BadPassphraseException from operation data.
     *
     * @param int    $code    Error code
     * @param string $message Error message
     *
     * @return Crypt_GPG_BadPassphraseException
     */
    protected function badPassException($code, $message)
    {
        $badPassphrases = array_diff_key(
            isset($this->data['BadPassphrases']) ? $this->data['BadPassphrases'] : array(),
            isset($this->data['MissingPassphrases']) ? $this->data['MissingPassphrases'] : array()
        );

        $missingPassphrases = array_intersect_key(
            isset($this->data['BadPassphrases']) ? $this->data['BadPassphrases'] : array(),
            isset($this->data['MissingPassphrases']) ? $this->data['MissingPassphrases'] : array()
        );

        if (count($badPassphrases) > 0) {
            $message .= ' Incorrect passphrase provided for keys: "' .
                implode('", "', $badPassphrases) . '".';
        }
        if (count($missingPassphrases) > 0) {
            $message .= ' No passphrase provided for keys: "' .
                implode('", "', $missingPassphrases) . '".';
        }

        return new Crypt_GPG_BadPassphraseException(
            $message,
            $code,
            $badPassphrases,
            $missingPassphrases
        );
    }

    // }}}
    // {{{ getPin()

    /**
     * Get registered passphrase for specified key.
     *
     * @param string $key Key identifier
     *
     * @return string Passphrase
     */
    protected function getPin($key)
    {
        $passphrase  = '';
        $keyIdLength = mb_strlen($key, '8bit');

        if ($keyIdLength && !empty($_ENV['PINENTRY_USER_DATA'])) {
            $passphrases = json_decode($_ENV['PINENTRY_USER_DATA'], true);
            foreach ($passphrases as $_keyId => $pass) {
                $keyId        = $key;
                $_keyIdLength = mb_strlen($_keyId, '8bit');

                // Get last X characters of key identifier to compare
                if ($keyIdLength < $_keyIdLength) {
                    $_keyId = mb_substr($_keyId, -$keyIdLength, null, '8bit');
                } else if ($keyIdLength > $_keyIdLength) {
                    $keyId = mb_substr($keyId, -$_keyIdLength, null, '8bit');
                }

                if ($_keyId === $keyId) {
                    $passphrase = $pass;
                    break;
                }
            }
        }

        return $passphrase;
    }

    // }}}
}

// }}}

?>


<?php
/**
 * Part of Crypt_GPG
 *
 * PHP version 5
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Christian Weiske <cweiske@php.net>
 * @copyright 2015 PEAR
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @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/
 */

/**
 * Information about a recently created signature.
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Christian Weiske <cweiske@php.net>
 * @copyright 2015 PEAR
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @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/
 */
class Crypt_GPG_SignatureCreationInfo
{
    /**
     * One of the three signature types:
     * - {@link Crypt_GPG::SIGN_MODE_NORMAL}
     * - {@link Crypt_GPG::SIGN_MODE_CLEAR}
     * - {@link Crypt_GPG::SIGN_MODE_DETACHED}
     *
     * @var integer
     */
    protected $mode;

    /**
     * Public Key algorithm
     *
     * @var integer
     */
    protected $pkAlgorithm;

    /**
     * Algorithm to hash the data
     *
     * @see RFC 2440 / 9.4. Hash Algorithm
     * @var integer
     */
    protected $hashAlgorithm;

    /**
     * OpenPGP signature class
     *
     * @var mixed
     */
    protected $class;

    /**
     * Unix timestamp when the signature was created
     *
     * @var integer
     */
    protected $timestamp;

    /**
     * Key fingerprint
     *
     * @var string
     */
    protected $keyFingerprint;

    /**
     * If the line given to the constructor was valid
     *
     * @var boolean
     */
    protected $valid;

    /**
     * Names for the hash algorithm IDs.
     *
     * Names taken from RFC 3156, without the leading "pgp-".
     *
     * @see RFC 2440 / 9.4. Hash Algorithm
     * @see RFC 3156 / 5. OpenPGP signed data
     * @var array
     */
    protected static $hashAlgorithmNames = array(
        1 => 'md5',
        2 => 'sha1',
        3 => 'ripemd160',
        5 => 'md2',
        6 => 'tiger192',
        7 => 'haval-5-160',
        8 => 'sha256',
        9 => 'sha384',
        10 => 'sha512',
        11 => 'sha224',
    );

    /**
     * Parse a SIG_CREATED line from gnupg
     *
     * @param string $sigCreatedLine Line beginning with "SIG_CREATED "
     */
    public function __construct($sigCreatedLine = null)
    {
        if ($sigCreatedLine === null) {
            $this->valid = false;
            return;
        }

        $parts = explode(' ', $sigCreatedLine);
        if (count($parts) !== 7) {
            $this->valid = false;
            return;
        }
        list(
            $title, $mode, $pkAlgorithm, $hashAlgorithm,
            $class, $timestamp, $keyFingerprint
        ) = $parts;

        switch (strtoupper($mode[0])) {
        case 'D':
            $this->mode = Crypt_GPG::SIGN_MODE_DETACHED;
            break;
        case 'C':
            $this->mode = Crypt_GPG::SIGN_MODE_CLEAR;
            break;
        case 'S':
            $this->mode = Crypt_GPG::SIGN_MODE_NORMAL;
            break;
        }

        $this->pkAlgorithm    = (int) $pkAlgorithm;
        $this->hashAlgorithm  = (int) $hashAlgorithm;
        $this->class          = $class;
        if (is_numeric($timestamp)) {
            $this->timestamp  = (int) $timestamp;
        } else {
            $this->timestamp  = strtotime($timestamp);
        }
        $this->keyFingerprint = $keyFingerprint;
        $this->valid = true;
    }

    /**
     * Get the signature type
     * - {@link Crypt_GPG::SIGN_MODE_NORMAL}
     * - {@link Crypt_GPG::SIGN_MODE_CLEAR}
     * - {@link Crypt_GPG::SIGN_MODE_DETACHED}
     *
     * @return integer
     */
    public function getMode()
    {
        return $this->mode;
    }

    /**
     * Return the public key algorithm used.
     *
     * @return integer
     */
    public function getPkAlgorithm()
    {
        return $this->pkAlgorithm;
    }

    /**
     * Return the hash algorithm used to hash the data to sign.
     *
     * @return integer
     */
    public function getHashAlgorithm()
    {
        return $this->hashAlgorithm;
    }

    /**
     * Get a name for the used hashing algorithm.
     *
     * @return string|null
     */
    public function getHashAlgorithmName()
    {
        if (!isset(self::$hashAlgorithmNames[$this->hashAlgorithm])) {
            return null;
        }
        return self::$hashAlgorithmNames[$this->hashAlgorithm];
    }

    /**
     * Return the timestamp at which the signature was created
     *
     * @return integer
     */
    public function getTimestamp()
    {
        return $this->timestamp;
    }

    /**
     * Return the key's fingerprint
     *
     * @return string
     */
    public function getKeyFingerprint()
    {
        return $this->keyFingerprint;
    }

    /**
     * Tell if the fingerprint line given to the constructor was valid
     *
     * @return boolean
     */
    public function isValid()
    {
        return $this->valid;
    }
}
?>


<?php

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

/**
 * Contains a class representing GPG sub-keys and constants for GPG algorithms
 *
 * PHP version 5
 *
 * 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>
 * @author    Nathan Fredrickson <nathan@silverorange.com>
 * @copyright 2005-2010 silverorange
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link      http://pear.php.net/package/Crypt_GPG
 */

// {{{ class Crypt_GPG_SubKey

/**
 * A class for GPG sub-key information
 *
 * This class is used to store the results of the {@link Crypt_GPG::getKeys()}
 * method. Sub-key objects are members of a {@link Crypt_GPG_Key} object.
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Michael Gauthier <mike@silverorange.com>
 * @author    Nathan Fredrickson <nathan@silverorange.com>
 * @copyright 2005-2010 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()
 * @see       Crypt_GPG_Key::getSubKeys()
 */
class Crypt_GPG_SubKey
{
    // {{{ algorithm class constants

    /**
     * RSA encryption algorithm.
     */
    const ALGORITHM_RSA = 1;

    /**
     * Elgamal encryption algorithm (encryption only).
     */
    const ALGORITHM_ELGAMAL_ENC = 16;

    /**
     * DSA encryption algorithm (sometimes called DH, sign only).
     */
    const ALGORITHM_DSA = 17;

    /**
     * Elgamal encryption algorithm (signage and encryption - should not be
     * used).
     */
    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

    /**
     * The id of this sub-key
     *
     * @var string
     */
    private $_id = '';

    /**
     * The algorithm used to create this sub-key
     *
     * The value is one of the Crypt_GPG_SubKey::ALGORITHM_* constants.
     *
     * @var integer
     */
    private $_algorithm = 0;

    /**
     * The fingerprint of this sub-key
     *
     * @var string
     */
    private $_fingerprint = '';

    /**
     * Length of this sub-key in bits
     *
     * @var integer
     */
    private $_length = 0;

    /**
     * Date this sub-key was created
     *
     * This is a Unix timestamp.
     *
     * @var integer
     */
    private $_creationDate = 0;

    /**
     * Date this sub-key expires
     *
     * This is a Unix timestamp. If this sub-key does not expire, this will be
     * zero.
     *
     * @var integer
     */
    private $_expirationDate = 0;

    /**
     * Contains usage flags of this sub-key
     *
     * @var int
     */
    private $_usage = 0;

    /**
     * Whether or not the private key for this sub-key exists in the keyring
     *
     * @var boolean
     */
    private $_hasPrivate = false;

    /**
     * Whether or not this sub-key is revoked
     *
     * @var boolean
     */
    private $_isRevoked = false;

    // }}}
    // {{{ __construct()

    /**
     * Creates a new sub-key object
     *
     * Sub-keys can be initialized from an array of named values. Available
     * names are:
     *
     * - string  id          - the key id of the sub-key.
     * - integer algorithm   - the encryption algorithm of the
     *                                    sub-key.
     * - string  fingerprint - the fingerprint of the sub-key. The
     *                                    fingerprint should not contain
     *                                    formatting characters.
     * - integer length      - the length of the sub-key in bits.
     * - integer creation    - the date the sub-key was created.
     *                                    This is a UNIX timestamp.
     * - integer expiration  - the date the sub-key expires. This
     *                                    is a UNIX timestamp. If the sub-key
     *                                    does not expire, use 0.
     * - boolean canSign     - whether or not the sub-key can be
     *                                    used to sign data.
     * - boolean canEncrypt  - whether or not the sub-key can be
     *                                    used to encrypt data.
     * - integer usage       - the sub-key usage flags
     * - boolean hasPrivate  - whether or not the private key for
     *                                    the sub-key exists in the keyring.
     * - boolean isRevoked   - whether or not this sub-key is
     *                                    revoked.
     *
     * @param Crypt_GPG_SubKey|string|array $key optional. Either an existing
     *        sub-key object, which is copied; a sub-key string, which is
     *        parsed; or an array of initial values.
     */
    public function __construct($key = null)
    {
        // parse from string
        if (is_string($key)) {
            $key = self::parse($key);
        }

        // copy from object
        if ($key instanceof Crypt_GPG_SubKey) {
            $this->_id             = $key->_id;
            $this->_algorithm      = $key->_algorithm;
            $this->_fingerprint    = $key->_fingerprint;
            $this->_length         = $key->_length;
            $this->_creationDate   = $key->_creationDate;
            $this->_expirationDate = $key->_expirationDate;
            $this->_usage          = $key->_usage;
            $this->_hasPrivate     = $key->_hasPrivate;
            $this->_isRevoked      = $key->_isRevoked;
        }

        // initialize from array
        if (is_array($key)) {
            if (array_key_exists('id', $key)) {
                $this->setId($key['id']);
            }

            if (array_key_exists('algorithm', $key)) {
                $this->setAlgorithm($key['algorithm']);
            }

            if (array_key_exists('fingerprint', $key)) {
                $this->setFingerprint($key['fingerprint']);
            }

            if (array_key_exists('length', $key)) {
                $this->setLength($key['length']);
            }

            if (array_key_exists('creation', $key)) {
                $this->setCreationDate($key['creation']);
            }

            if (array_key_exists('expiration', $key)) {
                $this->setExpirationDate($key['expiration']);
            }

            if (array_key_exists('usage', $key)) {
                $this->setUsage($key['usage']);
            }

            if (array_key_exists('canSign', $key)) {
                $this->setCanSign($key['canSign']);
            }

            if (array_key_exists('canEncrypt', $key)) {
                $this->setCanEncrypt($key['canEncrypt']);
            }

            if (array_key_exists('hasPrivate', $key)) {
                $this->setHasPrivate($key['hasPrivate']);
            }

            if (array_key_exists('isRevoked', $key)) {
                $this->setRevoked($key['isRevoked']);
            }
        }
    }

    // }}}
    // {{{ getId()

    /**
     * Gets the id of this sub-key
     *
     * @return string the id of this sub-key.
     */
    public function getId()
    {
        return $this->_id;
    }

    // }}}
    // {{{ getAlgorithm()

    /**
     * Gets the algorithm used by this sub-key
     *
     * The algorithm should be one of the Crypt_GPG_SubKey::ALGORITHM_*
     * constants.
     *
     * @return integer the algorithm used by this sub-key.
     */
    public function getAlgorithm()
    {
        return $this->_algorithm;
    }

    // }}}
    // {{{ getCreationDate()

    /**
     * Gets the creation date of this sub-key
     *
     * This is a Unix timestamp.
     *
     * @return integer the creation date of this sub-key.
     */
    public function getCreationDate()
    {
        return $this->_creationDate;
    }

    // }}}
    // {{{ getExpirationDate()

    /**
     * Gets the date this sub-key expires
     *
     * This is a Unix timestamp. If this sub-key does not expire, this will be
     * zero.
     *
     * @return integer the date this sub-key expires.
     */
    public function getExpirationDate()
    {
        return $this->_expirationDate;
    }

    // }}}
    // {{{ getFingerprint()

    /**
     * Gets the fingerprint of this sub-key
     *
     * @return string the fingerprint of this sub-key.
     */
    public function getFingerprint()
    {
        return $this->_fingerprint;
    }

    // }}}
    // {{{ getLength()

    /**
     * Gets the length of this sub-key in bits
     *
     * @return integer the length of this sub-key in bits.
     */
    public function getLength()
    {
        return $this->_length;
    }

    // }}}
    // {{{ canSign()

    /**
     * Gets whether or not this sub-key can sign data
     *
     * @return boolean true if this sub-key can sign data and false if this
     *                 sub-key can not sign data.
     */
    public function canSign()
    {
        return ($this->_usage & self::USAGE_SIGN) != 0;
    }

    // }}}
    // {{{ canEncrypt()

    /**
     * Gets whether or not this sub-key can encrypt data
     *
     * @return boolean true if this sub-key can encrypt data and false if this
     *                 sub-key can not encrypt data.
     */
    public function canEncrypt()
    {
        return ($this->_usage & self::USAGE_ENCRYPT) != 0;
    }

    // }}}
    // {{{ usage()

    /**
     * Gets usage flags of this sub-key
     *
     * @return int Sum of usage flags
     */
    public function usage()
    {
        return $this->_usage;
    }

    // }}}
    // {{{ hasPrivate()

    /**
     * Gets whether or not the private key for this sub-key exists in the
     * keyring
     *
     * @return boolean true the private key for this sub-key exists in the
     *                 keyring and false if it does not.
     */
    public function hasPrivate()
    {
        return $this->_hasPrivate;
    }

    // }}}
    // {{{ isRevoked()

    /**
     * Gets whether or not this sub-key is revoked
     *
     * @return boolean true if this sub-key is revoked and false if it is not.
     */
    public function isRevoked()
    {
        return $this->_isRevoked;
    }

    // }}}
    // {{{ setCreationDate()

    /**
     * Sets the creation date of this sub-key
     *
     * The creation date is a Unix timestamp.
     *
     * @param integer $creationDate the creation date of this sub-key.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setCreationDate($creationDate)
    {
        $this->_creationDate = intval($creationDate);
        return $this;
    }

    // }}}
    // {{{ setExpirationDate()

    /**
     * Sets the expiration date of this sub-key
     *
     * The expiration date is a Unix timestamp. Specify zero if this sub-key
     * does not expire.
     *
     * @param integer $expirationDate the expiration date of this sub-key.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setExpirationDate($expirationDate)
    {
        $this->_expirationDate = intval($expirationDate);
        return $this;
    }

    // }}}
    // {{{ setId()

    /**
     * Sets the id of this sub-key
     *
     * @param string $id the id of this sub-key.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setId($id)
    {
        $this->_id = strval($id);
        return $this;
    }

    // }}}
    // {{{ setAlgorithm()

    /**
     * Sets the algorithm used by this sub-key
     *
     * @param integer $algorithm the algorithm used by this sub-key.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setAlgorithm($algorithm)
    {
        $this->_algorithm = intval($algorithm);
        return $this;
    }

    // }}}
    // {{{ setFingerprint()

    /**
     * Sets the fingerprint of this sub-key
     *
     * @param string $fingerprint the fingerprint of this sub-key.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setFingerprint($fingerprint)
    {
        $this->_fingerprint = strval($fingerprint);
        return $this;
    }

    // }}}
    // {{{ setLength()

    /**
     * Sets the length of this sub-key in bits
     *
     * @param integer $length the length of this sub-key in bits.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setLength($length)
    {
        $this->_length = intval($length);
        return $this;
    }

    // }}}
    // {{{ setCanSign()

    /**
     * Sets whether or not this sub-key can sign data
     *
     * @param boolean $canSign true if this sub-key can sign data and false if
     *                         it can not.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setCanSign($canSign)
    {
        if ($canSign) {
            $this->_usage |= self::USAGE_SIGN;
        } else {
            $this->_usage &= ~self::USAGE_SIGN;
        }

        return $this;
    }

    // }}}
    // {{{ setCanEncrypt()

    /**
     * Sets whether or not this sub-key can encrypt data
     *
     * @param boolean $canEncrypt true if this sub-key can encrypt data and
     *                            false if it can not.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setCanEncrypt($canEncrypt)
    {
        if ($canEncrypt) {
            $this->_usage |= self::USAGE_ENCRYPT;
        } else {
            $this->_usage &= ~self::USAGE_ENCRYPT;
        }

        return $this;
    }

    // }}}
    // {{{ setUsage()

    /**
     * Sets usage flags of the sub-key
     *
     * @param integer $usage Usage flags
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setUsage($usage)
    {
        $this->_usage = (int) $usage;
        return $this;
    }

    // }}}
    // {{{ setHasPrivate()

    /**
     * Sets whether of not the private key for this sub-key exists in the
     * keyring
     *
     * @param boolean $hasPrivate true if the private key for this sub-key
     *                            exists in the keyring and false if it does
     *                            not.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setHasPrivate($hasPrivate)
    {
        $this->_hasPrivate = ($hasPrivate) ? true : false;
        return $this;
    }

    // }}}
    // {{{ setRevoked()

    /**
     * Sets whether or not this sub-key is revoked
     *
     * @param boolean $isRevoked whether or not this sub-key is revoked.
     *
     * @return Crypt_GPG_SubKey the current object, for fluent interface.
     */
    public function setRevoked($isRevoked)
    {
        $this->_isRevoked = ($isRevoked) ? true : false;
        return $this;
    }

    // }}}
    // {{{ parse()

    /**
     * Parses a sub-key object from a sub-key string
     *
     * See doc/DETAILS in the
     * {@link http://www.gnupg.org/download/ GPG distribution} for information
     * on how the sub-key string is parsed.
     *
     * @param string $string the string containing the sub-key.
     *
     * @return Crypt_GPG_SubKey the sub-key object parsed from the string.
     */
    public static function parse($string)
    {
        $tokens = explode(':', $string);

        $subKey = new Crypt_GPG_SubKey();

        $subKey->setId($tokens[4]);
        $subKey->setLength($tokens[2]);
        $subKey->setAlgorithm($tokens[3]);
        $subKey->setCreationDate(self::_parseDate($tokens[5]));
        $subKey->setExpirationDate(self::_parseDate($tokens[6]));

        if ($tokens[1] == 'r') {
            $subKey->setRevoked(true);
        }

        $usage = 0;
        $usage_map = array(
            'a' => self::USAGE_AUTHENTICATION,
            'c' => self::USAGE_CERTIFY,
            'e' => self::USAGE_ENCRYPT,
            's' => self::USAGE_SIGN,
        );

        foreach ($usage_map as $key => $flag) {
            if (strpos($tokens[11], $key) !== false) {
                $usage |= $flag;
            }
        }

        $subKey->setUsage($usage);

        return $subKey;
    }

    // }}}
    // {{{ _parseDate()

    /**
     * Parses a date string as provided by GPG into a UNIX timestamp
     *
     * @param string $string the date string.
     *
     * @return integer the UNIX timestamp corresponding to the provided date
     *                 string.
     */
    private static function _parseDate($string)
    {
        if ($string == '') {
            $timestamp = 0;
        } else {
            // all times are in UTC according to GPG documentation
            $timeZone = new DateTimeZone('UTC');

            if (strpos($string, 'T') === false) {
                // interpret as UNIX timestamp
                $string = '@' . $string;
            }

            $date = new DateTime($string, $timeZone);

            // convert to UNIX timestamp
            $timestamp = intval($date->format('U'));
        }

        return $timestamp;
    }

    // }}}
}

// }}}

?>