* @copyright 2008-2009 silverorange * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 * @version CVS: $Id: DecryptStatusHandler.php 302814 2010-08-26 15:43:07Z gauthierm $ * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ */ /** * Crypt_GPG base class */ require_once 'Crypt/GPG.php'; /** * GPG exception classes */ require_once 'Crypt/GPG/Exceptions.php'; /** * Status line handler for the decrypt operation * * This class is used internally by Crypt_GPG and does not need be used * directly. See the {@link Crypt_GPG} class for end-user API. * * This class is responsible for sending the passphrase commands when required * by the {@link Crypt_GPG::decrypt()} method. See doc/DETAILS in the * {@link http://www.gnupg.org/download/ GPG distribution} for detailed * information on GPG's status output for the decrypt operation. * * This class is also responsible for parsing error status and throwing a * meaningful exception in the event that decryption fails. * * @category Encryption * @package Crypt_GPG * @author Michael Gauthier * @copyright 2008 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_DecryptStatusHandler { // {{{ protected properties /** * Keys used to decrypt * * The array is of the form: * * array( * $key_id => array( * 'fingerprint' => $fingerprint, * 'passphrase' => $passphrase * ) * ); * * * @var array */ protected $keys = array(); /** * Engine used to which passphrases are passed * * @var Crypt_GPG_Engine */ protected $engine = null; /** * The id of the current sub-key used for decryption * * @var string */ protected $currentSubKey = ''; /** * Whether or not decryption succeeded * * If the message is only signed (compressed) and not encrypted, this is * always true. If the message is encrypted, this flag is set to false * until we know the decryption succeeded. * * @var boolean */ protected $decryptionOkay = true; /** * Whether or not there was no data for decryption * * @var boolean */ protected $noData = false; /** * Keys for which the passhprase is missing * * This contains primary user ids indexed by sub-key id and is used to * create helpful exception messages. * * @var array */ protected $missingPassphrases = array(); /** * Keys for which the passhprase is incorrect * * This contains primary user ids indexed by sub-key id and is used to * create helpful exception messages. * * @var array */ protected $badPassphrases = array(); /** * Keys that can be used to decrypt the data but are missing from the * keychain * * This is an array with both the key and value being the sub-key id of * the missing keys. * * @var array */ protected $missingKeys = array(); // }}} // {{{ __construct() /** * Creates a new decryption status handler * * @param Crypt_GPG_Engine $engine the GPG engine to which passphrases are * passed. * @param array $keys the decryption keys to use. */ public function __construct(Crypt_GPG_Engine $engine, array $keys) { $this->engine = $engine; $this->keys = $keys; } // }}} // {{{ handle() /** * Handles a status line * * @param string $line the status line to handle. * * @return void */ 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; } } // }}} // {{{ throwException() /** * Takes the final status of the decrypt operation and throws an * appropriate exception * * If decryption was successful, no exception is thrown. * * @return void * * @throws Crypt_GPG_KeyNotFoundException if the private key needed to * decrypt the data is not in the user's keyring. * * @throws Crypt_GPG_NoDataException if specified data does not contain * GPG 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_Exception if an unknown or unexpected error occurs. * Use the debug option and file a bug report if these * exceptions occur. */ public function throwException() { $code = Crypt_GPG::ERROR_NONE; if (!$this->decryptionOkay) { if (count($this->badPassphrases) > 0) { $code = Crypt_GPG::ERROR_BAD_PASSPHRASE; } elseif (count($this->missingKeys) > 0) { $code = Crypt_GPG::ERROR_KEY_NOT_FOUND; } else { $code = Crypt_GPG::ERROR_UNKNOWN; } } elseif ($this->noData) { $code = Crypt_GPG::ERROR_NO_DATA; } switch ($code) { case Crypt_GPG::ERROR_NONE: break; case Crypt_GPG::ERROR_KEY_NOT_FOUND: if (count($this->missingKeys) > 0) { $keyId = reset($this->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: $badPassphrases = array_diff_key( $this->badPassphrases, $this->missingPassphrases ); $missingPassphrases = array_intersect_key( $this->badPassphrases, $this->missingPassphrases ); $message = 'Cannot decrypt data.'; if (count($badPassphrases) > 0) { $message = ' Incorrect passphrase provided for keys: "' . implode('", "', $badPassphrases) . '".'; } if (count($missingPassphrases) > 0) { $message = ' No passphrase provided for keys: "' . implode('", "', $badPassphrases) . '".'; } throw new Crypt_GPG_BadPassphraseException($message, $code, $badPassphrases, $missingPassphrases); 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); } } // }}} } ?>