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 } |
