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