1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
22: class Horde_Crypt_Pgp extends Horde_Crypt
23: {
24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
37:
38:
39: const ARMOR_MESSAGE = 1;
40:
41:
42: const ARMOR_SIGNED_MESSAGE = 2;
43:
44:
45: const ARMOR_PUBLIC_KEY = 3;
46:
47:
48: const ARMOR_PRIVATE_KEY = 4;
49:
50: 51:
52: const ARMOR_SIGNATURE = 5;
53:
54:
55: const ARMOR_TEXT = 6;
56:
57: 58: 59: 60: 61: 62:
63: protected $_armor = array(
64: 'MESSAGE' => self::ARMOR_MESSAGE,
65: 'SIGNED MESSAGE' => self::ARMOR_SIGNED_MESSAGE,
66: 'PUBLIC KEY BLOCK' => self::ARMOR_PUBLIC_KEY,
67: 'PRIVATE KEY BLOCK' => self::ARMOR_PRIVATE_KEY,
68: 'SIGNATURE' => self::ARMOR_SIGNATURE
69: );
70:
71:
72: const KEYSERVER_PUBLIC = 'pgp.mit.edu';
73:
74: 75:
76: const KEYSERVER_REFUSE = 3;
77:
78: 79:
80: const KEYSERVER_TIMEOUT = 10;
81:
82: 83: 84: 85: 86:
87: protected $_hashAlg = array(
88: 1 => 'pgp-md5',
89: 2 => 'pgp-sha1',
90: 3 => 'pgp-ripemd160',
91: 5 => 'pgp-md2',
92: 6 => 'pgp-tiger192',
93: 7 => 'pgp-haval-5-160',
94: 8 => 'pgp-sha256',
95: 9 => 'pgp-sha384',
96: 10 => 'pgp-sha512',
97: 11 => 'pgp-sha224',
98: );
99:
100: 101: 102: 103: 104:
105: protected $_gnupg;
106:
107: 108: 109: 110: 111:
112: protected $_publicKeyring;
113:
114: 115: 116: 117: 118:
119: protected $_privateKeyring;
120:
121: 122: 123: 124: 125:
126: protected $_params = array();
127:
128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139:
140: public function __construct($params = array())
141: {
142: parent::__construct($params);
143:
144: if (empty($params['program'])) {
145: throw new InvalidArgumentException('The location of the GnuPG binary must be given to the Horde_Crypt_Pgp:: class.');
146: }
147:
148:
149: $this->_gnupg = array(
150: $params['program'],
151: '--no-tty',
152: '--no-secmem-warning',
153: '--no-options',
154: '--no-default-keyring',
155: '--yes',
156: '--homedir ' . $this->_tempdir
157: );
158:
159: if (strncasecmp(PHP_OS, 'WIN', 3)) {
160: array_unshift($this->_gnupg, 'LANG= ;');
161: }
162:
163: $this->_params = $params;
164: }
165:
166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185:
186: public function generateKey($realname, $email, $passphrase, $comment = '',
187: $keylength = 1024, $expire = null)
188: {
189:
190: $pub_file = $this->_createTempFile('horde-pgp');
191: $secret_file = $this->_createTempFile('horde-pgp');
192:
193: $expire = empty($expire)
194: ? 0
195: : date('Y-m-d', $expire);
196:
197:
198:
199: $input = array(
200: '%pubring ' . $pub_file,
201: '%secring ' . $secret_file,
202: 'Key-Type: DSA',
203: 'Key-Length: 1024',
204: 'Subkey-Type: ELG-E',
205: 'Subkey-Length: ' . $keylength,
206: 'Name-Real: ' . $realname,
207: 'Name-Email: ' . $email,
208: 'Expire-Date: ' . $expire,
209: 'Passphrase: ' . $passphrase
210: );
211: if (!empty($comment)) {
212: $input[] = 'Name-Comment: ' . $comment;
213: }
214: $input[] = '%commit';
215:
216:
217: $cmdline = array(
218: '--gen-key',
219: '--batch',
220: '--armor'
221: );
222:
223: $result = $this->_callGpg($cmdline, 'w', $input, true, true);
224:
225:
226: $public_key = file($pub_file);
227: $secret_key = file($secret_file);
228:
229:
230: if (empty($public_key) || empty($secret_key)) {
231: $msg = Horde_Crypt_Translation::t("Public/Private keypair not generated successfully.");
232: if (!empty($result->stderr)) {
233: $msg .= ' ' . Horde_Crypt_Translation::t("Returned error message:") . ' ' . $result->stderr;
234: }
235: throw new Horde_Crypt_Exception($msg);
236: }
237:
238: return array('public' => $public_key, 'private' => $secret_key);
239: }
240:
241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288:
289: public function pgpPacketInformation($pgpdata)
290: {
291: $data_array = array();
292: $keyid = '';
293: $header = null;
294: $input = $this->_createTempFile('horde-pgp');
295: $sig_id = $uid_idx = 0;
296:
297:
298: file_put_contents($input, $pgpdata);
299:
300: $cmdline = array(
301: '--list-packets',
302: $input
303: );
304: $result = $this->_callGpg($cmdline, 'r');
305:
306: foreach (explode("\n", $result->stdout) as $line) {
307: 308:
309: if (strpos($line, ':') === 0) {
310: $lowerLine = Horde_String::lower($line);
311:
312: 313:
314: if (strpos($lowerLine, ':public key packet:') !== false ||
315: strpos($lowerLine, ':secret key packet:') !== false) {
316: $cmdline = array(
317: '--with-colons',
318: $input
319: );
320: $data = $this->_callGpg($cmdline, 'r');
321: if (preg_match("/(sec|pub):.*:.*:.*:([A-F0-9]{16}):/", $data->stdout, $matches)) {
322: $keyid = $matches[2];
323: }
324: }
325:
326: if (strpos($lowerLine, ':public key packet:') !== false) {
327: $header = 'public_key';
328: } elseif (strpos($lowerLine, ':secret key packet:') !== false) {
329: $header = 'secret_key';
330: } elseif (strpos($lowerLine, ':user id packet:') !== false) {
331: $uid_idx++;
332: $line = preg_replace_callback('/\\\\x([0-9a-f]{2})/', array($this, '_pgpPacketInformationHelper'), $line);
333: if (preg_match("/\"([^\<]+)\<([^\>]+)\>\"/", $line, $matches)) {
334: $header = 'id' . $uid_idx;
335: if (preg_match('/([^\(]+)\((.+)\)$/', trim($matches[1]), $comment_matches)) {
336: $data_array['signature'][$header]['name'] = trim($comment_matches[1]);
337: $data_array['signature'][$header]['comment'] = $comment_matches[2];
338: } else {
339: $data_array['signature'][$header]['name'] = trim($matches[1]);
340: $data_array['signature'][$header]['comment'] = '';
341: }
342: $data_array['signature'][$header]['email'] = $matches[2];
343: $data_array['signature'][$header]['keyid'] = $keyid;
344: }
345: } elseif (strpos($lowerLine, ':signature packet:') !== false) {
346: if (empty($header) || empty($uid_idx)) {
347: $header = '_SIGNATURE';
348: }
349: if (preg_match("/keyid\s+([0-9A-F]+)/i", $line, $matches)) {
350: $sig_id = $matches[1];
351: $data_array['signature'][$header]['sig_' . $sig_id]['keyid'] = $matches[1];
352: $data_array['keyid'] = $matches[1];
353: }
354: } elseif (strpos($lowerLine, ':literal data packet:') !== false) {
355: $header = 'literal';
356: } elseif (strpos($lowerLine, ':encrypted data packet:') !== false) {
357: $header = 'encrypted';
358: } else {
359: $header = null;
360: }
361: } else {
362: if ($header == 'secret_key' || $header == 'public_key') {
363: if (preg_match("/created\s+(\d+),\s+expires\s+(\d+)/i", $line, $matches)) {
364: $data_array[$header]['created'] = $matches[1];
365: $data_array[$header]['expires'] = $matches[2];
366: } elseif (preg_match("/\s+[sp]key\[0\]:\s+\[(\d+)/i", $line, $matches)) {
367: $data_array[$header]['size'] = $matches[1];
368: }
369: } elseif ($header == 'literal' || $header == 'encrypted') {
370: $data_array[$header] = true;
371: } elseif ($header) {
372: if (preg_match("/version\s+\d+,\s+created\s+(\d+)/i", $line, $matches)) {
373: $data_array['signature'][$header]['sig_' . $sig_id]['created'] = $matches[1];
374: } elseif (isset($data_array['signature'][$header]['sig_' . $sig_id]['created']) &&
375: preg_match('/expires after (\d+y\d+d\d+h\d+m)\)$/', $line, $matches)) {
376: $expires = $matches[1];
377: preg_match('/^(\d+)y(\d+)d(\d+)h(\d+)m$/', $expires, $matches);
378: list(, $years, $days, $hours, $minutes) = $matches;
379: $data_array['signature'][$header]['sig_' . $sig_id]['expires'] =
380: strtotime('+ ' . $years . ' years + ' . $days . ' days + ' . $hours . ' hours + ' . $minutes . ' minutes', $data_array['signature'][$header]['sig_' . $sig_id]['created']);
381: } elseif (preg_match("/digest algo\s+(\d{1})/", $line, $matches)) {
382: $micalg = $this->_hashAlg[$matches[1]];
383: $data_array['signature'][$header]['sig_' . $sig_id]['micalg'] = $micalg;
384: if ($header == '_SIGNATURE') {
385:
386: $data_array['signature']['_SIGNATURE']['micalg'] = $micalg;
387: }
388: if ($sig_id == $keyid) {
389: 390: 391:
392: $data_array['signature']['_SIGNATURE']['micalg'] = $micalg;
393: $data_array['signature'][$header]['micalg'] = $micalg;
394: }
395: }
396: }
397: }
398: }
399:
400: $keyid && $data_array['keyid'] = $keyid;
401:
402: return $data_array;
403: }
404:
405: 406: 407:
408: protected function _pgpPacketInformationHelper($a)
409: {
410: return chr(hexdec($a[1]));
411: }
412:
413: 414: 415: 416: 417: 418: 419: 420:
421: public function pgpPrettyKey($pgpdata)
422: {
423: $msg = '';
424: $packet_info = $this->pgpPacketInformation($pgpdata);
425: $fingerprints = $this->getFingerprintsFromKey($pgpdata);
426:
427: if (!empty($packet_info['signature'])) {
428: 429:
430: $leftrow = array(
431: Horde_Crypt_Translation::t("Name"),
432: Horde_Crypt_Translation::t("Key Type"),
433: Horde_Crypt_Translation::t("Key Creation"),
434: Horde_Crypt_Translation::t("Expiration Date"),
435: Horde_Crypt_Translation::t("Key Length"),
436: Horde_Crypt_Translation::t("Comment"),
437: Horde_Crypt_Translation::t("E-Mail"),
438: Horde_Crypt_Translation::t("Hash-Algorithm"),
439: Horde_Crypt_Translation::t("Key ID"),
440: Horde_Crypt_Translation::t("Key Fingerprint")
441: );
442: $leftwidth = array_map('strlen', $leftrow);
443: $maxwidth = max($leftwidth) + 2;
444: array_walk($leftrow, array($this, '_pgpPrettyKeyFormatter'), $maxwidth);
445:
446: foreach ($packet_info['signature'] as $uid_idx => $val) {
447: if ($uid_idx == '_SIGNATURE') {
448: continue;
449: }
450: $key_info = $this->pgpPacketSignatureByUidIndex($pgpdata, $uid_idx);
451:
452: $keyid = empty($key_info['keyid'])
453: ? null
454: : $this->_getKeyIDString($key_info['keyid']);
455: $fingerprint = isset($fingerprints[$keyid])
456: ? $fingerprints[$keyid]
457: : null;
458: $sig_key = 'sig_' . $key_info['keyid'];
459:
460: $msg .= $leftrow[0] . (isset($key_info['name']) ? stripcslashes($key_info['name']) : '') . "\n"
461: . $leftrow[1] . (($key_info['key_type'] == 'public_key') ? Horde_Crypt_Translation::t("Public Key") : Horde_Crypt_Translation::t("Private Key")) . "\n"
462: . $leftrow[2] . strftime("%D", $val[$sig_key]['created']) . "\n"
463: . $leftrow[3] . (empty($val[$sig_key]['expires']) ? '[' . Horde_Crypt_Translation::t("Never") . ']' : strftime("%D", $val[$sig_key]['expires'])) . "\n"
464: . $leftrow[4] . $key_info['key_size'] . " Bytes\n"
465: . $leftrow[5] . (empty($key_info['comment']) ? '[' . Horde_Crypt_Translation::t("None") . ']' : $key_info['comment']) . "\n"
466: . $leftrow[6] . (empty($key_info['email']) ? '[' . Horde_Crypt_Translation::t("None") . ']' : $key_info['email']) . "\n"
467: . $leftrow[7] . (empty($key_info['micalg']) ? '[' . Horde_Crypt_Translation::t("Unknown") . ']' : $key_info['micalg']) . "\n"
468: . $leftrow[8] . (empty($keyid) ? '[' . Horde_Crypt_Translation::t("Unknown") . ']' : $keyid) . "\n"
469: . $leftrow[9] . (empty($fingerprint) ? '[' . Horde_Crypt_Translation::t("Unknown") . ']' : $fingerprint) . "\n\n";
470: }
471: }
472:
473: return $msg;
474: }
475:
476: 477: 478:
479: protected function _pgpPrettyKeyFormatter(&$s, $k, $m)
480: {
481: $s .= ':' . str_repeat(' ', $m - Horde_String::length($s));
482: }
483:
484: 485: 486:
487: protected function _getKeyIDString($keyid)
488: {
489:
490: if (strpos($keyid, '0x') === 0) {
491: $keyid = substr($keyid, 2);
492: }
493: if (strlen($keyid) > 8) {
494: $keyid = substr($keyid, -8);
495: }
496: return '0x' . $keyid;
497: }
498:
499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524:
525: public function pgpPacketSignature($pgpdata, $email)
526: {
527: $data = $this->pgpPacketInformation($pgpdata);
528: $return_array = array();
529:
530:
531: if (!isset($data['signature'])) {
532: return $return_array;
533: }
534:
535:
536: if (($email == '_SIGNATURE') &&
537: isset($data['signature']['_SIGNATURE'])) {
538: foreach ($data['signature'][$email] as $key => $value) {
539: $return_array[$key] = $value;
540: }
541: } else {
542: $uid_idx = 1;
543:
544: while (isset($data['signature']['id' . $uid_idx])) {
545: if ($data['signature']['id' . $uid_idx]['email'] == $email) {
546: foreach ($data['signature']['id' . $uid_idx] as $key => $val) {
547: $return_array[$key] = $val;
548: }
549: break;
550: }
551: $uid_idx++;
552: }
553: }
554:
555: return $this->_pgpPacketSignature($data, $return_array);
556: }
557:
558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568:
569: public function pgpPacketSignatureByUidIndex($pgpdata, $uid_idx)
570: {
571: $data = $this->pgpPacketInformation($pgpdata);
572: $return_array = array();
573:
574:
575: if (!isset($data['signature']) ||
576: !isset($data['signature'][$uid_idx])) {
577: return $return_array;
578: }
579:
580:
581: foreach ($data['signature'][$uid_idx] as $key => $value) {
582: $return_array[$key] = $value;
583: }
584:
585: return $this->_pgpPacketSignature($data, $return_array);
586: }
587:
588: 589: 590: 591: 592: 593: 594: 595:
596: protected function _pgpPacketSignature($data, $retarray)
597: {
598:
599: if (empty($retarray)) {
600: return $retarray;
601: }
602:
603: $key_type = null;
604:
605:
606: if (isset($data['public_key'])) {
607: $key_type = 'public_key';
608: } elseif (isset($data['secret_key'])) {
609: $key_type = 'secret_key';
610: }
611:
612: if ($key_type) {
613: $retarray['key_type'] = $key_type;
614: if (isset($data[$key_type]['created'])) {
615: $retarray['key_created'] = $data[$key_type]['created'];
616: }
617: if (isset($data[$key_type]['expires'])) {
618: $retarray['key_expires'] = $data[$key_type]['expires'];
619: }
620: if (isset($data[$key_type]['size'])) {
621: $retarray['key_size'] = $data[$key_type]['size'];
622: }
623: }
624:
625: return $retarray;
626: }
627:
628: 629: 630: 631: 632: 633: 634: 635:
636: public function getSignersKeyID($text)
637: {
638: $keyid = null;
639:
640: $input = $this->_createTempFile('horde-pgp');
641: file_put_contents($input, $text);
642:
643: $cmdline = array(
644: '--verify',
645: $input
646: );
647: $result = $this->_callGpg($cmdline, 'r', null, true, true);
648: if (preg_match('/gpg:\sSignature\smade.*ID\s+([A-F0-9]{8})\s+/', $result->stderr, $matches)) {
649: $keyid = $matches[1];
650: }
651:
652: return $keyid;
653: }
654:
655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665:
666: public function verifyPassphrase($public_key, $private_key, $passphrase)
667: {
668:
669: $key_info = $this->pgpPacketInformation($public_key);
670: if (!isset($key_info['signature']['id1']['email'])) {
671: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not determine the recipient's e-mail address."));
672: }
673:
674:
675: try {
676: $result = $this->encrypt('Test', array('type' => 'message', 'pubkey' => $public_key, 'recips' => array($key_info['signature']['id1']['email'] => $public_key)));
677: } catch (Horde_Crypt_Exception $e) {
678: return false;
679: }
680:
681:
682: try {
683: $this->decrypt($result, array('type' => 'message', 'pubkey' => $public_key, 'privkey' => $private_key, 'passphrase' => $passphrase));
684: } catch (Horde_Crypt_Exception $e) {
685: return false;
686: }
687:
688: return true;
689: }
690:
691: 692: 693: 694: 695: 696: 697: 698: 699: 700: 701: 702: 703: 704: 705:
706: public function parsePGPData($text)
707: {
708: $data = array();
709: $temp = array(
710: 'type' => self::ARMOR_TEXT
711: );
712:
713: $buffer = explode("\n", $text);
714: while (list(,$val) = each($buffer)) {
715: $val = rtrim($val, "\r");
716: if (preg_match('/^-----(BEGIN|END) PGP ([^-]+)-----\s*$/', $val, $matches)) {
717: if (isset($temp['data'])) {
718: $data[] = $temp;
719: }
720: $temp = array();
721:
722: if ($matches[1] == 'BEGIN') {
723: $temp['type'] = $this->_armor[$matches[2]];
724: $temp['data'][] = $val;
725: } elseif ($matches[1] == 'END') {
726: $temp['type'] = self::ARMOR_TEXT;
727: $data[count($data) - 1]['data'][] = $val;
728: }
729: } else {
730: $temp['data'][] = $val;
731: }
732: }
733:
734: if (isset($temp['data']) &&
735: ((count($temp['data']) > 1) || !empty($temp['data'][0]))) {
736: $data[] = $temp;
737: }
738:
739: return $data;
740: }
741:
742: 743: 744: 745: 746: 747: 748: 749: 750: 751: 752:
753: public function getPublicKeyserver($keyid,
754: $server = self::KEYSERVER_PUBLIC,
755: $timeout = self::KEYSERVER_TIMEOUT,
756: $address = null)
757: {
758: if (empty($keyid) && !empty($address)) {
759: $keyid = $this->getKeyID($address, $server, $timeout);
760: }
761:
762:
763: $uri = '/pks/lookup?op=get&search=' . $this->_getKeyIDString($keyid);
764: $output = $this->_connectKeyserver('GET', $server, $uri, '', $timeout);
765:
766:
767: if (($start = strstr($output, '-----BEGIN'))) {
768: $length = strpos($start, '-----END') + 34;
769: return substr($start, 0, $length);
770: }
771:
772: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not obtain public key from the keyserver."));
773: }
774:
775: 776: 777: 778: 779: 780: 781: 782: 783:
784: public function putPublicKeyserver($pubkey,
785: $server = self::KEYSERVER_PUBLIC,
786: $timeout = self::KEYSERVER_TIMEOUT)
787: {
788:
789: $info = $this->pgpPacketInformation($pubkey);
790:
791:
792: try {
793: $this->getPublicKeyserver($info['keyid'], $server, $timeout);
794: } catch (Horde_Crypt_Exception $e) {
795: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Key already exists on the public keyserver."));
796: }
797:
798:
799: $pubkey = 'keytext=' . urlencode(rtrim($pubkey));
800: $cmd = array(
801: 'Host: ' . $server . ':11371',
802: 'User-Agent: Horde Application Framework',
803: 'Content-Type: application/x-www-form-urlencoded',
804: 'Content-Length: ' . strlen($pubkey),
805: 'Connection: close',
806: '',
807: $pubkey
808: );
809:
810: return $this->_connectKeyserver('POST', $server, '/pks/add', implode("\r\n", $cmd), $timeout);
811: }
812:
813: 814: 815: 816: 817: 818: 819: 820: 821: 822: 823:
824: public function getKeyID($address, $server = self::KEYSERVER_PUBLIC,
825: $timeout = self::KEYSERVER_TIMEOUT)
826: {
827: $pubkey = null;
828:
829:
830: $uri = '/pks/lookup?op=index&options=mr&search=' . urlencode($address);
831: $output = $this->_connectKeyserver('GET', $server, $uri, '', $timeout);
832:
833: if (strpos($output, '-----BEGIN PGP PUBLIC KEY BLOCK') !== false) {
834: $pubkey = $output;
835: } elseif (strpos($output, 'pub:') !== false) {
836: $output = explode("\n", $output);
837: $keyids = $keyuids = array();
838: $curid = null;
839:
840: foreach ($output as $line) {
841: if (substr($line, 0, 4) == 'pub:') {
842: $line = explode(':', $line);
843:
844: if (count($line) != 7 ||
845: (!empty($line[5]) && $line[5] <= time())) {
846: continue;
847: }
848: $curid = $line[4];
849: $keyids[$curid] = $line[1];
850: } elseif (!is_null($curid) && substr($line, 0, 4) == 'uid:') {
851: preg_match("/>([^>]+)>/", $line, $matches);
852: $keyuids[$curid][] = $matches[1];
853: }
854: }
855:
856:
857: foreach ($keyuids as $id => $uids) {
858: $match = false;
859: foreach ($uids as $uid) {
860: if ($uid == $address) {
861: $match = true;
862: break;
863: }
864: }
865: if (!$match) {
866: unset($keyids[$id]);
867: }
868: }
869:
870:
871: if (count($keyids)) {
872: ksort($keyids);
873: $pubkey = $this->getPublicKeyserver(array_pop($keyids), $server, $timeout);
874: }
875: }
876:
877: if ($pubkey) {
878: $sig = $this->pgpPacketSignature($pubkey, $address);
879: if (!empty($sig['keyid']) &&
880: (empty($sig['public_key']['expires']) ||
881: $sig['public_key']['expires'] > time())) {
882: return substr($this->_getKeyIDString($sig['keyid']), 2);
883: }
884: }
885:
886: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not obtain public key from the keyserver."));
887: }
888:
889: 890: 891: 892: 893: 894: 895: 896:
897: public function getFingerprintsFromKey($pgpdata)
898: {
899: $fingerprints = array();
900:
901:
902: $keyring = $this->_putInKeyring($pgpdata);
903:
904:
905: $cmdline = array(
906: '--fingerprint',
907: $keyring,
908: );
909:
910: $result = $this->_callGpg($cmdline, 'r');
911: if (!$result || !$result->stdout) {
912: return $fingerprints;
913: }
914:
915:
916: $lines = explode("\n", $result->stdout);
917: $keyid = null;
918: foreach ($lines as $line) {
919: if (preg_match('/pub\s+\w+\/(\w{8})/', $line, $matches)) {
920: $keyid = '0x' . $matches[1];
921: } elseif ($keyid && preg_match('/^\s+[\s\w]+=\s*([\w\s]+)$/m', $line, $matches)) {
922: $fingerprints[$keyid] = trim($matches[1]);
923: $keyid = null;
924: }
925: }
926:
927: return $fingerprints;
928: }
929:
930: 931: 932: 933: 934: 935: 936: 937: 938: 939: 940: 941: 942:
943: protected function _connectKeyserver($method, $server, $resource,
944: $command, $timeout)
945: {
946: $connRefuse = 0;
947: $output = '';
948:
949: $port = '11371';
950: if (!empty($this->_params['proxy_host'])) {
951: $resource = 'http://' . $server . ':' . $port . $resource;
952:
953: $server = $this->_params['proxy_host'];
954: $port = isset($this->_params['proxy_port'])
955: ? $this->_params['proxy_port']
956: : 80;
957: }
958:
959: $command = $method . ' ' . $resource . ' HTTP/1.0' . ($command ? "\r\n" . $command : '');
960:
961:
962: do {
963: $errno = $errstr = null;
964:
965:
966: $fp = @fsockopen($server, $port, $errno, $errstr, $timeout);
967: if ($fp) {
968: fputs($fp, $command . "\n\n");
969: while (!feof($fp)) {
970: $output .= fgets($fp, 1024);
971: }
972: fclose($fp);
973: return $output;
974: }
975: } while (++$connRefuse < self::KEYSERVER_REFUSE);
976:
977: if ($errno == 0) {
978: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Connection refused to the public keyserver."));
979: } else {
980: $charset = 'UTF-8';
981: $lang_charset = setlocale(LC_ALL, 0);
982: if ((strpos($lang_charset, ';') === false) &&
983: (strpos($lang_charset, '/') === false)) {
984: $lang_charset = explode('.', $lang_charset);
985: if ((count($lang_charset) == 2) && !empty($lang_charset[1])) {
986: $charset = $lang_charset[1];
987: }
988: }
989: throw new Horde_Crypt_Exception(sprintf(Horde_Crypt_Translation::t("Connection refused to the public keyserver. Reason: %s (%s)"), Horde_String::convertCharset($errstr, $charset, 'UTF-8'), $errno));
990: }
991: }
992:
993: 994: 995: 996: 997: 998: 999: 1000: 1001: 1002: 1003:
1004: public function encrypt($text, $params = array())
1005: {
1006: if (isset($params['type'])) {
1007: if ($params['type'] === 'message') {
1008: return $this->_encryptMessage($text, $params);
1009: } elseif ($params['type'] === 'signature') {
1010: return $this->_encryptSignature($text, $params);
1011: }
1012: }
1013: }
1014:
1015: 1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025: 1026: 1027: 1028: 1029:
1030: public function decrypt($text, $params = array())
1031: {
1032: if (isset($params['type'])) {
1033: if ($params['type'] === 'message') {
1034: return $this->_decryptMessage($text, $params);
1035: } elseif (($params['type'] === 'signature') ||
1036: ($params['type'] === 'detached-signature')) {
1037: return $this->_decryptSignature($text, $params);
1038: }
1039: }
1040: }
1041:
1042: 1043: 1044: 1045: 1046: 1047: 1048: 1049:
1050: public function encryptedSymmetrically($text)
1051: {
1052: $cmdline = array(
1053: '--decrypt',
1054: '--batch'
1055: );
1056: $result = $this->_callGpg($cmdline, 'w', $text, true, true, true);
1057: return strpos($result->stderr, 'gpg: encrypted with 1 passphrase') !== false;
1058: }
1059:
1060: 1061: 1062: 1063: 1064: 1065: 1066: 1067:
1068: protected function _createKeyring($type = 'public')
1069: {
1070: $type = Horde_String::lower($type);
1071:
1072: if ($type === 'public') {
1073: if (empty($this->_publicKeyring)) {
1074: $this->_publicKeyring = $this->_createTempFile('horde-pgp');
1075: }
1076: return '--keyring ' . $this->_publicKeyring;
1077: } elseif ($type === 'private') {
1078: if (empty($this->_privateKeyring)) {
1079: $this->_privateKeyring = $this->_createTempFile('horde-pgp');
1080: }
1081: return '--secret-keyring ' . $this->_privateKeyring;
1082: }
1083: }
1084:
1085: 1086: 1087: 1088: 1089: 1090: 1091: 1092: 1093: 1094: 1095:
1096: protected function _putInKeyring($keys = array(), $type = 'public')
1097: {
1098: $type = Horde_String::lower($type);
1099:
1100: if (!is_array($keys)) {
1101: $keys = array($keys);
1102: }
1103:
1104:
1105: $keyring = $this->_createKeyring($type);
1106:
1107:
1108: $cmdline = array(
1109: '--allow-secret-key-import',
1110: '--fast-import',
1111: $keyring
1112: );
1113: $this->_callGpg($cmdline, 'w', array_values($keys));
1114:
1115: return $keyring;
1116: }
1117:
1118: 1119: 1120: 1121: 1122: 1123: 1124: 1125: 1126: 1127: 1128: 1129: 1130: 1131: 1132: 1133: 1134: 1135: 1136: 1137: 1138:
1139: protected function _encryptMessage($text, $params)
1140: {
1141:
1142: $input = $this->_createTempFile('horde-pgp');
1143: file_put_contents($input, $text);
1144:
1145:
1146: $cmdline = array(
1147: '--armor',
1148: '--batch',
1149: '--always-trust'
1150: );
1151:
1152: if (empty($params['symmetric'])) {
1153:
1154: $keyring = $this->_putInKeyring(array_values($params['recips']));
1155:
1156: $cmdline[] = $keyring;
1157: $cmdline[] = '--encrypt';
1158: foreach (array_keys($params['recips']) as $val) {
1159: $cmdline[] = '--recipient ' . $val;
1160: }
1161: } else {
1162: $cmdline[] = '--symmetric';
1163: $cmdline[] = '--passphrase-fd 0';
1164: }
1165: $cmdline[] = $input;
1166:
1167:
1168: $result = $this->_callGpg($cmdline, 'w', empty($params['symmetric']) ? null : $params['passphrase'], true, true);
1169: if (empty($result->output)) {
1170: $error = preg_replace('/\n.*/', '', $result->stderr);
1171: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not PGP encrypt message: ") . $error);
1172: }
1173:
1174: return $result->output;
1175: }
1176:
1177: 1178: 1179: 1180: 1181: 1182: 1183: 1184: 1185: 1186: 1187: 1188: 1189: 1190: 1191: 1192: 1193: 1194: 1195: 1196:
1197: protected function _encryptSignature($text, $params)
1198: {
1199:
1200: if (!isset($params['pubkey']) ||
1201: !isset($params['privkey']) ||
1202: !isset($params['passphrase'])) {
1203: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public PGP key, private PGP key, and passphrase are required to sign a message."));
1204: }
1205:
1206:
1207: $input = $this->_createTempFile('horde-pgp');
1208:
1209:
1210: $pub_keyring = $this->_putInKeyring(array($params['pubkey']));
1211: $sec_keyring = $this->_putInKeyring(array($params['privkey']), 'private');
1212:
1213:
1214: file_put_contents($input, $text);
1215:
1216:
1217: $cmdline = array();
1218: if (isset($params['sigtype']) &&
1219: $params['sigtype'] == 'cleartext') {
1220: $sign_type = '--clearsign';
1221: } else {
1222: $sign_type = '--detach-sign';
1223: }
1224:
1225:
1226: $cmdline += array(
1227: '--armor',
1228: '--batch',
1229: '--passphrase-fd 0',
1230: $sec_keyring,
1231: $pub_keyring,
1232: $sign_type,
1233: $input
1234: );
1235:
1236:
1237: $result = $this->_callGpg($cmdline, 'w', $params['passphrase'], true, true);
1238: if (empty($result->output)) {
1239: $error = preg_replace('/\n.*/', '', $result->stderr);
1240: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not PGP sign message: ") . $error);
1241: }
1242:
1243: return $result->output;
1244: }
1245:
1246: 1247: 1248: 1249: 1250: 1251: 1252: 1253: 1254: 1255: 1256: 1257: 1258: 1259: 1260: 1261: 1262: 1263: 1264: 1265: 1266: 1267:
1268: protected function _decryptMessage($text, $params)
1269: {
1270:
1271: if (!isset($params['passphrase']) && empty($params['no_passphrase'])) {
1272: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A passphrase is required to decrypt a message."));
1273: }
1274:
1275:
1276: $input = $this->_createTempFile('horde-pgp');
1277:
1278:
1279: file_put_contents($input, $text);
1280:
1281:
1282: $cmdline = array(
1283: '--always-trust',
1284: '--armor',
1285: '--batch'
1286: );
1287: if (empty($params['no_passphrase'])) {
1288: $cmdline[] = '--passphrase-fd 0';
1289: }
1290: if (!empty($params['pubkey']) && !empty($params['privkey'])) {
1291:
1292: $pub_keyring = $this->_putInKeyring(array($params['pubkey']));
1293: $sec_keyring = $this->_putInKeyring(array($params['privkey']), 'private');
1294: $cmdline[] = $sec_keyring;
1295: $cmdline[] = $pub_keyring;
1296: }
1297: $cmdline[] = '--decrypt';
1298: $cmdline[] = $input;
1299:
1300:
1301: $language = setlocale(LC_MESSAGES, 0);
1302: setlocale(LC_MESSAGES, 'C');
1303: if (empty($params['no_passphrase'])) {
1304: $result = $this->_callGpg($cmdline, 'w', $params['passphrase'], true, true);
1305: } else {
1306: $result = $this->_callGpg($cmdline, 'r', null, true, true);
1307: }
1308: setlocale(LC_MESSAGES, $language);
1309: if (empty($result->output)) {
1310: $error = preg_replace('/\n.*/', '', $result->stderr);
1311: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not decrypt PGP data: ") . $error);
1312: }
1313:
1314:
1315: return $this->_checkSignatureResult($result->stderr, $result->output);
1316: }
1317:
1318: 1319: 1320: 1321: 1322: 1323: 1324: 1325: 1326: 1327: 1328: 1329: 1330: 1331: 1332: 1333: 1334: 1335: 1336: 1337: 1338:
1339: protected function _decryptSignature($text, $params)
1340: {
1341:
1342: if (!isset($params['pubkey'])) {
1343: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public PGP key is required to verify a signed message."));
1344: }
1345: if (($params['type'] === 'detached-signature') &&
1346: !isset($params['signature'])) {
1347: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("The detached PGP signature block is required to verify the signed message."));
1348: }
1349:
1350:
1351: $input = $this->_createTempFile('horde-pgp');
1352:
1353:
1354: $keyring = $this->_putInKeyring($params['pubkey']);
1355:
1356:
1357: file_put_contents($input, $text);
1358:
1359:
1360: $cmdline = array(
1361: '--armor',
1362: '--always-trust',
1363: '--batch',
1364: '--charset ' . (isset($params['charset']) ? $params['charset'] : 'UTF-8'),
1365: $keyring,
1366: '--verify'
1367: );
1368:
1369:
1370: if ($params['type'] === 'detached-signature') {
1371: $sigfile = $this->_createTempFile('horde-pgp');
1372: $cmdline[] = $sigfile . ' ' . $input;
1373: file_put_contents($sigfile, $params['signature']);
1374: } else {
1375: $cmdline[] = $input;
1376: }
1377:
1378: 1379:
1380: $language = setlocale(LC_MESSAGES, 0);
1381: setlocale(LC_MESSAGES, 'C');
1382: $result = $this->_callGpg($cmdline, 'r', null, true, true);
1383: setlocale(LC_MESSAGES, $language);
1384: return $this->_checkSignatureResult($result->stderr, $result->stderr);
1385: }
1386:
1387: 1388: 1389: 1390: 1391: 1392: 1393: 1394: 1395: 1396: 1397: 1398:
1399: protected function _checkSignatureResult($result, $message = null)
1400: {
1401: 1402: 1403: 1404:
1405: if (strpos($result, 'gpg: BAD signature') !== false) {
1406: throw new Horde_Crypt_Exception($result);
1407: }
1408:
1409: $ob = new stdClass;
1410: $ob->message = $message;
1411: $ob->result = $result;
1412:
1413: return $ob;
1414: }
1415:
1416: 1417: 1418: 1419: 1420: 1421: 1422: 1423: 1424: 1425: 1426:
1427: public function signMIMEPart($mime_part, $params = array())
1428: {
1429: $params = array_merge($params, array('type' => 'signature', 'sigtype' => 'detach'));
1430:
1431: 1432: 1433: 1434:
1435: $msg_sign = $this->encrypt($mime_part->toString(array('headers' => true, 'canonical' => true, 'encode' => Horde_Mime_Part::ENCODE_7BIT)), $params);
1436:
1437:
1438: $pgp_sign = new Horde_Mime_Part();
1439: $pgp_sign->setType('application/pgp-signature');
1440: $pgp_sign->setHeaderCharset('UTF-8');
1441: $pgp_sign->setDisposition('inline');
1442: $pgp_sign->setDescription(Horde_Crypt_Translation::t("PGP Digital Signature"));
1443: $pgp_sign->setContents($msg_sign, array('encoding' => '7bit'));
1444:
1445: 1446: 1447:
1448: $sig_info = $this->pgpPacketSignature($msg_sign, '_SIGNATURE');
1449:
1450:
1451: $part = new Horde_Mime_Part();
1452: $part->setType('multipart/signed');
1453: $part->setContents("This message is in MIME format and has been PGP signed.\n");
1454: $part->addPart($mime_part);
1455: $part->addPart($pgp_sign);
1456: $part->setContentTypeParameter('protocol', 'application/pgp-signature');
1457: $part->setContentTypeParameter('micalg', $sig_info['micalg']);
1458:
1459: return $part;
1460: }
1461:
1462: 1463: 1464: 1465: 1466: 1467: 1468: 1469: 1470: 1471: 1472: 1473:
1474: public function encryptMIMEPart($mime_part, $params = array())
1475: {
1476: $params = array_merge($params, array('type' => 'message'));
1477:
1478: $signenc_body = $mime_part->toString(array('headers' => true, 'canonical' => true));
1479: $message_encrypt = $this->encrypt($signenc_body, $params);
1480:
1481:
1482: $part = new Horde_Mime_Part();
1483: $part->setType('multipart/encrypted');
1484: $part->setHeaderCharset('UTF-8');
1485: $part->setContentTypeParameter('protocol', 'application/pgp-encrypted');
1486: $part->setDescription(Horde_Crypt_Translation::t("PGP Encrypted Data"));
1487: $part->setContents("This message is in MIME format and has been PGP encrypted.\n");
1488:
1489: $part1 = new Horde_Mime_Part();
1490: $part1->setType('application/pgp-encrypted');
1491: $part1->setCharset(null);
1492: $part1->setContents("Version: 1\n", array('encoding' => '7bit'));
1493: $part->addPart($part1);
1494:
1495: $part2 = new Horde_Mime_Part();
1496: $part2->setType('application/octet-stream');
1497: $part2->setCharset(null);
1498: $part2->setContents($message_encrypt, array('encoding' => '7bit'));
1499: $part2->setDisposition('inline');
1500: $part->addPart($part2);
1501:
1502: return $part;
1503: }
1504:
1505: 1506: 1507: 1508: 1509: 1510: 1511: 1512: 1513: 1514: 1515: 1516: 1517:
1518: public function signAndEncryptMIMEPart($mime_part, $sign_params = array(),
1519: $encrypt_params = array())
1520: {
1521: 1522: 1523:
1524: $part = $this->signMIMEPart($mime_part, $sign_params);
1525: $part = $this->encryptMIMEPart($part, $encrypt_params);
1526: $part->setContents("This message is in MIME format and has been PGP signed and encrypted.\n");
1527:
1528: $part->setCharset($this->_params['email_charset']);
1529: $part->setDescription(Horde_String::convertCharset(Horde_Crypt_Translation::t("PGP Signed/Encrypted Data"), 'UTF-8', $this->_params['email_charset']));
1530:
1531: return $part;
1532: }
1533:
1534: 1535: 1536: 1537: 1538: 1539: 1540: 1541:
1542: public function publicKeyMIMEPart($key)
1543: {
1544: $part = new Horde_Mime_Part();
1545: $part->setType('application/pgp-keys');
1546: $part->setHeaderCharset('UTF-8');
1547: $part->setDescription(Horde_Crypt_Translation::t("PGP Public Key"));
1548: $part->setContents($key, array('encoding' => '7bit'));
1549:
1550: return $part;
1551: }
1552:
1553: 1554: 1555: 1556: 1557: 1558: 1559: 1560: 1561: 1562: 1563: 1564: 1565: 1566:
1567: protected function _callGpg($options, $mode, $input = array(),
1568: $output = false, $stderr = false,
1569: $verbose = false)
1570: {
1571: $data = new stdClass;
1572: $data->output = null;
1573: $data->stderr = null;
1574: $data->stdout = null;
1575:
1576:
1577: if (!$verbose) {
1578: array_unshift($options, '--quiet');
1579: }
1580:
1581:
1582: if ($output) {
1583: $output_file = $this->_createTempFile('horde-pgp', false);
1584: array_unshift($options, '--output ' . $output_file);
1585:
1586:
1587: if ($stderr) {
1588: $stderr_file = $this->_createTempFile('horde-pgp', false);
1589: $options[] = '2> ' . $stderr_file;
1590: }
1591: }
1592:
1593:
1594: if (!$output || !$stderr) {
1595: $options[] = '2> /dev/null';
1596: }
1597:
1598:
1599: $cmdline = implode(' ', array_merge($this->_gnupg, $options));
1600:
1601: if ($mode == 'w') {
1602: if ($fp = popen($cmdline, 'w')) {
1603: $win32 = !strncasecmp(PHP_OS, 'WIN', 3);
1604:
1605: if (!is_array($input)) {
1606: $input = array($input);
1607: }
1608:
1609: foreach ($input as $line) {
1610: if ($win32 && (strpos($line, "\x0d\x0a") !== false)) {
1611: $chunks = explode("\x0d\x0a", $line);
1612: foreach ($chunks as $chunk) {
1613: fputs($fp, $chunk . "\n");
1614: }
1615: } else {
1616: fputs($fp, $line . "\n");
1617: }
1618: }
1619: } else {
1620: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to pgp binary."));
1621: }
1622: } elseif ($mode == 'r') {
1623: if ($fp = popen($cmdline, 'r')) {
1624: while (!feof($fp)) {
1625: $data->stdout .= fgets($fp, 1024);
1626: }
1627: } else {
1628: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to pgp binary."));
1629: }
1630: }
1631: pclose($fp);
1632:
1633: if ($output) {
1634: $data->output = file_get_contents($output_file);
1635: unlink($output_file);
1636: if ($stderr) {
1637: $data->stderr = file_get_contents($stderr_file);
1638: unlink($stderr_file);
1639: }
1640: }
1641:
1642: return $data;
1643: }
1644:
1645: 1646: 1647: 1648: 1649: 1650: 1651: 1652: 1653: 1654:
1655: public function generateRevocation($key, $email, $passphrase)
1656: {
1657: $keyring = $this->_putInKeyring($key, 'private');
1658:
1659:
1660: $input = array(
1661: 'y',
1662: '0',
1663: '',
1664: 'y',
1665: );
1666: if (!empty($passphrase)) {
1667: $input[] = $passphrase;
1668: }
1669:
1670:
1671: $cmdline = array(
1672: $keyring,
1673: '--command-fd 0',
1674: '--gen-revoke ' . $email,
1675: );
1676: $results = $this->_callGpg($cmdline, 'w', $input, true);
1677:
1678:
1679: if (empty($results->output)) {
1680: throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Revocation key not generated successfully."));
1681: }
1682:
1683: return $results->output;
1684: }
1685:
1686: }
1687: