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.

PinEntry.php 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  3. /**
  4. * Contains a class implementing automatic pinentry for gpg-agent
  5. *
  6. * PHP version 5
  7. *
  8. * LICENSE:
  9. *
  10. * This library is free software; you can redistribute it and/or modify
  11. * it under the terms of the GNU Lesser General Public License as
  12. * published by the Free Software Foundation; either version 2.1 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This library is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  18. * Lesser General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Lesser General Public
  21. * License along with this library; if not, see
  22. * <http://www.gnu.org/licenses/>
  23. *
  24. * @category Encryption
  25. * @package Crypt_GPG
  26. * @author Michael Gauthier <mike@silverorange.com>
  27. * @copyright 2013 silverorange
  28. * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
  29. * @link http://pear.php.net/package/Crypt_GPG
  30. */
  31. /**
  32. * CLI user-interface and parser.
  33. */
  34. require_once 'Console/CommandLine.php';
  35. // {{{ class Crypt_GPG_PinEntry
  36. /**
  37. * A command-line dummy pinentry program for use with gpg-agent and Crypt_GPG
  38. *
  39. * This pinentry receives passphrases through en environment variable and
  40. * automatically enters the PIN in response to gpg-agent requests. No user-
  41. * interaction required.
  42. *
  43. * The pinentry can be run independently for testing and debugging with the
  44. * following syntax:
  45. *
  46. * <pre>
  47. * Usage:
  48. * crypt-gpg-pinentry [options]
  49. *
  50. * Options:
  51. * -l log, --log=log Optional location to log pinentry activity.
  52. * -v, --verbose Sets verbosity level. Use multiples for more detail
  53. * (e.g. "-vv").
  54. * -h, --help show this help message and exit
  55. * --version show the program version and exit
  56. * </pre>
  57. *
  58. * @category Encryption
  59. * @package Crypt_GPG
  60. * @author Michael Gauthier <mike@silverorange.com>
  61. * @copyright 2013 silverorange
  62. * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
  63. * @link http://pear.php.net/package/Crypt_GPG
  64. * @see Crypt_GPG::getKeys()
  65. */
  66. class Crypt_GPG_PinEntry
  67. {
  68. // {{{ class constants
  69. /**
  70. * Verbosity level for showing no output.
  71. */
  72. const VERBOSITY_NONE = 0;
  73. /**
  74. * Verbosity level for showing error output.
  75. */
  76. const VERBOSITY_ERRORS = 1;
  77. /**
  78. * Verbosity level for showing all output, including Assuan protocol
  79. * messages.
  80. */
  81. const VERBOSITY_ALL = 2;
  82. /**
  83. * Length of buffer for reading lines from the Assuan server.
  84. *
  85. * PHP reads 8192 bytes. If this is set to less than 8192, PHP reads 8192
  86. * and buffers the rest so we might as well just read 8192.
  87. *
  88. * Using values other than 8192 also triggers PHP bugs.
  89. *
  90. * @see http://bugs.php.net/bug.php?id=35224
  91. */
  92. const CHUNK_SIZE = 8192;
  93. // }}}
  94. // {{{ protected properties
  95. /**
  96. * File handle for the input stream
  97. *
  98. * @var resource
  99. */
  100. protected $stdin = null;
  101. /**
  102. * File handle for the output stream
  103. *
  104. * @var resource
  105. */
  106. protected $stdout = null;
  107. /**
  108. * File handle for the log file if a log file is used
  109. *
  110. * @var resource
  111. */
  112. protected $logFile = null;
  113. /**
  114. * Whether or not this pinentry is finished and is exiting
  115. *
  116. * @var boolean
  117. */
  118. protected $moribund = false;
  119. /**
  120. * Verbosity level
  121. *
  122. * One of:
  123. * - {@link Crypt_GPG_PinEntry::VERBOSITY_NONE},
  124. * - {@link Crypt_GPG_PinEntry::VERBOSITY_ERRORS}, or
  125. * - {@link Crypt_GPG_PinEntry::VERBOSITY_ALL}
  126. *
  127. * @var integer
  128. */
  129. protected $verbosity = self::VERBOSITY_NONE;
  130. /**
  131. * The command-line interface parser for this pinentry
  132. *
  133. * @var Console_CommandLine
  134. *
  135. * @see Crypt_GPG_PinEntry::getParser()
  136. */
  137. protected $parser = null;
  138. /**
  139. * PINs to be entered by this pinentry
  140. *
  141. * An indexed array of associative arrays in the form:
  142. * <code>
  143. * <?php
  144. * array(
  145. * array(
  146. * 'keyId' => $keyId,
  147. * 'passphrase' => $passphrase
  148. * ),
  149. * ...
  150. * );
  151. * ?>
  152. * </code>
  153. *
  154. * This array is parsed from the environment variable
  155. * <kbd>PINENTRY_USER_DATA</kbd>.
  156. *
  157. * @var array
  158. *
  159. * @see Crypt_GPG_PinEntry::initPinsFromENV()
  160. */
  161. protected $pins = array();
  162. /**
  163. * The PIN currently being requested by the Assuan server
  164. *
  165. * If set, this is an associative array in the form:
  166. * <code>
  167. * <?php
  168. * array(
  169. * 'keyId' => $shortKeyId,
  170. * 'userId' => $userIdString
  171. * );
  172. * ?>
  173. * </code>
  174. *
  175. * @var array|null
  176. */
  177. protected $currentPin = null;
  178. // }}}
  179. // {{{ __invoke()
  180. /**
  181. * Runs this pinentry
  182. *
  183. * @return void
  184. */
  185. public function __invoke()
  186. {
  187. $this->parser = $this->getCommandLineParser();
  188. try {
  189. $result = $this->parser->parse();
  190. $this->setVerbosity($result->options['verbose']);
  191. $this->setLogFilename($result->options['log']);
  192. $this->connect();
  193. $this->initPinsFromENV();
  194. while (($line = fgets($this->stdin, self::CHUNK_SIZE)) !== false) {
  195. $this->parseCommand(mb_substr($line, 0, -1, '8bit'));
  196. if ($this->moribund) {
  197. break;
  198. }
  199. }
  200. $this->disconnect();
  201. } catch (Console_CommandLineException $e) {
  202. $this->log($e->getMessage() . PHP_EOL, slf::VERBOSITY_ERRORS);
  203. exit(1);
  204. } catch (Exception $e) {
  205. $this->log($e->getMessage() . PHP_EOL, self::VERBOSITY_ERRORS);
  206. $this->log($e->getTraceAsString() . PHP_EOL, self::VERBOSITY_ERRORS);
  207. exit(1);
  208. }
  209. }
  210. // }}}
  211. // {{{ setVerbosity()
  212. /**
  213. * Sets the verbosity of logging for this pinentry
  214. *
  215. * Verbosity levels are:
  216. *
  217. * - {@link Crypt_GPG_PinEntry::VERBOSITY_NONE} - no logging.
  218. * - {@link Crypt_GPG_PinEntry::VERBOSITY_ERRORS} - log errors only.
  219. * - {@link Crypt_GPG_PinEntry::VERBOSITY_ALL} - log everything, including
  220. * the assuan protocol.
  221. *
  222. * @param integer $verbosity the level of verbosity of this pinentry.
  223. *
  224. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  225. */
  226. public function setVerbosity($verbosity)
  227. {
  228. $this->verbosity = (integer)$verbosity;
  229. return $this;
  230. }
  231. // }}}
  232. // {{{ setLogFilename()
  233. /**
  234. * Sets the log file location
  235. *
  236. * @param string $filename the new log filename to use. If an empty string
  237. * is used, file-based logging is disabled.
  238. *
  239. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  240. */
  241. public function setLogFilename($filename)
  242. {
  243. if (is_resource($this->logFile)) {
  244. fflush($this->logFile);
  245. fclose($this->logFile);
  246. $this->logFile = null;
  247. }
  248. if ($filename != '') {
  249. if (($this->logFile = fopen($filename, 'w')) === false) {
  250. $this->log(
  251. 'Unable to open log file "' . $filename . '" '
  252. . 'for writing.' . PHP_EOL,
  253. self::VERBOSITY_ERRORS
  254. );
  255. exit(1);
  256. } else {
  257. stream_set_write_buffer($this->logFile, 0);
  258. }
  259. }
  260. return $this;
  261. }
  262. // }}}
  263. // {{{ getUIXML()
  264. /**
  265. * Gets the CLI user-interface definition for this pinentry
  266. *
  267. * Detects whether or not this package is PEAR-installed and appropriately
  268. * locates the XML UI definition.
  269. *
  270. * @return string the location of the CLI user-interface definition XML.
  271. */
  272. protected function getUIXML()
  273. {
  274. // Find PinEntry config depending on the way how the package is installed
  275. $ds = DIRECTORY_SEPARATOR;
  276. $root = __DIR__ . $ds . '..' . $ds . '..' . $ds;
  277. $paths = array(
  278. '@data-dir@' . $ds . '@package-name@' . $ds . 'data', // PEAR
  279. $root . 'data', // Git
  280. $root . 'data' . $ds . 'Crypt_GPG' . $ds . 'data', // Composer
  281. );
  282. foreach ($paths as $path) {
  283. if (file_exists($path . $ds . 'pinentry-cli.xml')) {
  284. return $path . $ds . 'pinentry-cli.xml';
  285. }
  286. }
  287. }
  288. // }}}
  289. // {{{ getCommandLineParser()
  290. /**
  291. * Gets the CLI parser for this pinentry
  292. *
  293. * @return Console_CommandLine the CLI parser for this pinentry.
  294. */
  295. protected function getCommandLineParser()
  296. {
  297. return Console_CommandLine::fromXmlFile($this->getUIXML());
  298. }
  299. // }}}
  300. // {{{ log()
  301. /**
  302. * Logs a message at the specified verbosity level
  303. *
  304. * If a log file is used, the message is written to the log. Otherwise,
  305. * the message is sent to STDERR.
  306. *
  307. * @param string $data the message to log.
  308. * @param integer $level the verbosity level above which the message should
  309. * be logged.
  310. *
  311. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  312. */
  313. protected function log($data, $level)
  314. {
  315. if ($this->verbosity >= $level) {
  316. if (is_resource($this->logFile)) {
  317. fwrite($this->logFile, $data);
  318. fflush($this->logFile);
  319. } else {
  320. $this->parser->outputter->stderr($data);
  321. }
  322. }
  323. return $this;
  324. }
  325. // }}}
  326. // {{{ connect()
  327. /**
  328. * Connects this pinentry to the assuan server
  329. *
  330. * Opens I/O streams and sends initial handshake.
  331. *
  332. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  333. */
  334. protected function connect()
  335. {
  336. // Binary operations will not work on Windows with PHP < 5.2.6.
  337. $rb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'r' : 'rb';
  338. $wb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'w' : 'wb';
  339. $this->stdin = fopen('php://stdin', $rb);
  340. $this->stdout = fopen('php://stdout', $wb);
  341. if (function_exists('stream_set_read_buffer')) {
  342. stream_set_read_buffer($this->stdin, 0);
  343. }
  344. stream_set_write_buffer($this->stdout, 0);
  345. // initial handshake
  346. $this->send($this->getOK('Crypt_GPG pinentry ready and waiting'));
  347. return $this;
  348. }
  349. // }}}
  350. // {{{ parseCommand()
  351. /**
  352. * Parses an assuan command and performs the appropriate action
  353. *
  354. * Documentation of the assuan commands for pinentry is limited to
  355. * non-existent. Most of these commands were taken from the C source code
  356. * to gpg-agent and pinentry.
  357. *
  358. * Additional context was provided by using strace -f when calling the
  359. * gpg-agent.
  360. *
  361. * @param string $line the assuan command line to parse
  362. *
  363. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  364. */
  365. protected function parseCommand($line)
  366. {
  367. $this->log('<- ' . $line . PHP_EOL, self::VERBOSITY_ALL);
  368. $parts = explode(' ', $line, 2);
  369. $command = $parts[0];
  370. if (count($parts) === 2) {
  371. $data = $parts[1];
  372. } else {
  373. $data = null;
  374. }
  375. switch ($command) {
  376. case 'SETDESC':
  377. return $this->sendSetDescription($data);
  378. case 'MESSAGE':
  379. return $this->sendMessage();
  380. case 'CONFIRM':
  381. return $this->sendConfirm();
  382. case 'GETINFO':
  383. return $this->sendGetInfo($data);
  384. case 'GETPIN':
  385. return $this->sendGetPin($data);
  386. case 'RESET':
  387. return $this->sendReset();
  388. case 'BYE':
  389. return $this->sendBye();
  390. default:
  391. return $this->sendNotImplementedOK();
  392. }
  393. }
  394. // }}}
  395. // {{{ initPinsFromENV()
  396. /**
  397. * Initializes the PINs to be entered by this pinentry from the environment
  398. * variable PINENTRY_USER_DATA
  399. *
  400. * The PINs are parsed from a JSON-encoded string.
  401. *
  402. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  403. */
  404. protected function initPinsFromENV()
  405. {
  406. if (($userData = getenv('PINENTRY_USER_DATA')) !== false) {
  407. $pins = json_decode($userData, true);
  408. if ($pins === null) {
  409. $this->log(
  410. '-- failed to parse user data' . PHP_EOL,
  411. self::VERBOSITY_ERRORS
  412. );
  413. } else {
  414. $this->pins = $pins;
  415. $this->log(
  416. '-- got user data [not showing passphrases]' . PHP_EOL,
  417. self::VERBOSITY_ALL
  418. );
  419. }
  420. }
  421. return $this;
  422. }
  423. // }}}
  424. // {{{ disconnect()
  425. /**
  426. * Disconnects this pinentry from the Assuan server
  427. *
  428. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  429. */
  430. protected function disconnect()
  431. {
  432. $this->log('-- disconnecting' . PHP_EOL, self::VERBOSITY_ALL);
  433. fflush($this->stdout);
  434. fclose($this->stdout);
  435. fclose($this->stdin);
  436. $this->stdin = null;
  437. $this->stdout = null;
  438. $this->log('-- disconnected' . PHP_EOL, self::VERBOSITY_ALL);
  439. if (is_resource($this->logFile)) {
  440. fflush($this->logFile);
  441. fclose($this->logFile);
  442. $this->logFile = null;
  443. }
  444. return $this;
  445. }
  446. // }}}
  447. // {{{ sendNotImplementedOK()
  448. /**
  449. * Sends an OK response for a not implemented feature
  450. *
  451. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  452. */
  453. protected function sendNotImplementedOK()
  454. {
  455. return $this->send($this->getOK());
  456. }
  457. // }}}
  458. // {{{ sendSetDescription()
  459. /**
  460. * Parses the currently requested key identifier and user identifier from
  461. * the description passed to this pinentry
  462. *
  463. * @param string $text the raw description sent from gpg-agent.
  464. *
  465. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  466. */
  467. protected function sendSetDescription($text)
  468. {
  469. $text = rawurldecode($text);
  470. $matches = array();
  471. // TODO: handle user id with quotation marks
  472. $exp = '/\n"(.+)"\n.*\sID ([A-Z0-9]+),\n/mu';
  473. if (preg_match($exp, $text, $matches) === 1) {
  474. $userId = $matches[1];
  475. $keyId = $matches[2];
  476. if ($this->currentPin === null || $this->currentPin['keyId'] !== $keyId) {
  477. $this->currentPin = array(
  478. 'userId' => $userId,
  479. 'keyId' => $keyId
  480. );
  481. $this->log(
  482. '-- looking for PIN for ' . $keyId . PHP_EOL,
  483. self::VERBOSITY_ALL
  484. );
  485. }
  486. }
  487. return $this->send($this->getOK());
  488. }
  489. // }}}
  490. // {{{ sendConfirm()
  491. /**
  492. * Tells the assuan server to confirm the operation
  493. *
  494. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  495. */
  496. protected function sendConfirm()
  497. {
  498. return $this->send($this->getOK());
  499. }
  500. // }}}
  501. // {{{ sendMessage()
  502. /**
  503. * Tells the assuan server that any requested pop-up messages were confirmed
  504. * by pressing the fake 'close' button
  505. *
  506. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  507. */
  508. protected function sendMessage()
  509. {
  510. return $this->sendButtonInfo('close');
  511. }
  512. // }}}
  513. // {{{ sendButtonInfo()
  514. /**
  515. * Sends information about pressed buttons to the assuan server
  516. *
  517. * This is used to fake a user-interface for this pinentry.
  518. *
  519. * @param string $text the button status to send.
  520. *
  521. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  522. */
  523. protected function sendButtonInfo($text)
  524. {
  525. return $this->send('BUTTON_INFO ' . $text . "\n");
  526. }
  527. // }}}
  528. // {{{ sendGetPin()
  529. /**
  530. * Sends the PIN value for the currently requested key
  531. *
  532. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  533. */
  534. protected function sendGetPin()
  535. {
  536. $foundPin = '';
  537. if (is_array($this->currentPin)) {
  538. $keyIdLength = mb_strlen($this->currentPin['keyId'], '8bit');
  539. // search for the pin
  540. foreach ($this->pins as $_keyId => $pin) {
  541. // Warning: GnuPG 2.1 asks 3 times for passphrase if it is invalid
  542. $keyId = $this->currentPin['keyId'];
  543. $_keyIdLength = mb_strlen($_keyId, '8bit');
  544. // Get last X characters of key identifier to compare
  545. // Most GnuPG versions use 8 characters, but recent ones can use 16,
  546. // We support 8 for backward compatibility
  547. if ($keyIdLength < $_keyIdLength) {
  548. $_keyId = mb_substr($_keyId, -$keyIdLength, $keyIdLength, '8bit');
  549. } else if ($keyIdLength > $_keyIdLength) {
  550. $keyId = mb_substr($keyId, -$_keyIdLength, $_keyIdLength, '8bit');
  551. }
  552. if ($_keyId === $keyId) {
  553. $foundPin = $pin;
  554. break;
  555. }
  556. }
  557. }
  558. return $this
  559. ->send($this->getData($foundPin))
  560. ->send($this->getOK());
  561. }
  562. // }}}
  563. // {{{ sendGetInfo()
  564. /**
  565. * Sends information about this pinentry
  566. *
  567. * @param string $data the information requested by the assuan server.
  568. * Currently only 'pid' is supported. Other requests
  569. * return no information.
  570. *
  571. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  572. */
  573. protected function sendGetInfo($data)
  574. {
  575. $parts = explode(' ', $data, 2);
  576. $command = reset($parts);
  577. switch ($command) {
  578. case 'pid':
  579. return $this->sendGetInfoPID();
  580. default:
  581. return $this->send($this->getOK());
  582. }
  583. return $this;
  584. }
  585. // }}}
  586. // {{{ sendGetInfoPID()
  587. /**
  588. * Sends the PID of this pinentry to the assuan server
  589. *
  590. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  591. */
  592. protected function sendGetInfoPID()
  593. {
  594. return $this
  595. ->send($this->getData(getmypid()))
  596. ->send($this->getOK());
  597. }
  598. // }}}
  599. // {{{ sendBye()
  600. /**
  601. * Flags this pinentry for disconnection and sends an OK response
  602. *
  603. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  604. */
  605. protected function sendBye()
  606. {
  607. $return = $this->send($this->getOK('closing connection'));
  608. $this->moribund = true;
  609. return $return;
  610. }
  611. // }}}
  612. // {{{ sendReset()
  613. /**
  614. * Resets this pinentry and sends an OK response
  615. *
  616. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  617. */
  618. protected function sendReset()
  619. {
  620. $this->currentPin = null;
  621. return $this->send($this->getOK());
  622. }
  623. // }}}
  624. // {{{ getOK()
  625. /**
  626. * Gets an OK response to send to the assuan server
  627. *
  628. * @param string $data an optional message to include with the OK response.
  629. *
  630. * @return string the OK response.
  631. */
  632. protected function getOK($data = null)
  633. {
  634. $return = 'OK';
  635. if ($data) {
  636. $return .= ' ' . $data;
  637. }
  638. return $return . "\n";
  639. }
  640. // }}}
  641. // {{{ getData()
  642. /**
  643. * Gets data ready to send to the assuan server
  644. *
  645. * Data is appropriately escaped and long lines are wrapped.
  646. *
  647. * @param string $data the data to send to the assuan server.
  648. *
  649. * @return string the properly escaped, formatted data.
  650. *
  651. * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
  652. */
  653. protected function getData($data)
  654. {
  655. // Escape data. Only %, \n and \r need to be escaped but other
  656. // values are allowed to be escaped. See
  657. // http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
  658. $data = rawurlencode($data);
  659. $data = $this->getWordWrappedData($data, 'D');
  660. return $data;
  661. }
  662. // }}}
  663. // {{{ getComment()
  664. /**
  665. * Gets a comment ready to send to the assuan server
  666. *
  667. * @param string $data the comment to send to the assuan server.
  668. *
  669. * @return string the properly formatted comment.
  670. *
  671. * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
  672. */
  673. protected function getComment($data)
  674. {
  675. return $this->getWordWrappedData($data, '#');
  676. }
  677. // }}}
  678. // {{{ getWordWrappedData()
  679. /**
  680. * Wraps strings at 1,000 bytes without splitting UTF-8 multibyte
  681. * characters
  682. *
  683. * Each line is prepended with the specified line prefix. Wrapped lines
  684. * are automatically appended with \ characters.
  685. *
  686. * Protocol strings are UTF-8 but maximum line length is 1,000 bytes.
  687. * <kbd>mb_strcut()</kbd> is used so we can limit line length by bytes
  688. * and not split characters across multiple lines.
  689. *
  690. * @param string $data the data to wrap.
  691. * @param string $prefix a single character to use as the line prefix. For
  692. * example, 'D' or '#'.
  693. *
  694. * @return string the word-wrapped, prefixed string.
  695. *
  696. * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
  697. */
  698. protected function getWordWrappedData($data, $prefix)
  699. {
  700. $lines = array();
  701. do {
  702. if (mb_strlen($data, '8bit') > 997) {
  703. $line = $prefix . ' ' . mb_strcut($data, 0, 996, 'utf-8') . "\\\n";
  704. $lines[] = $line;
  705. $lineLength = mb_strlen($line, '8bit') - 1;
  706. $dataLength = mb_substr($data, '8bit');
  707. $data = mb_substr(
  708. $data,
  709. $lineLength,
  710. $dataLength - $lineLength,
  711. '8bit'
  712. );
  713. } else {
  714. $lines[] = $prefix . ' ' . $data . "\n";
  715. $data = '';
  716. }
  717. } while ($data != '');
  718. return implode('', $lines);
  719. }
  720. // }}}
  721. // {{{ send()
  722. /**
  723. * Sends raw data to the assuan server
  724. *
  725. * @param string $data the data to send.
  726. *
  727. * @return Crypt_GPG_PinEntry the current object, for fluent interface.
  728. */
  729. protected function send($data)
  730. {
  731. $this->log('-> ' . $data, self::VERBOSITY_ALL);
  732. fwrite($this->stdout, $data);
  733. fflush($this->stdout);
  734. return $this;
  735. }
  736. // }}}
  737. }
  738. // }}}
  739. ?>