/** * Handles a status line. * * @param string $line the status line to handle. */ public function handle($line) { $tokens = explode(' ', $line); switch ($tokens[0]) { case 'ENC_TO': // Now we know the message is encrypted. Set flag to check if // decryption succeeded. $this->decryptionOkay = false; // this is the new key message $this->currentSubKeyId = $tokens[1]; break; case 'NEED_PASSPHRASE': // send passphrase to the GPG engine $subKeyId = $tokens[1]; if (array_key_exists($subKeyId, $this->keys)) { $passphrase = $this->keys[$subKeyId]['passphrase']; $this->engine->sendCommand($passphrase); } else { $this->engine->sendCommand(''); } break; case 'USERID_HINT': // remember the user id for pretty exception messages $this->badPassphrases[$tokens[1]] = implode(' ', array_splice($tokens, 2)); break; case 'GOOD_PASSPHRASE': // if we got a good passphrase, remove the key from the list of // bad passphrases. unset($this->badPassphrases[$this->currentSubKeyId]); break; case 'MISSING_PASSPHRASE': $this->missingPassphrases[$this->currentSubKeyId] = $this->currentSubKeyId; break; case 'NO_SECKEY': // note: this message is also received if there are multiple // recipients and a previous key had a correct passphrase. $this->missingKeys[$tokens[1]] = $tokens[1]; break; case 'NODATA': $this->noData = true; break; case 'DECRYPTION_OKAY': // If the message is encrypted, this is the all-clear signal. $this->decryptionOkay = true; break; } }
/** * 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 // 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 // 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 // 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 // bad passphrase case 87: // bad PIN $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE; break; case 177: // no passphrase // 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; } }
/** * Decrypts and verifies encrypted, signed data * * @param string $data the encrypted signed data to be decrypted and * verified. * @param boolean $isFile whether or not the data is a filename. * @param string $outputFile the name of the file to which the decrypted * data should be written. If null, the decrypted * data is returned in the results array. * * @return array two element array. The array has an element 'data' * containing the decrypted data and an element * 'signatures' containing an array of * {@link Crypt_GPG_Signature} objects for the signed data. * If the decrypted data is written to a file, the 'data' * element is null. * * @throws Crypt_GPG_KeyNotFoundException if the private key needed to * decrypt the data is not in the user's keyring or it the public * key needed for verification is not in the user's keyring. * * @throws Crypt_GPG_NoDataException if specified data does not contain * GPG signed, encrypted data. * * @throws Crypt_GPG_BadPassphraseException if a required passphrase is * incorrect or if a required passphrase is not specified. See * {@link Crypt_GPG::addDecryptKey()}. * * @throws Crypt_GPG_FileException if the output file is not writeable or * if the input file is not readable. * * @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. * * @see Crypt_GPG_Signature */ protected function _decryptAndVerify($data, $isFile, $outputFile) { if ($isFile) { $input = @fopen($data, 'rb'); if ($input === false) { throw new Crypt_GPG_FileException('Could not open input file "' . $data . '" for decrypting and verifying.', 0, $data); } } else { $input = strval($data); if ($input == '') { throw new Crypt_GPG_NoDataException('No valid encrypted signed data found.', self::ERROR_NO_DATA); } } if ($outputFile === null) { $output = ''; } else { $output = @fopen($outputFile, 'wb'); if ($output === false) { if ($isFile) { fclose($input); } throw new Crypt_GPG_FileException('Could not open output file "' . $outputFile . '" for storing decrypted data.', 0, $outputFile); } } $verifyHandler = new Crypt_GPG_VerifyStatusHandler(); $decryptHandler = new Crypt_GPG_DecryptStatusHandler($this->engine, $this->decryptKeys); // If using gpg-agent, set the decrypt pins used by the pinentry $this->_setPinEntryEnv($this->decryptKeys); $this->engine->reset(); $this->engine->addStatusHandler(array($verifyHandler, 'handle')); $this->engine->addStatusHandler(array($decryptHandler, 'handle')); $this->engine->setInput($input); $this->engine->setOutput($output); $this->engine->setOperation('--decrypt'); $this->engine->run(); if ($isFile) { fclose($input); } if ($outputFile !== null) { fclose($output); } $return = array('data' => null, 'signatures' => $verifyHandler->getSignatures()); // if there was any problem decrypting the data, the handler will // deal with it here. try { $decryptHandler->throwException(); } catch (Exception $e) { if ($e instanceof Crypt_GPG_KeyNotFoundException) { throw new Crypt_GPG_KeyNotFoundException('Public key required for data verification not in ', 'the keyring. Either no suitable private decryption key ' . 'is in the keyring or the public key required for data ' . 'verification is not in the keyring. Import a suitable ' . 'key before trying to decrypt and verify this data.', self::ERROR_KEY_NOT_FOUND, $this->engine->getErrorKeyId()); } if ($e instanceof Crypt_GPG_NoDataException) { throw new Crypt_GPG_NoDataException('Cannot decrypt and verify data. No PGP encrypted data ' . 'was found in the provided data.', self::ERROR_NO_DATA); } throw $e; } if ($outputFile === null) { $return['data'] = $output; } return $return; }
/** * Creates a new GPG engine * * Available options are: * * - <kbd>string homedir</kbd> - the directory where the GPG * keyring files are stored. If not * specified, Crypt_GPG uses the * default of <kbd>~/.gnupg</kbd>. * - <kbd>string publicKeyring</kbd> - the file path of the public * keyring. Use this if the public * keyring is not in the homedir, or * if the keyring is in a directory * not writable by the process * invoking GPG (like Apache). Then * you can specify the path to the * keyring with this option * (/foo/bar/pubring.gpg), and specify * a writable directory (like /tmp) * using the <i>homedir</i> option. * - <kbd>string privateKeyring</kbd> - the file path of the private * keyring. Use this if the private * keyring is not in the homedir, or * if the keyring is in a directory * not writable by the process * invoking GPG (like Apache). Then * you can specify the path to the * keyring with this option * (/foo/bar/secring.gpg), and specify * a writable directory (like /tmp) * using the <i>homedir</i> option. * - <kbd>string trustDb</kbd> - the file path of the web-of-trust * database. Use this if the trust * database is not in the homedir, or * if the database is in a directory * not writable by the process * invoking GPG (like Apache). Then * you can specify the path to the * trust database with this option * (/foo/bar/trustdb.gpg), and specify * a writable directory (like /tmp) * using the <i>homedir</i> option. * - <kbd>string binary</kbd> - the location of the GPG binary. If * not specified, the driver attempts * to auto-detect the GPG binary * location using a list of known * default locations for the current * operating system. The option * <kbd>gpgBinary</kbd> is a * deprecated alias for this option. * - <kbd>boolean debug</kbd> - whether or not to use debug mode. * When debug mode is on, all * communication to and from the GPG * subprocess is logged. This can be * useful to diagnose errors when * using Crypt_GPG. * * @param array $options optional. An array of options used to create the * GPG object. All options are optional and are * represented as key-value pairs. * * @throws Crypt_GPG_FileException if the <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. */ public function __construct(array $options = array()) { $this->_isDarwin = strncmp(strtoupper(PHP_OS), 'DARWIN', 6) === 0; // populate mbstring overloading cache if not set if (self::$_mbStringOverload === null) { self::$_mbStringOverload = extension_loaded('mbstring') && (ini_get('mbstring.func_overload') & 0x2) === 0x2; } // get homedir if (array_key_exists('homedir', $options)) { $this->_homedir = (string) $options['homedir']; } else { // note: this requires the package OS dep exclude 'windows' $info = posix_getpwuid(posix_getuid()); $this->_homedir = $info['dir'] . '/.gnupg'; } // attempt to create homedir if it does not exist if (!is_dir($this->_homedir)) { if (@mkdir($this->_homedir, 0777, true)) { // Set permissions on homedir. Parent directories are created // with 0777, homedir is set to 0700. chmod($this->_homedir, 0700); } else { throw new Crypt_GPG_FileException('The \'homedir\' "' . $this->_homedir . '" is not readable or does not exist ' . 'and cannot be created. This can happen if \'homedir\' ' . 'is not specified in the Crypt_GPG options, Crypt_GPG is ' . 'run as the web user, and the web user has no home ' . 'directory.', 0, $this->_homedir); } } // get binary if (array_key_exists('binary', $options)) { $this->_binary = (string) $options['binary']; } elseif (array_key_exists('gpgBinary', $options)) { // deprecated alias $this->_binary = (string) $options['gpgBinary']; } else { $this->_binary = $this->_getBinary(); } if ($this->_binary == '' || !is_executable($this->_binary)) { throw new PEAR_Exception('GPG binary not found. If you are sure ' . 'the GPG binary is installed, please specify the location of ' . 'the GPG binary using the \'binary\' driver option.'); } /* * Note: * * Normally, GnuPG expects keyrings to be in the homedir and expects * to be able to write temporary files in the homedir. Sometimes, * keyrings are not in the homedir, or location of the keyrings does * not allow writing temporary files. In this case, the <i>homedir</i> * option by itself is not enough to specify the keyrings because GnuPG * can not write required temporary files. Additional options are * provided so you can specify the location of the keyrings separately * from the homedir. */ // get public keyring if (array_key_exists('publicKeyring', $options)) { $this->_publicKeyring = (string) $options['publicKeyring']; if (!is_readable($this->_publicKeyring)) { throw new Crypt_GPG_FileException('The \'publicKeyring\' "' . $this->_publicKeyring . '" does not exist or is ' . 'not readable. Check the location and ensure the file ' . 'permissions are correct.', 0, $this->_publicKeyring); } } // get private keyring if (array_key_exists('privateKeyring', $options)) { $this->_privateKeyring = (string) $options['privateKeyring']; if (!is_readable($this->_privateKeyring)) { throw new Crypt_GPG_FileException('The \'privateKeyring\' "' . $this->_privateKeyring . '" does not exist or is ' . 'not readable. Check the location and ensure the file ' . 'permissions are correct.', 0, $this->_privateKeyring); } } // get trust database if (array_key_exists('trustDb', $options)) { $this->_trustDb = (string) $options['trustDb']; if (!is_readable($this->_trustDb)) { throw new Crypt_GPG_FileException('The \'trustDb\' "' . $this->_trustDb . '" does not exist or is not readable. ' . 'Check the location and ensure the file permissions are ' . 'correct.', 0, $this->_trustDb); } } if (array_key_exists('debug', $options)) { $this->_debug = (bool) $options['debug']; } }
/** * Gets the available keys in the keyring. * * Calls GPG with the <kbd>--list-keys</kbd> command and grabs keys. See * the first section of <b>doc/DETAILS</b> in the * {@link http://www.gnupg.org/download/ GPG package} for a detailed * description of how the GPG command output is parsed. * * @param string $keyId optional. Only keys with that match the specified * pattern are returned. The pattern may be part of * a user id, a key id or a key fingerprint. If not * specified, all keys are returned. * * @return array an array of {@link Crypt_GPG_Key} objects. If no keys * match the specified <kbd>$keyId</kbd> an empty array is * returned. * * @throws Exception if an unknown or unexpected error occurs. * Use the <kbd>debug</kbd> option and file a bug report if these * exceptions occur. * * @see Crypt_GPG_Key */ protected function _getKeys($keyId = '') { // get private key fingerprints if ($keyId == '') { $operation = '--list-secret-keys'; } else { $operation = '--list-secret-keys ' . escapeshellarg($keyId); } // According to The file 'doc/DETAILS' in the GnuPG distribution, using // double '--with-fingerprint' also prints the fingerprint for subkeys. $arguments = array('--with-colons', '--with-fingerprint', '--with-fingerprint', '--fixed-list-mode'); $output = ''; $this->engine->reset(); $this->engine->setOutput($output); $this->engine->setOperation($operation, $arguments); $this->engine->run(); $code = $this->engine->getErrorCode(); switch ($code) { case self::ERROR_NONE: case self::ERROR_KEY_NOT_FOUND: // ignore not found key errors break; case self::ERROR_FILE_PERMISSIONS: $filename = $this->engine->getErrorFilename(); if ($filename) { throw new \Exception(sprintf('Error reading GnuPG data file \'%s\'. Check to make ' . 'sure it is readable by the current user.', $filename), $code); } throw new \Exception('Error reading GnuPG data file. Check to make GnuPG data ' . 'files are readable by the current user.', $code); default: throw new \Exception('Unknown error getting keys. Please use the \'debug\' option ', $code); } $privateKeyFingerprints = array(); $lines = explode(PHP_EOL, $output); foreach ($lines as $line) { $lineExp = explode(':', $line); if ($lineExp[0] == 'fpr') { $privateKeyFingerprints[] = $lineExp[9]; } } // get public keys if ($keyId == '') { $operation = '--list-public-keys'; } else { $operation = '--list-public-keys ' . escapeshellarg($keyId); } $output = ''; $this->engine->reset(); $this->engine->setOutput($output); $this->engine->setOperation($operation, $arguments); $this->engine->run(); $code = $this->engine->getErrorCode(); switch ($code) { case self::ERROR_NONE: case self::ERROR_KEY_NOT_FOUND: // ignore not found key errors break; case self::ERROR_FILE_PERMISSIONS: $filename = $this->engine->getErrorFilename(); if ($filename) { throw new \Exception(sprintf('Error reading GnuPG data file \'%s\'. Check to make ' . 'sure it is readable by the current user.', $filename), $code); } throw new \Exception('Error reading GnuPG data file. Check to make GnuPG data ' . 'files are readable by the current user.', $code); default: throw new \Exception('Unknown error getting keys. Please use the \'debug\' option ', $code); } $keys = array(); $key = null; // current key $subKey = null; // current sub-key $lines = explode(PHP_EOL, $output); foreach ($lines as $line) { $lineExp = explode(':', $line); if ($lineExp[0] == 'pub') { // new primary key means last key should be added to the array if ($key !== null) { $keys[] = $key; } $key = new Key(); $subKey = self::parseSubKeyLine($line); $key->addSubKey($subKey); } elseif ($lineExp[0] == 'sub') { $subKey = self::parseSubKeyLine($line); $key->addSubKey($subKey); } elseif ($lineExp[0] == 'fpr') { $fingerprint = $lineExp[9]; // set current sub-key fingerprint $subKey->setFingerprint($fingerprint); // if private key exists, set has private to true if (in_array($fingerprint, $privateKeyFingerprints)) { $subKey->setHasPrivate(true); } } elseif ($lineExp[0] == 'uid') { $string = stripcslashes($lineExp[9]); // as per documentation $userId = self::parseUserIdLine($string); // isRevoked if (strpos($lineExp[1], 'r') !== false) { $userId->setIsRevoked(true); } // isValid if (strpos($lineExp[1], 'i') !== false) { $userId->setIsValid(false); } // isValid if (strpos($lineExp[1], 'd') !== false) { $userId->setDisabled(true); } // validity if (strpos($lineExp[1], 'n') !== false) { //NEVER } elseif (strpos($lineExp[1], 'm') !== false) { //MARGINAL } elseif (strpos($lineExp[1], 'f') !== false) { //FULL } elseif (strpos($lineExp[1], 'u') !== false) { //ULTIMATE } $key->addUserId($userId); } } // add last key if ($key !== null) { $keys[] = $key; } return $keys; }
/** * Gets the available keys in the keyring * * Calls GPG with the <kbd>--list-keys</kbd> command and grabs keys. See * the first section of <b>doc/DETAILS</b> in the * {@link http://www.gnupg.org/download/ GPG package} for a detailed * description of how the GPG command output is parsed. * * @param string $keyId optional. Only keys with that match the specified * pattern are returned. The pattern may be part of * a user id, a key id or a key fingerprint. If not * specified, all keys are returned. * * @return array an array of {@link Crypt_GPG_Key} objects. If no keys * match the specified <kbd>$keyId</kbd> an empty array is * returned. * * @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. * * @see Crypt_GPG_Key */ protected function _getKeys($keyId = '') { // get private key fingerprints if ($keyId == '') { $operation = '--list-secret-keys'; } else { $operation = '--utf8-strings --list-secret-keys ' . escapeshellarg($keyId); } // According to The file 'doc/DETAILS' in the GnuPG distribution, using // double '--with-fingerprint' also prints the fingerprint for subkeys. $arguments = array('--with-colons', '--with-fingerprint', '--with-fingerprint', '--fixed-list-mode'); $output = ''; $this->engine->reset(); $this->engine->setOutput($output); $this->engine->setOperation($operation, $arguments); $this->engine->run(); $privateKeyFingerprints = array(); foreach (explode(PHP_EOL, $output) as $line) { $lineExp = explode(':', $line); if ($lineExp[0] == 'fpr') { $privateKeyFingerprints[] = $lineExp[9]; } } // get public keys if ($keyId == '') { $operation = '--list-public-keys'; } else { $operation = '--utf8-strings --list-public-keys ' . escapeshellarg($keyId); } $output = ''; $this->engine->reset(); $this->engine->setOutput($output); $this->engine->setOperation($operation, $arguments); $this->engine->run(); $keys = array(); $key = null; // current key $subKey = null; // current sub-key foreach (explode(PHP_EOL, $output) as $line) { $lineExp = explode(':', $line); if ($lineExp[0] == 'pub') { // new primary key means last key should be added to the array if ($key !== null) { $keys[] = $key; } $key = new Crypt_GPG_Key(); $subKey = Crypt_GPG_SubKey::parse($line); $key->addSubKey($subKey); } elseif ($lineExp[0] == 'sub') { $subKey = Crypt_GPG_SubKey::parse($line); $key->addSubKey($subKey); } elseif ($lineExp[0] == 'fpr') { $fingerprint = $lineExp[9]; // set current sub-key fingerprint $subKey->setFingerprint($fingerprint); // if private key exists, set has private to true if (in_array($fingerprint, $privateKeyFingerprints)) { $subKey->setHasPrivate(true); } } elseif ($lineExp[0] == 'uid') { $string = stripcslashes($lineExp[9]); // as per documentation $userId = new Crypt_GPG_UserId($string); if ($lineExp[1] == 'r') { $userId->setRevoked(true); } $key->addUserId($userId); } } // add last key if ($key !== null) { $keys[] = $key; } return $keys; }