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.

enigma_engine.php 40KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337
  1. <?php
  2. /**
  3. +-------------------------------------------------------------------------+
  4. | Engine of the Enigma Plugin |
  5. | |
  6. | Copyright (C) 2010-2016 The Roundcube Dev Team |
  7. | |
  8. | Licensed under the GNU General Public License version 3 or |
  9. | any later version with exceptions for skins & plugins. |
  10. | See the README file for a full license statement. |
  11. | |
  12. +-------------------------------------------------------------------------+
  13. | Author: Aleksander Machniak <alec@alec.pl> |
  14. +-------------------------------------------------------------------------+
  15. */
  16. /**
  17. * Enigma plugin engine.
  18. *
  19. * RFC2440: OpenPGP Message Format
  20. * RFC3156: MIME Security with OpenPGP
  21. * RFC3851: S/MIME
  22. */
  23. class enigma_engine
  24. {
  25. private $rc;
  26. private $enigma;
  27. private $pgp_driver;
  28. private $smime_driver;
  29. private $password_time;
  30. public $decryptions = array();
  31. public $signatures = array();
  32. public $encrypted_parts = array();
  33. const ENCRYPTED_PARTIALLY = 100;
  34. const SIGN_MODE_BODY = 1;
  35. const SIGN_MODE_SEPARATE = 2;
  36. const SIGN_MODE_MIME = 3;
  37. const ENCRYPT_MODE_BODY = 1;
  38. const ENCRYPT_MODE_MIME = 2;
  39. /**
  40. * Plugin initialization.
  41. */
  42. function __construct($enigma)
  43. {
  44. $this->rc = rcmail::get_instance();
  45. $this->enigma = $enigma;
  46. $this->password_time = $this->rc->config->get('enigma_password_time') * 60;
  47. // this will remove passwords from session after some time
  48. if ($this->password_time) {
  49. $this->get_passwords();
  50. }
  51. }
  52. /**
  53. * PGP driver initialization.
  54. */
  55. function load_pgp_driver()
  56. {
  57. if ($this->pgp_driver) {
  58. return;
  59. }
  60. $driver = 'enigma_driver_' . $this->rc->config->get('enigma_pgp_driver', 'gnupg');
  61. $username = $this->rc->user->get_username();
  62. // Load driver
  63. $this->pgp_driver = new $driver($username);
  64. if (!$this->pgp_driver) {
  65. rcube::raise_error(array(
  66. 'code' => 600, 'type' => 'php',
  67. 'file' => __FILE__, 'line' => __LINE__,
  68. 'message' => "Enigma plugin: Unable to load PGP driver: $driver"
  69. ), true, true);
  70. }
  71. // Initialise driver
  72. $result = $this->pgp_driver->init();
  73. if ($result instanceof enigma_error) {
  74. rcube::raise_error(array(
  75. 'code' => 600, 'type' => 'php',
  76. 'file' => __FILE__, 'line' => __LINE__,
  77. 'message' => "Enigma plugin: ".$result->getMessage()
  78. ), true, true);
  79. }
  80. }
  81. /**
  82. * S/MIME driver initialization.
  83. */
  84. function load_smime_driver()
  85. {
  86. if ($this->smime_driver) {
  87. return;
  88. }
  89. $driver = 'enigma_driver_' . $this->rc->config->get('enigma_smime_driver', 'phpssl');
  90. $username = $this->rc->user->get_username();
  91. // Load driver
  92. $this->smime_driver = new $driver($username);
  93. if (!$this->smime_driver) {
  94. rcube::raise_error(array(
  95. 'code' => 600, 'type' => 'php',
  96. 'file' => __FILE__, 'line' => __LINE__,
  97. 'message' => "Enigma plugin: Unable to load S/MIME driver: $driver"
  98. ), true, true);
  99. }
  100. // Initialise driver
  101. $result = $this->smime_driver->init();
  102. if ($result instanceof enigma_error) {
  103. rcube::raise_error(array(
  104. 'code' => 600, 'type' => 'php',
  105. 'file' => __FILE__, 'line' => __LINE__,
  106. 'message' => "Enigma plugin: ".$result->getMessage()
  107. ), true, true);
  108. }
  109. }
  110. /**
  111. * Handler for message signing
  112. *
  113. * @param Mail_mime Original message
  114. * @param int Encryption mode
  115. *
  116. * @return enigma_error On error returns error object
  117. */
  118. function sign_message(&$message, $mode = null)
  119. {
  120. $mime = new enigma_mime_message($message, enigma_mime_message::PGP_SIGNED);
  121. $from = $mime->getFromAddress();
  122. // find private key
  123. $key = $this->find_key($from, true);
  124. if (empty($key)) {
  125. return new enigma_error(enigma_error::KEYNOTFOUND);
  126. }
  127. // check if we have password for this key
  128. $passwords = $this->get_passwords();
  129. $pass = $passwords[$key->id];
  130. if ($pass === null) {
  131. // ask for password
  132. $error = array('missing' => array($key->id => $key->name));
  133. return new enigma_error(enigma_error::BADPASS, '', $error);
  134. }
  135. // select mode
  136. switch ($mode) {
  137. case self::SIGN_MODE_BODY:
  138. $pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
  139. break;
  140. case self::SIGN_MODE_MIME:
  141. $pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
  142. break;
  143. /*
  144. case self::SIGN_MODE_SEPARATE:
  145. $pgp_mode = Crypt_GPG::SIGN_MODE_NORMAL;
  146. break;
  147. */
  148. default:
  149. if ($mime->isMultipart()) {
  150. $pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
  151. }
  152. else {
  153. $pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
  154. }
  155. }
  156. // get message body
  157. if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
  158. // in this mode we'll replace text part
  159. // with the one containing signature
  160. $body = $message->getTXTBody();
  161. $text_charset = $message->getParam('text_charset');
  162. $line_length = $this->rc->config->get('line_length', 72);
  163. // We can't use format=flowed for signed messages
  164. if (strpos($text_charset, 'format=flowed')) {
  165. list($charset, $params) = explode(';', $text_charset);
  166. $body = rcube_mime::unfold_flowed($body);
  167. $body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset);
  168. $text_charset = str_replace(";\r\n format=flowed", '', $text_charset);
  169. }
  170. }
  171. else {
  172. // here we'll build PGP/MIME message
  173. $body = $mime->getOrigBody();
  174. }
  175. // sign the body
  176. $result = $this->pgp_sign($body, $key->id, $pass, $pgp_mode);
  177. if ($result !== true) {
  178. if ($result->getCode() == enigma_error::BADPASS) {
  179. // ask for password
  180. $error = array('bad' => array($key->id => $key->name));
  181. return new enigma_error(enigma_error::BADPASS, '', $error);
  182. }
  183. return $result;
  184. }
  185. // replace message body
  186. if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
  187. $message->setTXTBody($body);
  188. $message->setParam('text_charset', $text_charset);
  189. }
  190. else {
  191. $mime->addPGPSignature($body);
  192. $message = $mime;
  193. }
  194. }
  195. /**
  196. * Handler for message encryption
  197. *
  198. * @param Mail_mime Original message
  199. * @param int Encryption mode
  200. * @param bool Is draft-save action - use only sender's key for encryption
  201. *
  202. * @return enigma_error On error returns error object
  203. */
  204. function encrypt_message(&$message, $mode = null, $is_draft = false)
  205. {
  206. $mime = new enigma_mime_message($message, enigma_mime_message::PGP_ENCRYPTED);
  207. // always use sender's key
  208. $recipients = array($mime->getFromAddress());
  209. // if it's not a draft we add all recipients' keys
  210. if (!$is_draft) {
  211. $recipients = array_merge($recipients, $mime->getRecipients());
  212. }
  213. if (empty($recipients)) {
  214. return new enigma_error(enigma_error::KEYNOTFOUND);
  215. }
  216. $recipients = array_unique($recipients);
  217. // find recipient public keys
  218. foreach ((array) $recipients as $email) {
  219. $key = $this->find_key($email);
  220. if (empty($key)) {
  221. return new enigma_error(enigma_error::KEYNOTFOUND, '', array(
  222. 'missing' => $email
  223. ));
  224. }
  225. $keys[] = $key->id;
  226. }
  227. // select mode
  228. switch ($mode) {
  229. case self::ENCRYPT_MODE_BODY:
  230. $encrypt_mode = $mode;
  231. break;
  232. case self::ENCRYPT_MODE_MIME:
  233. $encrypt_mode = $mode;
  234. break;
  235. default:
  236. $encrypt_mode = $mime->isMultipart() ? self::ENCRYPT_MODE_MIME : self::ENCRYPT_MODE_BODY;
  237. }
  238. // get message body
  239. if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
  240. // in this mode we'll replace text part
  241. // with the one containing encrypted message
  242. $body = $message->getTXTBody();
  243. }
  244. else {
  245. // here we'll build PGP/MIME message
  246. $body = $mime->getOrigBody();
  247. }
  248. // sign the body
  249. $result = $this->pgp_encrypt($body, $keys);
  250. if ($result !== true) {
  251. return $result;
  252. }
  253. // replace message body
  254. if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
  255. $message->setTXTBody($body);
  256. }
  257. else {
  258. $mime->setPGPEncryptedBody($body);
  259. $message = $mime;
  260. }
  261. }
  262. /**
  263. * Handler for attaching public key to a message
  264. *
  265. * @param Mail_mime Original message
  266. *
  267. * @return bool True on success, False on failure
  268. */
  269. function attach_public_key(&$message)
  270. {
  271. $headers = $message->headers();
  272. $from = rcube_mime::decode_address_list($headers['From'], 1, false, null, true);
  273. $from = $from[1];
  274. // find my key
  275. if ($from && ($key = $this->find_key($from))) {
  276. $pubkey_armor = $this->export_key($key->id);
  277. if (!$pubkey_armor instanceof enigma_error) {
  278. $pubkey_name = '0x' . enigma_key::format_id($key->id) . '.asc';
  279. $message->addAttachment($pubkey_armor, 'application/pgp-keys', $pubkey_name, false, '7bit');
  280. return true;
  281. }
  282. }
  283. return false;
  284. }
  285. /**
  286. * Handler for message_part_structure hook.
  287. * Called for every part of the message.
  288. *
  289. * @param array Original parameters
  290. * @param string Part body (will be set if used internally)
  291. *
  292. * @return array Modified parameters
  293. */
  294. function part_structure($p, $body = null)
  295. {
  296. if ($p['mimetype'] == 'text/plain' || $p['mimetype'] == 'application/pgp') {
  297. $this->parse_plain($p, $body);
  298. }
  299. else if ($p['mimetype'] == 'multipart/signed') {
  300. $this->parse_signed($p, $body);
  301. }
  302. else if ($p['mimetype'] == 'multipart/encrypted') {
  303. $this->parse_encrypted($p);
  304. }
  305. else if ($p['mimetype'] == 'application/pkcs7-mime') {
  306. $this->parse_encrypted($p);
  307. }
  308. return $p;
  309. }
  310. /**
  311. * Handler for message_part_body hook.
  312. *
  313. * @param array Original parameters
  314. *
  315. * @return array Modified parameters
  316. */
  317. function part_body($p)
  318. {
  319. // encrypted attachment, see parse_plain_encrypted()
  320. if ($p['part']->need_decryption && $p['part']->body === null) {
  321. $this->load_pgp_driver();
  322. $storage = $this->rc->get_storage();
  323. $body = $storage->get_message_part($p['object']->uid, $p['part']->mime_id, $p['part'], null, null, true, 0, false);
  324. $result = $this->pgp_decrypt($body);
  325. // @TODO: what to do on error?
  326. if ($result === true) {
  327. $p['part']->body = $body;
  328. $p['part']->size = strlen($body);
  329. $p['part']->body_modified = true;
  330. }
  331. }
  332. return $p;
  333. }
  334. /**
  335. * Handler for plain/text message.
  336. *
  337. * @param array Reference to hook's parameters
  338. * @param string Part body (will be set if used internally)
  339. */
  340. function parse_plain(&$p, $body = null)
  341. {
  342. $part = $p['structure'];
  343. // exit, if we're already inside a decrypted message
  344. if (in_array($part->mime_id, $this->encrypted_parts)) {
  345. return;
  346. }
  347. // Get message body from IMAP server
  348. if ($body === null) {
  349. $body = $this->get_part_body($p['object'], $part);
  350. }
  351. // In this way we can use fgets on string as on file handle
  352. // Don't use php://temp for security (body may come from an encrypted part)
  353. $fd = fopen('php://memory', 'r+');
  354. if (!$fd) {
  355. return;
  356. }
  357. fwrite($fd, $body);
  358. rewind($fd);
  359. $body = '';
  360. $prefix = '';
  361. $mode = '';
  362. $tokens = array(
  363. 'BEGIN PGP SIGNED MESSAGE' => 'signed-start',
  364. 'END PGP SIGNATURE' => 'signed-end',
  365. 'BEGIN PGP MESSAGE' => 'encrypted-start',
  366. 'END PGP MESSAGE' => 'encrypted-end',
  367. );
  368. $regexp = '/^-----(' . implode('|', array_keys($tokens)) . ')-----[\r\n]*/';
  369. while (($line = fgets($fd)) !== false) {
  370. if ($line[0] === '-' && $line[4] === '-' && preg_match($regexp, $line, $m)) {
  371. switch ($tokens[$m[1]]) {
  372. case 'signed-start':
  373. $body = $line;
  374. $mode = 'signed';
  375. break;
  376. case 'signed-end':
  377. if ($mode === 'signed') {
  378. $body .= $line;
  379. }
  380. break 2; // ignore anything after this line
  381. case 'encrypted-start':
  382. $body = $line;
  383. $mode = 'encrypted';
  384. break;
  385. case 'encrypted-end':
  386. if ($mode === 'encrypted') {
  387. $body .= $line;
  388. }
  389. break 2; // ignore anything after this line
  390. }
  391. continue;
  392. }
  393. if ($mode === 'signed') {
  394. $body .= $line;
  395. }
  396. else if ($mode === 'encrypted') {
  397. $body .= $line;
  398. }
  399. else {
  400. $prefix .= $line;
  401. }
  402. }
  403. fclose($fd);
  404. if ($mode === 'signed') {
  405. $this->parse_plain_signed($p, $body, $prefix);
  406. }
  407. else if ($mode === 'encrypted') {
  408. $this->parse_plain_encrypted($p, $body, $prefix);
  409. }
  410. }
  411. /**
  412. * Handler for multipart/signed message.
  413. *
  414. * @param array Reference to hook's parameters
  415. * @param string Part body (will be set if used internally)
  416. */
  417. function parse_signed(&$p, $body = null)
  418. {
  419. $struct = $p['structure'];
  420. // S/MIME
  421. if ($struct->parts[1] && $struct->parts[1]->mimetype == 'application/pkcs7-signature') {
  422. $this->parse_smime_signed($p, $body);
  423. }
  424. // PGP/MIME: RFC3156
  425. // The multipart/signed body MUST consist of exactly two parts.
  426. // The first part contains the signed data in MIME canonical format,
  427. // including a set of appropriate content headers describing the data.
  428. // The second body MUST contain the PGP digital signature. It MUST be
  429. // labeled with a content type of "application/pgp-signature".
  430. else if (count($struct->parts) == 2
  431. && $struct->parts[1] && $struct->parts[1]->mimetype == 'application/pgp-signature'
  432. ) {
  433. $this->parse_pgp_signed($p, $body);
  434. }
  435. }
  436. /**
  437. * Handler for multipart/encrypted message.
  438. *
  439. * @param array Reference to hook's parameters
  440. */
  441. function parse_encrypted(&$p)
  442. {
  443. $struct = $p['structure'];
  444. // S/MIME
  445. if ($p['mimetype'] == 'application/pkcs7-mime') {
  446. $this->parse_smime_encrypted($p);
  447. }
  448. // PGP/MIME: RFC3156
  449. // The multipart/encrypted MUST consist of exactly two parts. The first
  450. // MIME body part must have a content type of "application/pgp-encrypted".
  451. // This body contains the control information.
  452. // The second MIME body part MUST contain the actual encrypted data. It
  453. // must be labeled with a content type of "application/octet-stream".
  454. else if (count($struct->parts) == 2
  455. && $struct->parts[0] && $struct->parts[0]->mimetype == 'application/pgp-encrypted'
  456. && $struct->parts[1] && $struct->parts[1]->mimetype == 'application/octet-stream'
  457. ) {
  458. $this->parse_pgp_encrypted($p);
  459. }
  460. }
  461. /**
  462. * Handler for plain signed message.
  463. * Excludes message and signature bodies and verifies signature.
  464. *
  465. * @param array Reference to hook's parameters
  466. * @param string Message (part) body
  467. * @param string Body prefix (additional text before the encrypted block)
  468. */
  469. private function parse_plain_signed(&$p, $body, $prefix = '')
  470. {
  471. if (!$this->rc->config->get('enigma_signatures', true)) {
  472. return;
  473. }
  474. $this->load_pgp_driver();
  475. $part = $p['structure'];
  476. // Verify signature
  477. if ($this->rc->action == 'show' || $this->rc->action == 'preview' || $this->rc->action == 'print') {
  478. $sig = $this->pgp_verify($body);
  479. }
  480. // In this way we can use fgets on string as on file handle
  481. // Don't use php://temp for security (body may come from an encrypted part)
  482. $fd = fopen('php://memory', 'r+');
  483. if (!$fd) {
  484. return;
  485. }
  486. fwrite($fd, $body);
  487. rewind($fd);
  488. $body = $part->body = null;
  489. $part->body_modified = true;
  490. // Extract body (and signature?)
  491. while (($line = fgets($fd, 1024)) !== false) {
  492. if ($part->body === null)
  493. $part->body = '';
  494. else if (preg_match('/^-----BEGIN PGP SIGNATURE-----/', $line))
  495. break;
  496. else
  497. $part->body .= $line;
  498. }
  499. fclose($fd);
  500. // Remove "Hash" Armor Headers
  501. $part->body = preg_replace('/^.*\r*\n\r*\n/', '', $part->body);
  502. // de-Dash-Escape (RFC2440)
  503. $part->body = preg_replace('/(^|\n)- -/', '\\1-', $part->body);
  504. if ($prefix) {
  505. $part->body = $prefix . $part->body;
  506. }
  507. // Store signature data for display
  508. if (!empty($sig)) {
  509. $sig->partial = !empty($prefix);
  510. $this->signatures[$part->mime_id] = $sig;
  511. }
  512. }
  513. /**
  514. * Handler for PGP/MIME signed message.
  515. * Verifies signature.
  516. *
  517. * @param array Reference to hook's parameters
  518. * @param string Part body (will be set if used internally)
  519. */
  520. private function parse_pgp_signed(&$p, $body = null)
  521. {
  522. if (!$this->rc->config->get('enigma_signatures', true)) {
  523. return;
  524. }
  525. if ($this->rc->action != 'show' && $this->rc->action != 'preview' && $this->rc->action != 'print') {
  526. return;
  527. }
  528. $this->load_pgp_driver();
  529. $struct = $p['structure'];
  530. $msg_part = $struct->parts[0];
  531. $sig_part = $struct->parts[1];
  532. // Get bodies
  533. if ($body === null) {
  534. if (!$struct->body_modified) {
  535. $body = $this->get_part_body($p['object'], $struct);
  536. }
  537. }
  538. $boundary = $struct->ctype_parameters['boundary'];
  539. // when it is a signed message forwarded as attachment
  540. // ctype_parameters property will not be set
  541. if (!$boundary && $struct->headers['content-type']
  542. && preg_match('/boundary="?([a-zA-Z0-9\'()+_,-.\/:=?]+)"?/', $struct->headers['content-type'], $m)
  543. ) {
  544. $boundary = $m[1];
  545. }
  546. // set signed part body
  547. list($msg_body, $sig_body) = $this->explode_signed_body($body, $boundary);
  548. // Verify
  549. if ($sig_body && $msg_body) {
  550. $sig = $this->pgp_verify($msg_body, $sig_body);
  551. // Store signature data for display
  552. $this->signatures[$struct->mime_id] = $sig;
  553. $this->signatures[$msg_part->mime_id] = $sig;
  554. }
  555. }
  556. /**
  557. * Handler for S/MIME signed message.
  558. * Verifies signature.
  559. *
  560. * @param array Reference to hook's parameters
  561. * @param string Part body (will be set if used internally)
  562. */
  563. private function parse_smime_signed(&$p, $body = null)
  564. {
  565. if (!$this->rc->config->get('enigma_signatures', true)) {
  566. return;
  567. }
  568. // @TODO
  569. }
  570. /**
  571. * Handler for plain encrypted message.
  572. *
  573. * @param array Reference to hook's parameters
  574. * @param string Message (part) body
  575. * @param string Body prefix (additional text before the encrypted block)
  576. */
  577. private function parse_plain_encrypted(&$p, $body, $prefix = '')
  578. {
  579. if (!$this->rc->config->get('enigma_decryption', true)) {
  580. return;
  581. }
  582. $this->load_pgp_driver();
  583. $part = $p['structure'];
  584. // Decrypt
  585. $result = $this->pgp_decrypt($body);
  586. // Store decryption status
  587. $this->decryptions[$part->mime_id] = $result;
  588. // find parent part ID
  589. if (strpos($part->mime_id, '.')) {
  590. $items = explode('.', $part->mime_id);
  591. array_pop($items);
  592. $parent = implode('.', $items);
  593. }
  594. else {
  595. $parent = 0;
  596. }
  597. // Parse decrypted message
  598. if ($result === true) {
  599. $part->body = $prefix . $body;
  600. $part->body_modified = true;
  601. // it maybe PGP signed inside, verify signature
  602. $this->parse_plain($p, $body);
  603. // Remember it was decrypted
  604. $this->encrypted_parts[] = $part->mime_id;
  605. // Inform the user that only a part of the body was encrypted
  606. if ($prefix) {
  607. $this->decryptions[$part->mime_id] = self::ENCRYPTED_PARTIALLY;
  608. }
  609. // Encrypted plain message may contain encrypted attachments
  610. // in such case attachments have .pgp extension and type application/octet-stream.
  611. // This is what happens when you select "Encrypt each attachment separately
  612. // and send the message using inline PGP" in Thunderbird's Enigmail.
  613. if ($p['object']->mime_parts[$parent]) {
  614. foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
  615. if ($p->disposition == 'attachment' && $p->mimetype == 'application/octet-stream'
  616. && preg_match('/^(.*)\.pgp$/i', $p->filename, $m)
  617. ) {
  618. // modify filename
  619. $p->filename = $m[1];
  620. // flag the part, it will be decrypted when needed
  621. $p->need_decryption = true;
  622. // disable caching
  623. $p->body_modified = true;
  624. }
  625. }
  626. }
  627. }
  628. // decryption failed, but the message may have already
  629. // been cached with the modified parts (see above),
  630. // let's bring the original state back
  631. else if ($p['object']->mime_parts[$parent]) {
  632. foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
  633. if ($p->need_decryption && !preg_match('/^(.*)\.pgp$/i', $p->filename, $m)) {
  634. // modify filename
  635. $p->filename .= '.pgp';
  636. // flag the part, it will be decrypted when needed
  637. unset($p->need_decryption);
  638. }
  639. }
  640. }
  641. }
  642. /**
  643. * Handler for PGP/MIME encrypted message.
  644. *
  645. * @param array Reference to hook's parameters
  646. */
  647. private function parse_pgp_encrypted(&$p)
  648. {
  649. if (!$this->rc->config->get('enigma_decryption', true)) {
  650. return;
  651. }
  652. $this->load_pgp_driver();
  653. $struct = $p['structure'];
  654. $part = $struct->parts[1];
  655. // Get body
  656. $body = $this->get_part_body($p['object'], $part);
  657. // Decrypt
  658. $result = $this->pgp_decrypt($body);
  659. if ($result === true) {
  660. // Parse decrypted message
  661. $struct = $this->parse_body($body);
  662. // Modify original message structure
  663. $this->modify_structure($p, $struct, strlen($body));
  664. // Parse the structure (there may be encrypted/signed parts inside
  665. $this->part_structure(array(
  666. 'object' => $p['object'],
  667. 'structure' => $struct,
  668. 'mimetype' => $struct->mimetype
  669. ), $body);
  670. // Attach the decryption message to all parts
  671. $this->decryptions[$struct->mime_id] = $result;
  672. foreach ((array) $struct->parts as $sp) {
  673. $this->decryptions[$sp->mime_id] = $result;
  674. }
  675. }
  676. else {
  677. $this->decryptions[$part->mime_id] = $result;
  678. // Make sure decryption status message will be displayed
  679. $part->type = 'content';
  680. $p['object']->parts[] = $part;
  681. // don't show encrypted part on attachments list
  682. // don't show "cannot display encrypted message" text
  683. $p['abort'] = true;
  684. }
  685. }
  686. /**
  687. * Handler for S/MIME encrypted message.
  688. *
  689. * @param array Reference to hook's parameters
  690. */
  691. private function parse_smime_encrypted(&$p)
  692. {
  693. if (!$this->rc->config->get('enigma_decryption', true)) {
  694. return;
  695. }
  696. // @TODO
  697. }
  698. /**
  699. * PGP signature verification.
  700. *
  701. * @param mixed Message body
  702. * @param mixed Signature body (for MIME messages)
  703. *
  704. * @return mixed enigma_signature or enigma_error
  705. */
  706. private function pgp_verify(&$msg_body, $sig_body=null)
  707. {
  708. // @TODO: Handle big bodies using (temp) files
  709. $sig = $this->pgp_driver->verify($msg_body, $sig_body);
  710. if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::KEYNOTFOUND)
  711. rcube::raise_error(array(
  712. 'code' => 600, 'type' => 'php',
  713. 'file' => __FILE__, 'line' => __LINE__,
  714. 'message' => "Enigma plugin: " . $sig->getMessage()
  715. ), true, false);
  716. return $sig;
  717. }
  718. /**
  719. * PGP message decryption.
  720. *
  721. * @param mixed Message body
  722. *
  723. * @return mixed True or enigma_error
  724. */
  725. private function pgp_decrypt(&$msg_body)
  726. {
  727. // @TODO: Handle big bodies using (temp) files
  728. $keys = $this->get_passwords();
  729. $result = $this->pgp_driver->decrypt($msg_body, $keys);
  730. if ($result instanceof enigma_error) {
  731. $err_code = $result->getCode();
  732. if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
  733. rcube::raise_error(array(
  734. 'code' => 600, 'type' => 'php',
  735. 'file' => __FILE__, 'line' => __LINE__,
  736. 'message' => "Enigma plugin: " . $result->getMessage()
  737. ), true, false);
  738. return $result;
  739. }
  740. $msg_body = $result;
  741. return true;
  742. }
  743. /**
  744. * PGP message signing
  745. *
  746. * @param mixed Message body
  747. * @param string Key ID
  748. * @param string Key passphrase
  749. * @param int Signing mode
  750. *
  751. * @return mixed True or enigma_error
  752. */
  753. private function pgp_sign(&$msg_body, $keyid, $password, $mode = null)
  754. {
  755. // @TODO: Handle big bodies using (temp) files
  756. $result = $this->pgp_driver->sign($msg_body, $keyid, $password, $mode);
  757. if ($result instanceof enigma_error) {
  758. $err_code = $result->getCode();
  759. if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
  760. rcube::raise_error(array(
  761. 'code' => 600, 'type' => 'php',
  762. 'file' => __FILE__, 'line' => __LINE__,
  763. 'message' => "Enigma plugin: " . $result->getMessage()
  764. ), true, false);
  765. return $result;
  766. }
  767. $msg_body = $result;
  768. return true;
  769. }
  770. /**
  771. * PGP message encrypting
  772. *
  773. * @param mixed Message body
  774. * @param array Keys
  775. *
  776. * @return mixed True or enigma_error
  777. */
  778. private function pgp_encrypt(&$msg_body, $keys)
  779. {
  780. // @TODO: Handle big bodies using (temp) files
  781. $result = $this->pgp_driver->encrypt($msg_body, $keys);
  782. if ($result instanceof enigma_error) {
  783. $err_code = $result->getCode();
  784. if (!in_array($err_code, array(enigma_error::KEYNOTFOUND, enigma_error::BADPASS)))
  785. rcube::raise_error(array(
  786. 'code' => 600, 'type' => 'php',
  787. 'file' => __FILE__, 'line' => __LINE__,
  788. 'message' => "Enigma plugin: " . $result->getMessage()
  789. ), true, false);
  790. return $result;
  791. }
  792. $msg_body = $result;
  793. return true;
  794. }
  795. /**
  796. * PGP keys listing.
  797. *
  798. * @param mixed Key ID/Name pattern
  799. *
  800. * @return mixed Array of keys or enigma_error
  801. */
  802. function list_keys($pattern = '')
  803. {
  804. $this->load_pgp_driver();
  805. $result = $this->pgp_driver->list_keys($pattern);
  806. if ($result instanceof enigma_error) {
  807. rcube::raise_error(array(
  808. 'code' => 600, 'type' => 'php',
  809. 'file' => __FILE__, 'line' => __LINE__,
  810. 'message' => "Enigma plugin: " . $result->getMessage()
  811. ), true, false);
  812. }
  813. return $result;
  814. }
  815. /**
  816. * Find PGP private/public key
  817. *
  818. * @param string E-mail address
  819. * @param bool Need a key for signing?
  820. *
  821. * @return enigma_key The key
  822. */
  823. function find_key($email, $can_sign = false)
  824. {
  825. $this->load_pgp_driver();
  826. $result = $this->pgp_driver->list_keys($email);
  827. if ($result instanceof enigma_error) {
  828. rcube::raise_error(array(
  829. 'code' => 600, 'type' => 'php',
  830. 'file' => __FILE__, 'line' => __LINE__,
  831. 'message' => "Enigma plugin: " . $result->getMessage()
  832. ), true, false);
  833. return;
  834. }
  835. $mode = $can_sign ? enigma_key::CAN_SIGN : enigma_key::CAN_ENCRYPT;
  836. // check key validity and type
  837. foreach ($result as $key) {
  838. if ($keyid = $key->find_subkey($email, $mode)) {
  839. return $key;
  840. }
  841. }
  842. }
  843. /**
  844. * PGP key details.
  845. *
  846. * @param mixed Key ID
  847. *
  848. * @return mixed enigma_key or enigma_error
  849. */
  850. function get_key($keyid)
  851. {
  852. $this->load_pgp_driver();
  853. $result = $this->pgp_driver->get_key($keyid);
  854. if ($result instanceof enigma_error) {
  855. rcube::raise_error(array(
  856. 'code' => 600, 'type' => 'php',
  857. 'file' => __FILE__, 'line' => __LINE__,
  858. 'message' => "Enigma plugin: " . $result->getMessage()
  859. ), true, false);
  860. }
  861. return $result;
  862. }
  863. /**
  864. * PGP key delete.
  865. *
  866. * @param string Key ID
  867. *
  868. * @return enigma_error|bool True on success
  869. */
  870. function delete_key($keyid)
  871. {
  872. $this->load_pgp_driver();
  873. $result = $this->pgp_driver->delete_key($keyid);
  874. if ($result instanceof enigma_error) {
  875. rcube::raise_error(array(
  876. 'code' => 600, 'type' => 'php',
  877. 'file' => __FILE__, 'line' => __LINE__,
  878. 'message' => "Enigma plugin: " . $result->getMessage()
  879. ), true, false);
  880. }
  881. return $result;
  882. }
  883. /**
  884. * PGP keys pair generation.
  885. *
  886. * @param array Key pair parameters
  887. *
  888. * @return mixed enigma_key or enigma_error
  889. */
  890. function generate_key($data)
  891. {
  892. $this->load_pgp_driver();
  893. $result = $this->pgp_driver->gen_key($data);
  894. if ($result instanceof enigma_error) {
  895. rcube::raise_error(array(
  896. 'code' => 600, 'type' => 'php',
  897. 'file' => __FILE__, 'line' => __LINE__,
  898. 'message' => "Enigma plugin: " . $result->getMessage()
  899. ), true, false);
  900. }
  901. return $result;
  902. }
  903. /**
  904. * PGP keys/certs import.
  905. *
  906. * @param mixed Import file name or content
  907. * @param boolean True if first argument is a filename
  908. *
  909. * @return mixed Import status data array or enigma_error
  910. */
  911. function import_key($content, $isfile=false)
  912. {
  913. $this->load_pgp_driver();
  914. $result = $this->pgp_driver->import($content, $isfile);
  915. if ($result instanceof enigma_error) {
  916. rcube::raise_error(array(
  917. 'code' => 600, 'type' => 'php',
  918. 'file' => __FILE__, 'line' => __LINE__,
  919. 'message' => "Enigma plugin: " . $result->getMessage()
  920. ), true, false);
  921. }
  922. else {
  923. $result['imported'] = $result['public_imported'] + $result['private_imported'];
  924. $result['unchanged'] = $result['public_unchanged'] + $result['private_unchanged'];
  925. }
  926. return $result;
  927. }
  928. /**
  929. * PGP keys/certs export.
  930. *
  931. * @param string Key ID
  932. * @param resource Optional output stream
  933. * @param bool Include private key
  934. *
  935. * @return mixed Key content or enigma_error
  936. */
  937. function export_key($key, $fp = null, $include_private = false)
  938. {
  939. $this->load_pgp_driver();
  940. $result = $this->pgp_driver->export($key, $include_private);
  941. if ($result instanceof enigma_error) {
  942. rcube::raise_error(array(
  943. 'code' => 600, 'type' => 'php',
  944. 'file' => __FILE__, 'line' => __LINE__,
  945. 'message' => "Enigma plugin: " . $result->getMessage()
  946. ), true, false);
  947. return $result;
  948. }
  949. if ($fp) {
  950. fwrite($fp, $result);
  951. }
  952. else {
  953. return $result;
  954. }
  955. }
  956. /**
  957. * Registers password for specified key/cert sent by the password prompt.
  958. */
  959. function password_handler()
  960. {
  961. $keyid = rcube_utils::get_input_value('_keyid', rcube_utils::INPUT_POST);
  962. $passwd = rcube_utils::get_input_value('_passwd', rcube_utils::INPUT_POST, true);
  963. if ($keyid && $passwd !== null && strlen($passwd)) {
  964. $this->save_password($keyid, $passwd);
  965. }
  966. }
  967. /**
  968. * Saves key/cert password in user session
  969. */
  970. function save_password($keyid, $password)
  971. {
  972. // we store passwords in session for specified time
  973. if ($config = $_SESSION['enigma_pass']) {
  974. $config = $this->rc->decrypt($config);
  975. $config = @unserialize($config);
  976. }
  977. $config[$keyid] = array($password, time());
  978. $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
  979. }
  980. /**
  981. * Returns currently stored passwords
  982. */
  983. function get_passwords()
  984. {
  985. if ($config = $_SESSION['enigma_pass']) {
  986. $config = $this->rc->decrypt($config);
  987. $config = @unserialize($config);
  988. }
  989. $threshold = $this->password_time ? time() - $this->password_time : 0;
  990. $keys = array();
  991. // delete expired passwords
  992. foreach ((array) $config as $key => $value) {
  993. if ($threshold && $value[1] < $threshold) {
  994. unset($config[$key]);
  995. $modified = true;
  996. }
  997. else {
  998. $keys[$key] = $value[0];
  999. }
  1000. }
  1001. if ($modified) {
  1002. $_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
  1003. }
  1004. return $keys;
  1005. }
  1006. /**
  1007. * Get message part body.
  1008. *
  1009. * @param rcube_message Message object
  1010. * @param rcube_message_part Message part
  1011. */
  1012. private function get_part_body($msg, $part)
  1013. {
  1014. // @TODO: Handle big bodies using file handles
  1015. // This is a special case when we want to get the whole body
  1016. // using direct IMAP access, in other cases we prefer
  1017. // rcube_message::get_part_body() as the body may be already in memory
  1018. if (!$part->mime_id) {
  1019. // fake the size which may be empty for multipart/* parts
  1020. // otherwise get_message_part() below will fail
  1021. if (!$part->size) {
  1022. $reset = true;
  1023. $part->size = 1;
  1024. }
  1025. $storage = $this->rc->get_storage();
  1026. $body = $storage->get_message_part($msg->uid, $part->mime_id, $part,
  1027. null, null, true, 0, false);
  1028. if ($reset) {
  1029. $part->size = 0;
  1030. }
  1031. }
  1032. else {
  1033. $body = $msg->get_part_body($part->mime_id, false);
  1034. }
  1035. return $body;
  1036. }
  1037. /**
  1038. * Parse decrypted message body into structure
  1039. *
  1040. * @param string Message body
  1041. *
  1042. * @return array Message structure
  1043. */
  1044. private function parse_body(&$body)
  1045. {
  1046. // Mail_mimeDecode need \r\n end-line, but gpg may return \n
  1047. $body = preg_replace('/\r?\n/', "\r\n", $body);
  1048. // parse the body into structure
  1049. $struct = rcube_mime::parse_message($body);
  1050. return $struct;
  1051. }
  1052. /**
  1053. * Replace message encrypted structure with decrypted message structure
  1054. *
  1055. * @param array Hook arguments
  1056. * @param rcube_message_part Part structure
  1057. * @param int Part size
  1058. */
  1059. private function modify_structure(&$p, $struct, $size = 0)
  1060. {
  1061. // modify mime_parts property of the message object
  1062. $old_id = $p['structure']->mime_id;
  1063. foreach (array_keys($p['object']->mime_parts) as $idx) {
  1064. if (!$old_id || $idx == $old_id || strpos($idx, $old_id . '.') === 0) {
  1065. unset($p['object']->mime_parts[$idx]);
  1066. }
  1067. }
  1068. // set some part params used by Roundcube core
  1069. $struct->headers = array_merge($p['structure']->headers, $struct->headers);
  1070. $struct->size = $size;
  1071. $struct->filename = $p['structure']->filename;
  1072. // modify the new structure to be correctly handled by Roundcube
  1073. $this->modify_structure_part($struct, $p['object'], $old_id);
  1074. // replace old structure with the new one
  1075. $p['structure'] = $struct;
  1076. $p['mimetype'] = $struct->mimetype;
  1077. }
  1078. /**
  1079. * Modify decrypted message part
  1080. *
  1081. * @param rcube_message_part
  1082. * @param rcube_message
  1083. */
  1084. private function modify_structure_part($part, $msg, $old_id)
  1085. {
  1086. // never cache the body
  1087. $part->body_modified = true;
  1088. $part->encoding = 'stream';
  1089. // modify part identifier
  1090. if ($old_id) {
  1091. $part->mime_id = !$part->mime_id ? $old_id : ($old_id . '.' . $part->mime_id);
  1092. }
  1093. // Cache the fact it was decrypted
  1094. $this->encrypted_parts[] = $part->mime_id;
  1095. $msg->mime_parts[$part->mime_id] = $part;
  1096. // modify sub-parts
  1097. foreach ((array) $part->parts as $p) {
  1098. $this->modify_structure_part($p, $msg, $old_id);
  1099. }
  1100. }
  1101. /**
  1102. * Extracts body and signature of multipart/signed message body
  1103. */
  1104. private function explode_signed_body($body, $boundary)
  1105. {
  1106. if (!$body) {
  1107. return array();
  1108. }
  1109. $boundary = '--' . $boundary;
  1110. $boundary_len = strlen($boundary) + 2;
  1111. // Find boundaries
  1112. $start = strpos($body, $boundary) + $boundary_len;
  1113. $end = strpos($body, $boundary, $start);
  1114. // Get signed body and signature
  1115. $sig = substr($body, $end + $boundary_len);
  1116. $body = substr($body, $start, $end - $start - 2);
  1117. // Cleanup signature
  1118. $sig = substr($sig, strpos($sig, "\r\n\r\n") + 4);
  1119. $sig = substr($sig, 0, strpos($sig, $boundary));
  1120. return array($body, $sig);
  1121. }
  1122. /**
  1123. * Checks if specified message part is a PGP-key or S/MIME cert data
  1124. *
  1125. * @param rcube_message_part Part object
  1126. *
  1127. * @return boolean True if part is a key/cert
  1128. */
  1129. public function is_keys_part($part)
  1130. {
  1131. // @TODO: S/MIME
  1132. return (
  1133. // Content-Type: application/pgp-keys
  1134. $part->mimetype == 'application/pgp-keys'
  1135. );
  1136. }
  1137. }