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_driver_gnupg.php 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. <?php
  2. /**
  3. +-------------------------------------------------------------------------+
  4. | GnuPG (PGP) driver for the Enigma Plugin |
  5. | |
  6. | Copyright (C) 2010-2015 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. require_once 'Crypt/GPG.php';
  17. class enigma_driver_gnupg extends enigma_driver
  18. {
  19. protected $rc;
  20. protected $gpg;
  21. protected $homedir;
  22. protected $user;
  23. protected $last_sig_algorithm;
  24. function __construct($user)
  25. {
  26. $this->rc = rcmail::get_instance();
  27. $this->user = $user;
  28. }
  29. /**
  30. * Driver initialization and environment checking.
  31. * Should only return critical errors.
  32. *
  33. * @return mixed NULL on success, enigma_error on failure
  34. */
  35. function init()
  36. {
  37. $homedir = $this->rc->config->get('enigma_pgp_homedir', INSTALL_PATH . 'plugins/enigma/home');
  38. $debug = $this->rc->config->get('enigma_debug');
  39. $binary = $this->rc->config->get('enigma_pgp_binary');
  40. $agent = $this->rc->config->get('enigma_pgp_agent');
  41. $gpgconf = $this->rc->config->get('enigma_pgp_gpgconf');
  42. if (!$homedir) {
  43. return new enigma_error(enigma_error::INTERNAL,
  44. "Option 'enigma_pgp_homedir' not specified");
  45. }
  46. // check if homedir exists (create it if not) and is readable
  47. if (!file_exists($homedir)) {
  48. return new enigma_error(enigma_error::INTERNAL,
  49. "Keys directory doesn't exists: $homedir");
  50. }
  51. if (!is_writable($homedir)) {
  52. return new enigma_error(enigma_error::INTERNAL,
  53. "Keys directory isn't writeable: $homedir");
  54. }
  55. $homedir = $homedir . '/' . $this->user;
  56. // check if user's homedir exists (create it if not) and is readable
  57. if (!file_exists($homedir)) {
  58. mkdir($homedir, 0700);
  59. }
  60. if (!file_exists($homedir)) {
  61. return new enigma_error(enigma_error::INTERNAL,
  62. "Unable to create keys directory: $homedir");
  63. }
  64. if (!is_writable($homedir)) {
  65. return new enigma_error(enigma_error::INTERNAL,
  66. "Unable to write to keys directory: $homedir");
  67. }
  68. $this->homedir = $homedir;
  69. $options = array('homedir' => $this->homedir);
  70. if ($debug) {
  71. $options['debug'] = array($this, 'debug');
  72. }
  73. if ($binary) {
  74. $options['binary'] = $binary;
  75. }
  76. if ($agent) {
  77. $options['agent'] = $agent;
  78. }
  79. if ($gpgconf) {
  80. $options['gpgconf'] = $gpgconf;
  81. }
  82. // Create Crypt_GPG object
  83. try {
  84. $this->gpg = new Crypt_GPG($options);
  85. }
  86. catch (Exception $e) {
  87. return $this->get_error_from_exception($e);
  88. }
  89. }
  90. /**
  91. * Encryption (and optional signing).
  92. *
  93. * @param string Message body
  94. * @param array List of keys (enigma_key objects)
  95. * @param enigma_key Optional signing Key ID
  96. *
  97. * @return mixed Encrypted message or enigma_error on failure
  98. */
  99. function encrypt($text, $keys, $sign_key = null)
  100. {
  101. try {
  102. foreach ($keys as $key) {
  103. $this->gpg->addEncryptKey($key->reference);
  104. }
  105. if ($sign_key) {
  106. $this->gpg->addSignKey($sign_key->reference, $sign_key->password);
  107. $res = $this->gpg->encryptAndSign($text, true);
  108. $sigInfo = $this->gpg->getLastSignatureInfo();
  109. $this->last_sig_algorithm = $sigInfo->getHashAlgorithmName();
  110. return $res;
  111. }
  112. return $this->gpg->encrypt($text, true);
  113. }
  114. catch (Exception $e) {
  115. return $this->get_error_from_exception($e);
  116. }
  117. }
  118. /**
  119. * Decrypt a message (and verify if signature found)
  120. *
  121. * @param string Encrypted message
  122. * @param array List of key-password mapping
  123. * @param enigma_signature Signature information (if available)
  124. *
  125. * @return mixed Decrypted message or enigma_error on failure
  126. */
  127. function decrypt($text, $keys = array(), &$signature = null)
  128. {
  129. try {
  130. foreach ($keys as $key => $password) {
  131. $this->gpg->addDecryptKey($key, $password);
  132. }
  133. $result = $this->gpg->decryptAndVerify($text, true);
  134. if (!empty($result['signatures'])) {
  135. $signature = $this->parse_signature($result['signatures'][0]);
  136. }
  137. return $result['data'];
  138. }
  139. catch (Exception $e) {
  140. return $this->get_error_from_exception($e);
  141. }
  142. }
  143. /**
  144. * Signing.
  145. *
  146. * @param string Message body
  147. * @param enigma_key The key
  148. * @param int Signing mode (enigma_engine::SIGN_*)
  149. *
  150. * @return mixed True on success or enigma_error on failure
  151. */
  152. function sign($text, $key, $mode = null)
  153. {
  154. try {
  155. $this->gpg->addSignKey($key->reference, $key->password);
  156. $res = $this->gpg->sign($text, $mode, CRYPT_GPG::ARMOR_ASCII, true);
  157. $sigInfo = $this->gpg->getLastSignatureInfo();
  158. $this->last_sig_algorithm = $sigInfo->getHashAlgorithmName();
  159. return $res;
  160. }
  161. catch (Exception $e) {
  162. return $this->get_error_from_exception($e);
  163. }
  164. }
  165. /**
  166. * Signature verification.
  167. *
  168. * @param string Message body
  169. * @param string Signature, if message is of type PGP/MIME and body doesn't contain it
  170. *
  171. * @return mixed Signature information (enigma_signature) or enigma_error
  172. */
  173. function verify($text, $signature)
  174. {
  175. try {
  176. $verified = $this->gpg->verify($text, $signature);
  177. return $this->parse_signature($verified[0]);
  178. }
  179. catch (Exception $e) {
  180. return $this->get_error_from_exception($e);
  181. }
  182. }
  183. /**
  184. * Key file import.
  185. *
  186. * @param string File name or file content
  187. * @param bolean True if first argument is a filename
  188. * @param array Optional key => password map
  189. *
  190. * @return mixed Import status array or enigma_error
  191. */
  192. public function import($content, $isfile = false, $passwords = array())
  193. {
  194. try {
  195. // GnuPG 2.1 requires secret key passphrases on import
  196. foreach ($passwords as $keyid => $pass) {
  197. $this->gpg->addPassphrase($keyid, $pass);
  198. }
  199. if ($isfile)
  200. return $this->gpg->importKeyFile($content);
  201. else
  202. return $this->gpg->importKey($content);
  203. }
  204. catch (Exception $e) {
  205. return $this->get_error_from_exception($e);
  206. }
  207. }
  208. /**
  209. * Key export.
  210. *
  211. * @param string Key ID
  212. * @param bool Include private key
  213. * @param array Optional key => password map
  214. *
  215. * @return mixed Key content or enigma_error
  216. */
  217. public function export($keyid, $with_private = false, $passwords = array())
  218. {
  219. try {
  220. $key = $this->gpg->exportPublicKey($keyid, true);
  221. if ($with_private) {
  222. // GnuPG 2.1 requires secret key passphrases on export
  223. foreach ($passwords as $_keyid => $pass) {
  224. $this->gpg->addPassphrase($_keyid, $pass);
  225. }
  226. $priv = $this->gpg->exportPrivateKey($keyid, true);
  227. $key .= $priv;
  228. }
  229. return $key;
  230. }
  231. catch (Exception $e) {
  232. return $this->get_error_from_exception($e);
  233. }
  234. }
  235. /**
  236. * Keys listing.
  237. *
  238. * @param string Optional pattern for key ID, user ID or fingerprint
  239. *
  240. * @return mixed Array of enigma_key objects or enigma_error
  241. */
  242. public function list_keys($pattern = '')
  243. {
  244. try {
  245. $keys = $this->gpg->getKeys($pattern);
  246. $result = array();
  247. foreach ($keys as $idx => $key) {
  248. $result[] = $this->parse_key($key);
  249. unset($keys[$idx]);
  250. }
  251. return $result;
  252. }
  253. catch (Exception $e) {
  254. return $this->get_error_from_exception($e);
  255. }
  256. }
  257. /**
  258. * Single key information.
  259. *
  260. * @param string Key ID, user ID or fingerprint
  261. *
  262. * @return mixed Key (enigma_key) object or enigma_error
  263. */
  264. public function get_key($keyid)
  265. {
  266. $list = $this->list_keys($keyid);
  267. if (is_array($list)) {
  268. return $list[key($list)];
  269. }
  270. // error
  271. return $list;
  272. }
  273. /**
  274. * Key pair generation.
  275. *
  276. * @param array Key/User data (user, email, password, size)
  277. *
  278. * @return mixed Key (enigma_key) object or enigma_error
  279. */
  280. public function gen_key($data)
  281. {
  282. try {
  283. $debug = $this->rc->config->get('enigma_debug');
  284. $keygen = new Crypt_GPG_KeyGenerator(array(
  285. 'homedir' => $this->homedir,
  286. // 'binary' => '/usr/bin/gpg2',
  287. 'debug' => $debug ? array($this, 'debug') : false,
  288. ));
  289. $key = $keygen
  290. ->setExpirationDate(0)
  291. ->setPassphrase($data['password'])
  292. ->generateKey($data['user'], $data['email']);
  293. return $this->parse_key($key);
  294. }
  295. catch (Exception $e) {
  296. return $this->get_error_from_exception($e);
  297. }
  298. }
  299. /**
  300. * Key deletion.
  301. *
  302. * @param string Key ID
  303. *
  304. * @return mixed True on success or enigma_error
  305. */
  306. public function delete_key($keyid)
  307. {
  308. // delete public key
  309. $result = $this->delete_pubkey($keyid);
  310. // error handling
  311. if ($result !== true) {
  312. $code = $result->getCode();
  313. // if not found, delete private key
  314. if ($code == enigma_error::KEYNOTFOUND) {
  315. $result = $this->delete_privkey($keyid);
  316. }
  317. // need to delete private key first
  318. else if ($code == enigma_error::DELKEY) {
  319. $key = $this->get_key($keyid);
  320. for ($i = count($key->subkeys) - 1; $i >= 0; $i--) {
  321. $type = ($key->subkeys[$i]->usage & enigma_key::CAN_ENCRYPT) ? 'priv' : 'pub';
  322. $result = $this->{'delete_' . $type . 'key'}($key->subkeys[$i]->id);
  323. if ($result !== true) {
  324. return $result;
  325. }
  326. }
  327. }
  328. }
  329. return $result;
  330. }
  331. /**
  332. * Returns a name of the hash algorithm used for the last
  333. * signing operation.
  334. *
  335. * @return string Hash algorithm name e.g. sha1
  336. */
  337. public function signature_algorithm()
  338. {
  339. return $this->last_sig_algorithm;
  340. }
  341. /**
  342. * Private key deletion.
  343. */
  344. protected function delete_privkey($keyid)
  345. {
  346. try {
  347. $this->gpg->deletePrivateKey($keyid);
  348. return true;
  349. }
  350. catch (Exception $e) {
  351. return $this->get_error_from_exception($e);
  352. }
  353. }
  354. /**
  355. * Public key deletion.
  356. */
  357. protected function delete_pubkey($keyid)
  358. {
  359. try {
  360. $this->gpg->deletePublicKey($keyid);
  361. return true;
  362. }
  363. catch (Exception $e) {
  364. return $this->get_error_from_exception($e);
  365. }
  366. }
  367. /**
  368. * Converts Crypt_GPG exception into Enigma's error object
  369. *
  370. * @param mixed Exception object
  371. *
  372. * @return enigma_error Error object
  373. */
  374. protected function get_error_from_exception($e)
  375. {
  376. $data = array();
  377. if ($e instanceof Crypt_GPG_KeyNotFoundException) {
  378. $error = enigma_error::KEYNOTFOUND;
  379. $data['id'] = $e->getKeyId();
  380. }
  381. else if ($e instanceof Crypt_GPG_BadPassphraseException) {
  382. $error = enigma_error::BADPASS;
  383. $data['bad'] = $e->getBadPassphrases();
  384. $data['missing'] = $e->getMissingPassphrases();
  385. }
  386. else if ($e instanceof Crypt_GPG_NoDataException) {
  387. $error = enigma_error::NODATA;
  388. }
  389. else if ($e instanceof Crypt_GPG_DeletePrivateKeyException) {
  390. $error = enigma_error::DELKEY;
  391. }
  392. else {
  393. $error = enigma_error::INTERNAL;
  394. }
  395. $msg = $e->getMessage();
  396. return new enigma_error($error, $msg, $data);
  397. }
  398. /**
  399. * Converts Crypt_GPG_Signature object into Enigma's signature object
  400. *
  401. * @param Crypt_GPG_Signature Signature object
  402. *
  403. * @return enigma_signature Signature object
  404. */
  405. protected function parse_signature($sig)
  406. {
  407. $data = new enigma_signature();
  408. $data->id = $sig->getId() ?: $sig->getKeyId();
  409. $data->valid = $sig->isValid();
  410. $data->fingerprint = $sig->getKeyFingerprint();
  411. $data->created = $sig->getCreationDate();
  412. $data->expires = $sig->getExpirationDate();
  413. // In case of ERRSIG user may not be set
  414. if ($user = $sig->getUserId()) {
  415. $data->name = $user->getName();
  416. $data->comment = $user->getComment();
  417. $data->email = $user->getEmail();
  418. }
  419. return $data;
  420. }
  421. /**
  422. * Converts Crypt_GPG_Key object into Enigma's key object
  423. *
  424. * @param Crypt_GPG_Key Key object
  425. *
  426. * @return enigma_key Key object
  427. */
  428. protected function parse_key($key)
  429. {
  430. $ekey = new enigma_key();
  431. foreach ($key->getUserIds() as $idx => $user) {
  432. $id = new enigma_userid();
  433. $id->name = $user->getName();
  434. $id->comment = $user->getComment();
  435. $id->email = $user->getEmail();
  436. $id->valid = $user->isValid();
  437. $id->revoked = $user->isRevoked();
  438. $ekey->users[$idx] = $id;
  439. }
  440. $ekey->name = trim($ekey->users[0]->name . ' <' . $ekey->users[0]->email . '>');
  441. // keep reference to Crypt_GPG's key for performance reasons
  442. $ekey->reference = $key;
  443. foreach ($key->getSubKeys() as $idx => $subkey) {
  444. $skey = new enigma_subkey();
  445. $skey->id = $subkey->getId();
  446. $skey->revoked = $subkey->isRevoked();
  447. $skey->created = $subkey->getCreationDate();
  448. $skey->expires = $subkey->getExpirationDate();
  449. $skey->fingerprint = $subkey->getFingerprint();
  450. $skey->has_private = $subkey->hasPrivate();
  451. $skey->algorithm = $subkey->getAlgorithm();
  452. $skey->length = $subkey->getLength();
  453. $skey->usage = $subkey->usage();
  454. $ekey->subkeys[$idx] = $skey;
  455. };
  456. $ekey->id = $ekey->subkeys[0]->id;
  457. return $ekey;
  458. }
  459. /**
  460. * Write debug info from Crypt_GPG to logs/enigma
  461. */
  462. public function debug($line)
  463. {
  464. rcube::write_log('enigma', 'GPG: ' . $line);
  465. }
  466. }