enigma_driver_gnupg.php 15 KB

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