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