<?php

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

namespace Crypt\GPG;

use Crypt\GPG;
use Crypt\GPG\Engine;
use Crypt\GPG\Exceptions;
use Crypt\GPG\Signature;
use Crypt\GPG\UserId;

/**
 * Status/Error handler for GPG process pipes.
 *
 * This class is used internally by {@link \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   https://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link      https://github.com/pear/Crypt_GPG
 */
class ProcessHandler
{
    /**
     * Engine used to control the GPG subprocess
     *
     * @var Engine
     */
    protected $engine;

    /**
     * The error code of the current operation
     *
     * @var int
     */
    protected $errorCode = 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 int
     */
    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 = [];

    /**
     * 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;

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

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

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

        if (!str_contains($operation, ' ')) {
            $op = trim($operation, '- ');
        } elseif (preg_match($regexp, $operation, $matches, PREG_OFFSET_CAPTURE)) {
            $op      = trim($matches[0][0], '-');
            $op_len  = $matches[0][1] + strlen($op) + 3;
            $command = substr($operation, $op_len, null);

            // 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'] = [];
    }

    /**
     * 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.
     */
    public function handleStatus($line): void
    {
        $tokens = explode(' ', $line);
        switch ($tokens[0]) {
            case 'NODATA':
                $this->errorCode = 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 = GPG::ERROR_NONE;
                break;

            case 'DELETE_PROBLEM':
                if ($tokens[1] == '1') {
                    $this->errorCode = GPG::ERROR_KEY_NOT_FOUND;
                    break;
                } elseif ($tokens[1] == '2') {
                    $this->errorCode = 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'] = [$tokens[2]];
                } elseif (!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 != GPG::ERROR_MISSING_PASSPHRASE
                    && $this->errorCode != GPG::ERROR_BAD_PASSPHRASE
                    && !(
                        $this->operation == 'decrypt'
                        && $tokens[0] == 'NO_PUBKEY'
                        && !empty($this->data['IgnoreVerifyErrors'])
                    )
                ) {
                    $this->errorCode = 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 (preg_match('/[1-9A-F]/', $keyId)) {
                    $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 = [];
                }

                // @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 = GPG::ERROR_BAD_PASSPHRASE;
                break;

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

                $this->errorCode = 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 = GPG::ERROR_BAD_SIGNATURE;
                // no break
            case 'GOODSIG':
                $signature = new 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 (strlen($tokens[1]) > 16) {
                    $signature->setKeyFingerprint($tokens[1]);
                    $signature->setKeyId(substr($tokens[1], -16));
                } else {
                    $signature->setKeyId($tokens[1]);
                }

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

                    $signature->setUserId(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 (!str_contains($tokens[3], 'T')) {
                    $signature->setCreationDate($tokens[3]);
                } else {
                    $signature->setCreationDate(strtotime($tokens[3]));
                }

                if (array_key_exists(4, $tokens)) {
                    if (!str_contains($tokens[4], 'T')) {
                        $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 = 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 = GPG::ERROR_BAD_PASSPHRASE;
                        break;

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

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

                break;
        }
    }

    /**
     * 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.
     */
    public function handleError($line): void
    {
        if (stripos($line, 'gpg: WARNING: ') !== false) {
            $this->data['Warnings'][] = substr($line, 14);
        }

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

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

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

        $pattern = '/No public key|public key not found/';
        if (preg_match($pattern, $line) === 1) {
            if ($this->operation == 'decrypt' && !empty($this->data['IgnoreVerifyErrors'])) {
                return;
            }

            $this->errorCode = 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 = 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 = GPG::ERROR_BAD_PASSPHRASE;
            } else {
                $this->errorCode = 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          = GPG::ERROR_BAD_KEY_PARAMS;
                $this->data['LineNumber'] = intval($matches[1]);
            }
        }
    }

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

        if ($this->errorCode === 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 " . GPG::BUG_URI;

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

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

                    case GPG::ERROR_FILE_PERMISSIONS:
                        if (!empty($this->data['ErrorFilename'])) {
                            throw new Exceptions\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 Exceptions\FileException(
                            'Error reading GnuPG data file. Check to make sure that '
                            . 'GnuPG data files are readable by the current user.',
                            $code
                        );

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

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

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

                    default:
                        throw new Exceptions\Exception(
                            'Unknown error deleting key. ' . $note,
                            $code
                        );
                }
                break; // @phpstan-ignore-line

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

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

                    default:
                        throw new Exceptions\Exception('Unknown error importing GPG key. ' . $note, $code);
                }
                break; // @phpstan-ignore-line

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

                    default:
                        throw new Exceptions\Exception('Unknown error exporting a key. ' . $note, $code);
                }
                break; // @phpstan-ignore-line

            case 'encrypt':
            case 'sign':
            case 'clearsign':
            case 'detach-sign':
                switch ($code) {
                    case GPG::ERROR_KEY_NOT_FOUND:
                        throw new Exceptions\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 GPG::ERROR_BAD_PASSPHRASE:
                        throw new Exceptions\BadPassphraseException(
                            'Cannot sign data. Incorrect passphrase provided.',
                            $code
                        );

                    case GPG::ERROR_MISSING_PASSPHRASE:
                        throw new Exceptions\BadPassphraseException(
                            'Cannot sign data. No passphrase provided.',
                            $code
                        );

                    default:
                        throw new Exceptions\Exception(
                            "Unknown error {$this->operation}ing data. $note",
                            $code
                        );
                }
                break; // @phpstan-ignore-line

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

                    case GPG::ERROR_NO_DATA:
                        throw new Exceptions\NoDataException(
                            'No valid signature data found.',
                            $code
                        );

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

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

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

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

                        throw new Exceptions\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 GPG::ERROR_BAD_PASSPHRASE:
                    case GPG::ERROR_MISSING_PASSPHRASE:
                        throw $this->badPassException($code, 'Cannot decrypt data.');

                    case GPG::ERROR_NO_DATA:
                        throw new Exceptions\NoDataException(
                            'Cannot decrypt data. No PGP encrypted data was found in '
                            . 'the provided data.',
                            $code
                        );

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

            case 'gen-key':
                switch ($code) {
                    case GPG::ERROR_BAD_KEY_PARAMS:
                        throw new Exceptions\InvalidKeyParamsException(
                            'Invalid key algorithm specified.',
                            $code
                        );

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

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

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

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

        return GPG::ERROR_UNKNOWN;
    }

    /**
     * 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 $this->data[$name] ?? null;
    }

    /**
     * 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
     */
    public function setData($name, $value): void
    {
        switch ($name) {
            case 'Handle':
                $this->data[$name] = strval($value);
                break;

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

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

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

        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 Exceptions\BadPassphraseException(
            $message,
            $code,
            $badPassphrases,
            $missingPassphrases
        );
    }

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

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

                // Get last X characters of key identifier to compare
                if ($keyIdLength < $_keyIdLength) {
                    $_keyId = substr($_keyId, -$keyIdLength);
                } elseif ($keyIdLength > $_keyIdLength) {
                    $keyId = substr($keyId, -$_keyIdLength);
                }

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

        return $passphrase;
    }
}
