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

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