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 42KB

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