enigma_engine.php 41 KB

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