You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProcessHandler.php 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928
  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  3. /**
  4. * Crypt_GPG is a package to use GPG from PHP
  5. *
  6. * This file contains handler for status and error pipes of GPG process.
  7. *
  8. * PHP version 5
  9. *
  10. * LICENSE:
  11. *
  12. * This library is free software; you can redistribute it and/or modify
  13. * it under the terms of the GNU Lesser General Public License as
  14. * published by the Free Software Foundation; either version 2.1 of the
  15. * License, or (at your option) any later version.
  16. *
  17. * This library is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  20. * Lesser General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Lesser General Public
  23. * License along with this library; if not, see
  24. * <http://www.gnu.org/licenses/>
  25. *
  26. * @category Encryption
  27. * @package Crypt_GPG
  28. * @author Nathan Fredrickson <nathan@silverorange.com>
  29. * @author Michael Gauthier <mike@silverorange.com>
  30. * @author Aleksander Machniak <alec@alec.pl>
  31. * @copyright 2005-2013 silverorange
  32. * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
  33. * @link http://pear.php.net/package/Crypt_GPG
  34. * @link http://www.gnupg.org/
  35. */
  36. /**
  37. * GPG exception classes.
  38. */
  39. require_once 'Crypt/GPG/Exceptions.php';
  40. /**
  41. * Signature object class definition
  42. */
  43. require_once 'Crypt/GPG/Signature.php';
  44. // {{{ class Crypt_GPG_ProcessHandler
  45. /**
  46. * Status/Error handler for GPG process pipes.
  47. *
  48. * This class is used internally by Crypt_GPG_Engine and does not need to be used
  49. * directly. See the {@link Crypt_GPG} class for end-user API.
  50. *
  51. * @category Encryption
  52. * @package Crypt_GPG
  53. * @author Nathan Fredrickson <nathan@silverorange.com>
  54. * @author Michael Gauthier <mike@silverorange.com>
  55. * @author Aleksander Machniak <alec@alec.pl>
  56. * @copyright 2005-2013 silverorange
  57. * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
  58. * @link http://pear.php.net/package/Crypt_GPG
  59. * @link http://www.gnupg.org/
  60. */
  61. class Crypt_GPG_ProcessHandler
  62. {
  63. // {{{ protected class properties
  64. /**
  65. * Engine used to control the GPG subprocess
  66. *
  67. * @var Crypt_GPG_Engine
  68. */
  69. protected $engine;
  70. /**
  71. * The error code of the current operation
  72. *
  73. * @var integer
  74. */
  75. protected $errorCode = Crypt_GPG::ERROR_NONE;
  76. /**
  77. * The number of currently needed passphrases
  78. *
  79. * If this is not zero when the GPG command is completed, the error code is
  80. * set to {@link Crypt_GPG::ERROR_MISSING_PASSPHRASE}.
  81. *
  82. * @var integer
  83. */
  84. protected $needPassphrase = 0;
  85. /**
  86. * Some data collected while processing the operation
  87. * or set for the operation
  88. *
  89. * @var array
  90. * @see self::setData()
  91. * @see self::getData()
  92. */
  93. protected $data = array();
  94. /**
  95. * The name of the current operation
  96. *
  97. * @var string
  98. * @see self::setOperation()
  99. */
  100. protected $operation = null;
  101. /**
  102. * The value of the argument of current operation
  103. *
  104. * @var string
  105. * @see self::setOperation()
  106. */
  107. protected $operationArg = null;
  108. // }}}
  109. // {{{ __construct()
  110. /**
  111. * Creates a new instance
  112. *
  113. * @param Crypt_GPG_Engine $engine Engine object
  114. */
  115. public function __construct($engine)
  116. {
  117. $this->engine = $engine;
  118. }
  119. // }}}
  120. // {{{ setOperation()
  121. /**
  122. * Sets the operation that is being performed by the engine.
  123. *
  124. * @param string $operation The GPG operation to perform.
  125. *
  126. * @return void
  127. */
  128. public function setOperation($operation)
  129. {
  130. $op = null;
  131. $opArg = null;
  132. // Regexp matching all GPG "operational" arguments
  133. $regexp = '/--('
  134. . 'version|import|list-public-keys|list-secret-keys'
  135. . '|list-keys|delete-key|delete-secret-key|encrypt|sign|clearsign'
  136. . '|detach-sign|decrypt|verify|export-secret-keys|export|gen-key'
  137. . ')/';
  138. if (strpos($operation, ' ') === false) {
  139. $op = trim($operation, '- ');
  140. } else if (preg_match($regexp, $operation, $matches, PREG_OFFSET_CAPTURE)) {
  141. $op = trim($matches[0][0], '-');
  142. $op_len = $matches[0][1] + mb_strlen($op, '8bit') + 3;
  143. $command = mb_substr($operation, $op_len, null, '8bit');
  144. // we really need the argument if it is a key ID/fingerprint or email
  145. // address se we can use simplified regexp to "revert escapeshellarg()"
  146. if (preg_match('/^[\'"]([a-zA-Z0-9:@._-]+)[\'"]/', $command, $matches)) {
  147. $opArg = $matches[1];
  148. }
  149. }
  150. $this->operation = $op;
  151. $this->operationArg = $opArg;
  152. }
  153. // }}}
  154. // {{{ handleStatus()
  155. /**
  156. * Handles error values in the status output from GPG
  157. *
  158. * This method is responsible for setting the
  159. * {@link self::$errorCode}. See <b>doc/DETAILS</b> in the
  160. * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
  161. * information on GPG's status output.
  162. *
  163. * @param string $line the status line to handle.
  164. *
  165. * @return void
  166. */
  167. public function handleStatus($line)
  168. {
  169. $tokens = explode(' ', $line);
  170. switch ($tokens[0]) {
  171. case 'NODATA':
  172. $this->errorCode = Crypt_GPG::ERROR_NO_DATA;
  173. break;
  174. case 'DECRYPTION_OKAY':
  175. // If the message is encrypted, this is the all-clear signal.
  176. $this->data['DecryptionOkay'] = true;
  177. $this->errorCode = Crypt_GPG::ERROR_NONE;
  178. break;
  179. case 'DELETE_PROBLEM':
  180. if ($tokens[1] == '1') {
  181. $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
  182. break;
  183. } elseif ($tokens[1] == '2') {
  184. $this->errorCode = Crypt_GPG::ERROR_DELETE_PRIVATE_KEY;
  185. break;
  186. }
  187. break;
  188. case 'IMPORT_OK':
  189. $this->data['Import']['fingerprint'] = $tokens[2];
  190. if (empty($this->data['Import']['fingerprints'])) {
  191. $this->data['Import']['fingerprints'] = array($tokens[2]);
  192. } else if (!in_array($tokens[2], $this->data['Import']['fingerprints'])) {
  193. $this->data['Import']['fingerprints'][] = $tokens[2];
  194. }
  195. break;
  196. case 'IMPORT_RES':
  197. $this->data['Import']['public_imported'] = intval($tokens[3]);
  198. $this->data['Import']['public_unchanged'] = intval($tokens[5]);
  199. $this->data['Import']['private_imported'] = intval($tokens[11]);
  200. $this->data['Import']['private_unchanged'] = intval($tokens[12]);
  201. break;
  202. case 'NO_PUBKEY':
  203. case 'NO_SECKEY':
  204. $this->data['ErrorKeyId'] = $tokens[1];
  205. if ($this->errorCode != Crypt_GPG::ERROR_MISSING_PASSPHRASE
  206. && $this->errorCode != Crypt_GPG::ERROR_BAD_PASSPHRASE
  207. ) {
  208. $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
  209. }
  210. // note: this message is also received if there are multiple
  211. // recipients and a previous key had a correct passphrase.
  212. $this->data['MissingKeys'][$tokens[1]] = $tokens[1];
  213. // @FIXME: remove missing passphrase registered in ENC_TO handler
  214. // This is for GnuPG 2.1
  215. unset($this->data['MissingPassphrases'][$tokens[1]]);
  216. break;
  217. case 'KEY_CONSIDERED':
  218. // In GnuPG 2.1.x exporting/importing a secret key requires passphrase
  219. // However, no NEED_PASSPRASE is returned, https://bugs.gnupg.org/gnupg/issue2667
  220. // So, handling KEY_CONSIDERED and GET_HIDDEN is needed.
  221. if (!array_key_exists('KeyConsidered', $this->data)) {
  222. $this->data['KeyConsidered'] = $tokens[1];
  223. }
  224. break;
  225. case 'USERID_HINT':
  226. // remember the user id for pretty exception messages
  227. // GnuPG 2.1.15 gives me: "USERID_HINT 0000000000000000 [?]"
  228. $keyId = $tokens[1];
  229. if (strcspn($keyId, '0')) {
  230. $username = implode(' ', array_splice($tokens, 2));
  231. $this->data['BadPassphrases'][$keyId] = $username;
  232. }
  233. break;
  234. case 'ENC_TO':
  235. // Now we know the message is encrypted. Set flag to check if
  236. // decryption succeeded.
  237. $this->data['DecryptionOkay'] = false;
  238. // this is the new key message
  239. $this->data['CurrentSubKeyId'] = $keyId = $tokens[1];
  240. // For some reason in GnuPG 2.1.11 I get only ENC_TO and no
  241. // NEED_PASSPHRASE/MISSING_PASSPHRASE/USERID_HINT
  242. // This is not needed for GnuPG 2.1.15
  243. if (!empty($_ENV['PINENTRY_USER_DATA'])) {
  244. $passphrases = json_decode($_ENV['PINENTRY_USER_DATA'], true);
  245. } else {
  246. $passphrases = array();
  247. }
  248. // @TODO: Get user name/email
  249. $this->data['BadPassphrases'][$keyId] = $keyId;
  250. if (empty($passphrases) || empty($passphrases[$keyId])) {
  251. $this->data['MissingPassphrases'][$keyId] = $keyId;
  252. }
  253. break;
  254. case 'GOOD_PASSPHRASE':
  255. // if we got a good passphrase, remove the key from the list of
  256. // bad passphrases.
  257. if (isset($this->data['CurrentSubKeyId'])) {
  258. unset($this->data['BadPassphrases'][$this->data['CurrentSubKeyId']]);
  259. unset($this->data['MissingPassphrases'][$this->data['CurrentSubKeyId']]);
  260. }
  261. $this->needPassphrase--;
  262. break;
  263. case 'BAD_PASSPHRASE':
  264. $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE;
  265. break;
  266. case 'MISSING_PASSPHRASE':
  267. if (isset($this->data['CurrentSubKeyId'])) {
  268. $this->data['MissingPassphrases'][$this->data['CurrentSubKeyId']]
  269. = $this->data['CurrentSubKeyId'];
  270. }
  271. $this->errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE;
  272. break;
  273. case 'GET_HIDDEN':
  274. if ($tokens[1] == 'passphrase.enter' && isset($this->data['KeyConsidered'])) {
  275. $tokens[1] = $this->data['KeyConsidered'];
  276. } else {
  277. break;
  278. }
  279. // no break
  280. case 'NEED_PASSPHRASE':
  281. $passphrase = $this->getPin($tokens[1]);
  282. $this->engine->sendCommand($passphrase);
  283. if ($passphrase === '') {
  284. $this->needPassphrase++;
  285. }
  286. break;
  287. case 'SIG_CREATED':
  288. $this->data['SigCreated'] = $line;
  289. break;
  290. case 'SIG_ID':
  291. // note: signature id comes before new signature line and may not
  292. // exist for some signature types
  293. $this->data['SignatureId'] = $tokens[1];
  294. break;
  295. case 'EXPSIG':
  296. case 'EXPKEYSIG':
  297. case 'REVKEYSIG':
  298. case 'BADSIG':
  299. case 'ERRSIG':
  300. $this->errorCode = Crypt_GPG::ERROR_BAD_SIGNATURE;
  301. // no break
  302. case 'GOODSIG':
  303. $signature = new Crypt_GPG_Signature();
  304. // if there was a signature id, set it on the new signature
  305. if (!empty($this->data['SignatureId'])) {
  306. $signature->setId($this->data['SignatureId']);
  307. $this->data['SignatureId'] = '';
  308. }
  309. // Detect whether fingerprint or key id was returned and set
  310. // signature values appropriately. Key ids are strings of either
  311. // 16 or 8 hexadecimal characters. Fingerprints are strings of 40
  312. // hexadecimal characters. The key id is the last 16 characters of
  313. // the key fingerprint.
  314. if (mb_strlen($tokens[1], '8bit') > 16) {
  315. $signature->setKeyFingerprint($tokens[1]);
  316. $signature->setKeyId(mb_substr($tokens[1], -16, null, '8bit'));
  317. } else {
  318. $signature->setKeyId($tokens[1]);
  319. }
  320. // get user id string
  321. if ($tokens[0] != 'ERRSIG') {
  322. $string = implode(' ', array_splice($tokens, 2));
  323. $string = rawurldecode($string);
  324. $signature->setUserId(Crypt_GPG_UserId::parse($string));
  325. }
  326. $this->data['Signatures'][] = $signature;
  327. break;
  328. case 'VALIDSIG':
  329. if (empty($this->data['Signatures'])) {
  330. break;
  331. }
  332. $signature = end($this->data['Signatures']);
  333. $signature->setValid(true);
  334. $signature->setKeyFingerprint($tokens[1]);
  335. if (strpos($tokens[3], 'T') === false) {
  336. $signature->setCreationDate($tokens[3]);
  337. } else {
  338. $signature->setCreationDate(strtotime($tokens[3]));
  339. }
  340. if (array_key_exists(4, $tokens)) {
  341. if (strpos($tokens[4], 'T') === false) {
  342. $signature->setExpirationDate($tokens[4]);
  343. } else {
  344. $signature->setExpirationDate(strtotime($tokens[4]));
  345. }
  346. }
  347. break;
  348. case 'KEY_CREATED':
  349. if (isset($this->data['Handle']) && $tokens[3] == $this->data['Handle']) {
  350. $this->data['KeyCreated'] = $tokens[2];
  351. }
  352. break;
  353. case 'KEY_NOT_CREATED':
  354. if (isset($this->data['Handle']) && $tokens[1] == $this->data['Handle']) {
  355. $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_CREATED;
  356. }
  357. break;
  358. case 'PROGRESS':
  359. // todo: at some point, support reporting status async
  360. break;
  361. // GnuPG 2.1 uses FAILURE and ERROR responses
  362. case 'FAILURE':
  363. case 'ERROR':
  364. $errnum = (int) $tokens[2];
  365. $source = $errnum >> 24;
  366. $errcode = $errnum & 0xFFFFFF;
  367. switch ($errcode) {
  368. case 11: // bad passphrase
  369. case 87: // bad PIN
  370. $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE;
  371. break;
  372. case 177: // no passphrase
  373. case 178: // no PIN
  374. $this->errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE;
  375. break;
  376. case 58:
  377. $this->errorCode = Crypt_GPG::ERROR_NO_DATA;
  378. break;
  379. }
  380. break;
  381. }
  382. }
  383. // }}}
  384. // {{{ handleError()
  385. /**
  386. * Handles error values in the error output from GPG
  387. *
  388. * This method is responsible for setting the
  389. * {@link Crypt_GPG_Engine::$_errorCode}.
  390. *
  391. * @param string $line the error line to handle.
  392. *
  393. * @return void
  394. */
  395. public function handleError($line)
  396. {
  397. if ($this->errorCode === Crypt_GPG::ERROR_NONE) {
  398. $pattern = '/no valid OpenPGP data found/';
  399. if (preg_match($pattern, $line) === 1) {
  400. $this->errorCode = Crypt_GPG::ERROR_NO_DATA;
  401. }
  402. }
  403. if ($this->errorCode === Crypt_GPG::ERROR_NONE) {
  404. $pattern = '/No secret key|secret key not available/';
  405. if (preg_match($pattern, $line) === 1) {
  406. $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
  407. }
  408. }
  409. if ($this->errorCode === Crypt_GPG::ERROR_NONE) {
  410. $pattern = '/No public key|public key not found/';
  411. if (preg_match($pattern, $line) === 1) {
  412. $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
  413. }
  414. }
  415. if ($this->errorCode === Crypt_GPG::ERROR_NONE) {
  416. $matches = array();
  417. $pattern = '/can\'t (?:access|open) `(.*?)\'/';
  418. if (preg_match($pattern, $line, $matches) === 1) {
  419. $this->data['ErrorFilename'] = $matches[1];
  420. $this->errorCode = Crypt_GPG::ERROR_FILE_PERMISSIONS;
  421. }
  422. }
  423. // GnuPG 2.1: It should return MISSING_PASSPHRASE, but it does not
  424. // we have to detect it this way. This happens e.g. on private key import
  425. if ($this->errorCode === Crypt_GPG::ERROR_NONE) {
  426. $matches = array();
  427. $pattern = '/key ([0-9A-F]+).* (Bad|No) passphrase/';
  428. if (preg_match($pattern, $line, $matches) === 1) {
  429. $keyId = $matches[1];
  430. // @TODO: Get user name/email
  431. if (empty($this->data['BadPassphrases'][$keyId])) {
  432. $this->data['BadPassphrases'][$keyId] = $keyId;
  433. }
  434. if ($matches[2] == 'Bad') {
  435. $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE;
  436. } else {
  437. $this->errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE;
  438. if (empty($this->data['MissingPassphrases'][$keyId])) {
  439. $this->data['MissingPassphrases'][$keyId] = $keyId;
  440. }
  441. }
  442. }
  443. }
  444. if ($this->errorCode === Crypt_GPG::ERROR_NONE && $this->operation == 'gen-key') {
  445. $pattern = '/:([0-9]+): invalid algorithm$/';
  446. if (preg_match($pattern, $line, $matches) === 1) {
  447. $this->errorCode = Crypt_GPG::ERROR_BAD_KEY_PARAMS;
  448. $this->data['LineNumber'] = intval($matches[1]);
  449. }
  450. }
  451. }
  452. // }}}
  453. // {{{ throwException()
  454. /**
  455. * On error throws exception
  456. *
  457. * @param int $exitcode GPG process exit code
  458. *
  459. * @return void
  460. * @throws Crypt_GPG_Exception
  461. */
  462. public function throwException($exitcode = 0)
  463. {
  464. if ($exitcode > 0 && $this->errorCode === Crypt_GPG::ERROR_NONE) {
  465. $this->errorCode = $this->setErrorCode($exitcode);
  466. }
  467. if ($this->errorCode === Crypt_GPG::ERROR_NONE) {
  468. return;
  469. }
  470. $code = $this->errorCode;
  471. $note = "Please use the 'debug' option when creating the Crypt_GPG " .
  472. "object, and file a bug report at " . Crypt_GPG::BUG_URI;
  473. switch ($this->operation) {
  474. case 'version':
  475. throw new Crypt_GPG_Exception(
  476. 'Unknown error getting GnuPG version information. ' . $note,
  477. $code
  478. );
  479. case 'list-secret-keys':
  480. case 'list-public-keys':
  481. case 'list-keys':
  482. switch ($code) {
  483. case Crypt_GPG::ERROR_KEY_NOT_FOUND:
  484. // ignore not found key errors
  485. break;
  486. case Crypt_GPG::ERROR_FILE_PERMISSIONS:
  487. if (!empty($this->data['ErrorFilename'])) {
  488. throw new Crypt_GPG_FileException(
  489. sprintf(
  490. 'Error reading GnuPG data file \'%s\'. Check to make ' .
  491. 'sure it is readable by the current user.',
  492. $this->data['ErrorFilename']
  493. ),
  494. $code,
  495. $this->data['ErrorFilename']
  496. );
  497. }
  498. throw new Crypt_GPG_FileException(
  499. 'Error reading GnuPG data file. Check to make sure that ' .
  500. 'GnuPG data files are readable by the current user.',
  501. $code
  502. );
  503. default:
  504. throw new Crypt_GPG_Exception(
  505. 'Unknown error getting keys. ' . $note, $code
  506. );
  507. }
  508. break;
  509. case 'delete-key':
  510. case 'delete-secret-key':
  511. switch ($code) {
  512. case Crypt_GPG::ERROR_KEY_NOT_FOUND:
  513. throw new Crypt_GPG_KeyNotFoundException(
  514. 'Key not found: ' . $this->operationArg,
  515. $code,
  516. $this->operationArg
  517. );
  518. case Crypt_GPG::ERROR_DELETE_PRIVATE_KEY:
  519. throw new Crypt_GPG_DeletePrivateKeyException(
  520. 'Private key must be deleted before public key can be ' .
  521. 'deleted.',
  522. $code,
  523. $this->operationArg
  524. );
  525. default:
  526. throw new Crypt_GPG_Exception(
  527. 'Unknown error deleting key. ' . $note, $code
  528. );
  529. }
  530. break;
  531. case 'import':
  532. switch ($code) {
  533. case Crypt_GPG::ERROR_NO_DATA:
  534. throw new Crypt_GPG_NoDataException(
  535. 'No valid GPG key data found.', $code
  536. );
  537. case Crypt_GPG::ERROR_BAD_PASSPHRASE:
  538. case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
  539. throw $this->badPassException($code, 'Cannot import private key.');
  540. default:
  541. throw new Crypt_GPG_Exception(
  542. 'Unknown error importing GPG key. ' . $note, $code
  543. );
  544. }
  545. break;
  546. case 'export':
  547. case 'export-secret-keys':
  548. switch ($code) {
  549. case Crypt_GPG::ERROR_BAD_PASSPHRASE:
  550. case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
  551. throw $this->badPassException($code, 'Cannot export private key.');
  552. default:
  553. throw new Crypt_GPG_Exception(
  554. 'Unknown error exporting a key. ' . $note, $code
  555. );
  556. }
  557. break;
  558. case 'encrypt':
  559. case 'sign':
  560. case 'clearsign':
  561. case 'detach-sign':
  562. switch ($code) {
  563. case Crypt_GPG::ERROR_KEY_NOT_FOUND:
  564. throw new Crypt_GPG_KeyNotFoundException(
  565. 'Cannot sign data. Private key not found. Import the '.
  566. 'private key before trying to sign data.',
  567. $code,
  568. !empty($this->data['ErrorKeyId']) ? $this->data['ErrorKeyId'] : null
  569. );
  570. case Crypt_GPG::ERROR_BAD_PASSPHRASE:
  571. throw new Crypt_GPG_BadPassphraseException(
  572. 'Cannot sign data. Incorrect passphrase provided.', $code
  573. );
  574. case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
  575. throw new Crypt_GPG_BadPassphraseException(
  576. 'Cannot sign data. No passphrase provided.', $code
  577. );
  578. default:
  579. throw new Crypt_GPG_Exception(
  580. "Unknown error {$this->operation}ing data. $note", $code
  581. );
  582. }
  583. break;
  584. case 'verify':
  585. switch ($code) {
  586. case Crypt_GPG::ERROR_BAD_SIGNATURE:
  587. // ignore bad signature errors
  588. break;
  589. case Crypt_GPG::ERROR_NO_DATA:
  590. throw new Crypt_GPG_NoDataException(
  591. 'No valid signature data found.', $code
  592. );
  593. case Crypt_GPG::ERROR_KEY_NOT_FOUND:
  594. throw new Crypt_GPG_KeyNotFoundException(
  595. 'Public key required for data verification not in keyring.',
  596. $code,
  597. !empty($this->data['ErrorKeyId']) ? $this->data['ErrorKeyId'] : null
  598. );
  599. default:
  600. throw new Crypt_GPG_Exception(
  601. 'Unknown error validating signature details. ' . $note,
  602. $code
  603. );
  604. }
  605. break;
  606. case 'decrypt':
  607. switch ($code) {
  608. case Crypt_GPG::ERROR_BAD_SIGNATURE:
  609. // ignore bad signature errors
  610. break;
  611. case Crypt_GPG::ERROR_KEY_NOT_FOUND:
  612. if (!empty($this->data['MissingKeys'])) {
  613. $keyId = reset($this->data['MissingKeys']);
  614. } else {
  615. $keyId = '';
  616. }
  617. throw new Crypt_GPG_KeyNotFoundException(
  618. 'Cannot decrypt data. No suitable private key is in the ' .
  619. 'keyring. Import a suitable private key before trying to ' .
  620. 'decrypt this data.',
  621. $code,
  622. $keyId
  623. );
  624. case Crypt_GPG::ERROR_BAD_PASSPHRASE:
  625. case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
  626. throw $this->badPassException($code, 'Cannot decrypt data.');
  627. case Crypt_GPG::ERROR_NO_DATA:
  628. throw new Crypt_GPG_NoDataException(
  629. 'Cannot decrypt data. No PGP encrypted data was found in '.
  630. 'the provided data.',
  631. $code
  632. );
  633. default:
  634. throw new Crypt_GPG_Exception(
  635. 'Unknown error decrypting data.', $code
  636. );
  637. }
  638. break;
  639. case 'gen-key':
  640. switch ($code) {
  641. case Crypt_GPG::ERROR_BAD_KEY_PARAMS:
  642. throw new Crypt_GPG_InvalidKeyParamsException(
  643. 'Invalid key algorithm specified.', $code
  644. );
  645. default:
  646. throw new Crypt_GPG_Exception(
  647. 'Unknown error generating key-pair. ' . $note, $code
  648. );
  649. }
  650. }
  651. }
  652. // }}}
  653. // {{{ throwException()
  654. /**
  655. * Check exit code of the GPG operation.
  656. *
  657. * @param int $exitcode GPG process exit code
  658. *
  659. * @return int Internal error code
  660. */
  661. protected function setErrorCode($exitcode)
  662. {
  663. if ($this->needPassphrase > 0) {
  664. return Crypt_GPG::ERROR_MISSING_PASSPHRASE;
  665. }
  666. if ($this->operation == 'import') {
  667. return Crypt_GPG::ERROR_NONE;
  668. }
  669. if ($this->operation == 'decrypt' && !empty($this->data['DecryptionOkay'])) {
  670. if (!empty($this->data['IgnoreVerifyErrors'])) {
  671. return Crypt_GPG::ERROR_NONE;
  672. }
  673. if (!empty($this->data['MissingKeys'])) {
  674. return Crypt_GPG::ERROR_KEY_NOT_FOUND;
  675. }
  676. }
  677. return Crypt_GPG::ERROR_UNKNOWN;
  678. }
  679. // }}}
  680. // {{{ getData()
  681. /**
  682. * Get data from the last process execution.
  683. *
  684. * @param string $name Data element name:
  685. * - SigCreated: The last SIG_CREATED status.
  686. * - KeyConsidered: The last KEY_CONSIDERED status identifier.
  687. * - KeyCreated: The KEY_CREATED status (for specified Handle).
  688. * - Signatures: Signatures data from verification process.
  689. * - LineNumber: Number of the gen-key error line.
  690. * - Import: Result of IMPORT_OK/IMPORT_RES
  691. *
  692. * @return mixed
  693. */
  694. public function getData($name)
  695. {
  696. return isset($this->data[$name]) ? $this->data[$name] : null;
  697. }
  698. // }}}
  699. // {{{ setData()
  700. /**
  701. * Set data for the process execution.
  702. *
  703. * @param string $name Data element name:
  704. * - Handle: The unique key handle used by this handler
  705. * The key handle is used to track GPG status output
  706. * for a particular key on --gen-key command before
  707. * the key has its own identifier.
  708. * - IgnoreVerifyErrors: Do not throw exceptions
  709. * when signature verification failes because
  710. * of a missing public key.
  711. * @param mixed $value Data element value
  712. *
  713. * @return void
  714. */
  715. public function setData($name, $value)
  716. {
  717. switch ($name) {
  718. case 'Handle':
  719. $this->data[$name] = strval($value);
  720. break;
  721. case 'IgnoreVerifyErrors':
  722. $this->data[$name] = (bool) $value;
  723. break;
  724. }
  725. }
  726. // }}}
  727. // {{{ setData()
  728. /**
  729. * Create Crypt_GPG_BadPassphraseException from operation data.
  730. *
  731. * @param int $code Error code
  732. * @param string $message Error message
  733. *
  734. * @return Crypt_GPG_BadPassphraseException
  735. */
  736. protected function badPassException($code, $message)
  737. {
  738. $badPassphrases = array_diff_key(
  739. isset($this->data['BadPassphrases']) ? $this->data['BadPassphrases'] : array(),
  740. isset($this->data['MissingPassphrases']) ? $this->data['MissingPassphrases'] : array()
  741. );
  742. $missingPassphrases = array_intersect_key(
  743. isset($this->data['BadPassphrases']) ? $this->data['BadPassphrases'] : array(),
  744. isset($this->data['MissingPassphrases']) ? $this->data['MissingPassphrases'] : array()
  745. );
  746. if (count($badPassphrases) > 0) {
  747. $message .= ' Incorrect passphrase provided for keys: "' .
  748. implode('", "', $badPassphrases) . '".';
  749. }
  750. if (count($missingPassphrases) > 0) {
  751. $message .= ' No passphrase provided for keys: "' .
  752. implode('", "', $missingPassphrases) . '".';
  753. }
  754. return new Crypt_GPG_BadPassphraseException(
  755. $message,
  756. $code,
  757. $badPassphrases,
  758. $missingPassphrases
  759. );
  760. }
  761. // }}}
  762. // {{{ getPin()
  763. /**
  764. * Get registered passphrase for specified key.
  765. *
  766. * @param string $key Key identifier
  767. *
  768. * @return string Passphrase
  769. */
  770. protected function getPin($key)
  771. {
  772. $passphrase = '';
  773. $keyIdLength = mb_strlen($key, '8bit');
  774. if ($keyIdLength && !empty($_ENV['PINENTRY_USER_DATA'])) {
  775. $passphrases = json_decode($_ENV['PINENTRY_USER_DATA'], true);
  776. foreach ($passphrases as $_keyId => $pass) {
  777. $keyId = $key;
  778. $_keyIdLength = mb_strlen($_keyId, '8bit');
  779. // Get last X characters of key identifier to compare
  780. if ($keyIdLength < $_keyIdLength) {
  781. $_keyId = mb_substr($_keyId, -$keyIdLength, null, '8bit');
  782. } else if ($keyIdLength > $_keyIdLength) {
  783. $keyId = mb_substr($keyId, -$_keyIdLength, null, '8bit');
  784. }
  785. if ($_keyId === $keyId) {
  786. $passphrase = $pass;
  787. break;
  788. }
  789. }
  790. }
  791. return $passphrase;
  792. }
  793. // }}}
  794. }
  795. // }}}
  796. ?>