* @author Stephen Paul Weber * @link http://github.com/bendiken/openpgp-php */ ////////////////////////////////////////////////////////////////////////////// // OpenPGP utilities /** * @see http://tools.ietf.org/html/rfc4880 */ class OpenPGP { /** * @see http://tools.ietf.org/html/rfc4880#section-6 * @see http://tools.ietf.org/html/rfc4880#section-6.2 * @see http://tools.ietf.org/html/rfc2045 */ static function enarmor($data, $marker = 'MESSAGE', array $headers = array()) { $text = self::header($marker) . "\n"; foreach ($headers as $key => $value) { $text .= $key . ': ' . (string)$value . "\n"; } $text .= "\n" . base64_encode($data); $text .= "\n".'=' . base64_encode(substr(pack('N', self::crc24($data)), 1)) . "\n"; $text .= self::footer($marker) . "\n"; return $text; } /** * @see http://tools.ietf.org/html/rfc4880#section-6 * @see http://tools.ietf.org/html/rfc2045 */ static function unarmor($text, $header = 'PGP PUBLIC KEY BLOCK') { $header = self::header($header); $text = str_replace(array("\r\n", "\r"), array("\n", ''), $text); if (($pos1 = strpos($text, $header)) !== FALSE && ($pos1 = strpos($text, "\n\n", $pos1 += strlen($header))) !== FALSE && ($pos2 = strpos($text, "\n=", $pos1 += 2)) !== FALSE) { return base64_decode($text = substr($text, $pos1, $pos2 - $pos1)); } } /** * @see http://tools.ietf.org/html/rfc4880#section-6.2 */ static function header($marker) { return '-----BEGIN ' . strtoupper((string)$marker) . '-----'; } /** * @see http://tools.ietf.org/html/rfc4880#section-6.2 */ static function footer($marker) { return '-----END ' . strtoupper((string)$marker) . '-----'; } /** * @see http://tools.ietf.org/html/rfc4880#section-6 * @see http://tools.ietf.org/html/rfc4880#section-6.1 */ static function crc24($data) { $crc = 0x00b704ce; for ($i = 0; $i < strlen($data); $i++) { $crc ^= (ord($data[$i]) & 255) << 16; for ($j = 0; $j < 8; $j++) { $crc <<= 1; if ($crc & 0x01000000) { $crc ^= 0x01864cfb; } } } return $crc & 0x00ffffff; } /** * @see http://tools.ietf.org/html/rfc4880#section-12.2 */ static function bitlength($data) { return (strlen($data) - 1) * 8 + (int)floor(log(ord($data[0]), 2)) + 1; } static function decode_s2k_count($c) { return ((int)16 + ($c & 15)) << (($c >> 4) + 6); } static function encode_s2k_count($iterations) { if($iterations >= 65011712) return 255; $count = $iterations >> 6; $c = 0; while($count >= 32) { $count = $count >> 1; $c++; } $result = ($c << 4) | ($count - 16); if(OpenPGP::decode_s2k_count($result) < $iterations) { return $result + 1; } return $result; } } ////////////////////////////////////////////////////////////////////////////// // OpenPGP messages /** * @see http://tools.ietf.org/html/rfc4880#section-4.1 * @see http://tools.ietf.org/html/rfc4880#section-11 * @see http://tools.ietf.org/html/rfc4880#section-11.3 */ class OpenPGP_Message implements IteratorAggregate, ArrayAccess { public $uri = NULL; public $packets = array(); static function parse_file($path) { if (($msg = self::parse(file_get_contents($path)))) { $msg->uri = preg_match('!^[\w\d]+://!', $path) ? $path : 'file://' . realpath($path); return $msg; } } /** * @see http://tools.ietf.org/html/rfc4880#section-4.1 * @see http://tools.ietf.org/html/rfc4880#section-4.2 */ static function parse($input) { if (is_resource($input)) { return self::parse_stream($input); } if (is_string($input)) { return self::parse_string($input); } } static function parse_stream($input) { return self::parse_string(stream_get_contents($input)); } static function parse_string($input) { $msg = new self; while (($length = strlen($input)) > 0) { if (($packet = OpenPGP_Packet::parse($input))) { $msg[] = $packet; } if ($length == strlen($input)) { // is parsing stuck? break; } } return $msg; } function __construct(array $packets = array()) { $this->packets = $packets; } function to_bytes() { $bytes = ''; foreach($this as $p) { $bytes .= $p->to_bytes(); } return $bytes; } function signature_and_data($index=0) { $msg = $this; while($msg[0] instanceof OpenPGP_CompressedDataPacket) $msg = $msg[0]; $i = 0; foreach($msg as $p) { if($p instanceof OpenPGP_SignaturePacket) { if($i == $index) $signature_packet = $p; $i++; } if($p instanceof OpenPGP_LiteralDataPacket) $data_packet = $p; if($signature_packet && $data_packet) break; } return array($signature_packet, $data_packet); } /** * Function to verify signature number $index * $verifiers is an array of callbacks formatted like array('RSA' => array('SHA256' => CALLBACK)) that take two parameters: message and signature */ function verify($verifiers, $index=0) { list($signature_packet, $data_packet) = $this->signature_and_data($index); if(!$signature_packet || !$data_packet) return NULL; // No signature or no data $verifier = $verifiers[$signature_packet->key_algorithm_name()][$signature_packet->hash_algorithm_name()]; if(!$verifier) return NULL; // No verifier $data_packet->normalize(); return call_user_func($verifier, $data_packet->data.$signature_packet->trailer, $signature_packet->data); } // IteratorAggregate interface function getIterator() { return new ArrayIterator($this->packets); } // ArrayAccess interface function offsetExists($offset) { return isset($this->packets[$offset]); } function offsetGet($offset) { return $this->packets[$offset]; } function offsetSet($offset, $value) { return is_null($offset) ? $this->packets[] = $value : $this->packets[$offset] = $value; } function offsetUnset($offset) { unset($this->packets[$offset]); } } ////////////////////////////////////////////////////////////////////////////// // OpenPGP packets /** * OpenPGP packet. * * @see http://tools.ietf.org/html/rfc4880#section-4.1 * @see http://tools.ietf.org/html/rfc4880#section-4.3 */ class OpenPGP_Packet { public $tag, $size, $data; static function class_for($tag) { return isset(self::$tags[$tag]) && class_exists( $class = 'OpenPGP_' . self::$tags[$tag] . 'Packet') ? $class : __CLASS__; } /** * Parses an OpenPGP packet. * * @see http://tools.ietf.org/html/rfc4880#section-4.2 */ static function parse(&$input) { $packet = NULL; if (strlen($input) > 0) { $parser = ord($input[0]) & 64 ? 'parse_new_format' : 'parse_old_format'; list($tag, $head_length, $data_length) = self::$parser($input); $input = substr($input, $head_length); if ($tag && ($class = self::class_for($tag))) { $packet = new $class(); $packet->tag = $tag; $packet->input = substr($input, 0, $data_length); $packet->length = $data_length; $packet->read(); unset($packet->input); unset($packet->length); } $input = substr($input, $data_length); } return $packet; } /** * Parses a new-format (RFC 4880) OpenPGP packet. * * @see http://tools.ietf.org/html/rfc4880#section-4.2.2 */ static function parse_new_format($input) { $tag = ord($input[0]) & 63; $len = ord($input[1]); if($len < 192) { // One octet length return array($tag, 2, $len); } if($len > 191 && $len < 224) { // Two octet length return array($tag, 3, (($len - 192) << 8) + ord($input[2]) + 192); } if($len == 255) { // Five octet length $unpacked = unpack('N', substr($input, 2, 4)); return array($tag, 6, array_pop($unpacked)); } // TODO: Partial body lengths. 1 << ($len & 0x1F) } /** * Parses an old-format (PGP 2.6.x) OpenPGP packet. * * @see http://tools.ietf.org/html/rfc4880#section-4.2.1 */ static function parse_old_format($input) { $len = ($tag = ord($input[0])) & 3; $tag = ($tag >> 2) & 15; switch ($len) { case 0: // The packet has a one-octet length. The header is 2 octets long. $head_length = 2; $data_length = ord($input[1]); break; case 1: // The packet has a two-octet length. The header is 3 octets long. $head_length = 3; $data_length = unpack('n', substr($input, 1, 2)); $data_length = $data_length[1]; break; case 2: // The packet has a four-octet length. The header is 5 octets long. $head_length = 5; $data_length = unpack('N', substr($input, 1, 4)); $data_length = $data_length[1]; break; case 3: // The packet is of indeterminate length. The header is 1 octet long. $head_length = 1; $data_length = strlen($input) - $head_length; break; } return array($tag, $head_length, $data_length); } function __construct($data=NULL) { $this->tag = array_search(substr(substr(get_class($this), 8), 0, -6), self::$tags); $this->data = $data; } function read() { } function body() { return $this->data; // Will normally be overridden by subclasses } function header_and_body() { $body = $this->body(); // Get body first, we will need it's length $tag = chr($this->tag | 0xC0); // First two bits are 1 for new packet format $size = chr(255).pack('N', strlen($body)); // Use 5-octet lengths return array('header' => $tag.$size, 'body' => $body); } function to_bytes() { $data = $this->header_and_body(); return $data['header'].$data['body']; } /** * @see http://tools.ietf.org/html/rfc4880#section-3.5 */ function read_timestamp() { return $this->read_unpacked(4, 'N'); } /** * @see http://tools.ietf.org/html/rfc4880#section-3.2 */ function read_mpi() { $length = $this->read_unpacked(2, 'n'); // length in bits $length = (int)floor(($length + 7) / 8); // length in bytes return $this->read_bytes($length); } /** * @see http://php.net/manual/en/function.unpack.php */ function read_unpacked($count, $format) { $unpacked = unpack($format, $this->read_bytes($count)); return array_pop($unpacked); } function read_byte() { return ($bytes = $this->read_bytes()) ? $bytes[0] : NULL; } function read_bytes($count = 1) { $bytes = substr($this->input, 0, $count); $this->input = substr($this->input, $count); return $bytes; } static $tags = array( 1 => 'AsymmetricSessionKey', // Public-Key Encrypted Session Key 2 => 'Signature', // Signature Packet 3 => 'SymmetricSessionKey', // Symmetric-Key Encrypted Session Key Packet 4 => 'OnePassSignature', // One-Pass Signature Packet 5 => 'SecretKey', // Secret-Key Packet 6 => 'PublicKey', // Public-Key Packet 7 => 'SecretSubkey', // Secret-Subkey Packet 8 => 'CompressedData', // Compressed Data Packet 9 => 'EncryptedData', // Symmetrically Encrypted Data Packet 10 => 'Marker', // Marker Packet 11 => 'LiteralData', // Literal Data Packet 12 => 'Trust', // Trust Packet 13 => 'UserID', // User ID Packet 14 => 'PublicSubkey', // Public-Subkey Packet 17 => 'UserAttribute', // User Attribute Packet 18 => 'IntegrityProtectedData', // Sym. Encrypted and Integrity Protected Data Packet 19 => 'ModificationDetectionCode', // Modification Detection Code Packet 60 => 'Experimental', // Private or Experimental Values 61 => 'Experimental', // Private or Experimental Values 62 => 'Experimental', // Private or Experimental Values 63 => 'Experimental', // Private or Experimental Values ); } /** * OpenPGP Public-Key Encrypted Session Key packet (tag 1). * * @see http://tools.ietf.org/html/rfc4880#section-5.1 */ class OpenPGP_AsymmetricSessionKeyPacket extends OpenPGP_Packet { // TODO } /** * OpenPGP Signature packet (tag 2). * Be sure to NULL the trailer if you update a signature packet! * * @see http://tools.ietf.org/html/rfc4880#section-5.2 */ class OpenPGP_SignaturePacket extends OpenPGP_Packet { public $version, $signature_type, $hash_algorithm, $key_algorithm, $hashed_subpackets, $unhashed_subpackets, $hash_head; public $trailer; // This is the literal bytes that get tacked on the end of the message when verifying the signature function __construct($data=NULL, $key_algorithm=NULL, $hash_algorithm=NULL) { parent::__construct(); $this->version = 4; // Default to version 4 sigs if(is_string($this->hash_algorithm = $hash_algorithm)) { $this->hash_algorithm = array_search($this->hash_algorithm, self::$hash_algorithms); } if(is_string($this->key_algorithm = $key_algorithm)) { $this->key_algorithm = array_search($this->key_algorithm, OpenPGP_PublicKeyPacket::$algorithms); } if($data) { // If we have any data, set up the creation time $this->hashed_subpackets = array(new OpenPGP_SignaturePacket_SignatureCreationTimePacket(time())); } if($data instanceof OpenPGP_LiteralDataPacket) { $this->signature_type = ($data->format == 'b') ? 0x00 : 0x01; $data->normalize(); $data = $data->data; } else if($data instanceof OpenPGP_Message && $data[0] instanceof OpenPGP_PublicKeyPacket) { // $data is a message with PublicKey first, UserID second $key = implode('', $data[0]->fingerprint_material()); $user_id = $data[1]->body(); $data = $key . chr(0xB4) . pack('N', strlen($user_id)) . $user_id; } $this->data = $data; // Store to-be-signed data in here until the signing happens } /** * $this->data must be set to the data to sign (done by constructor) * $signers in the same format as $verifiers for OpenPGP_Message. */ function sign_data($signers) { $this->trailer = $this->body(true); $signer = $signers[$this->key_algorithm_name()][$this->hash_algorithm_name()]; $this->data = call_user_func($signer, $this->data.$this->trailer); $unpacked = unpack('n', substr($this->data, 0, 2)); $this->hash_head = array_pop($unpacked); } function read() { switch($this->version = ord($this->read_byte())) { case 2: case 3: assert(ord($this->read_byte()) == 5); $this->signature_type = ord($this->read_byte()); $creation_time = $this->read_timestamp(); $keyid = $this->read_bytes(8); $keyidHex = ''; for($i = 0; $i < strlen($keyid); $i++) { // Store KeyID in Hex $keyidHex .= sprintf('%02X',ord($keyid{$i})); } $this->hashed_subpackets = array(); $this->unhashed_subpackets = array( new OpenPGP_SignaturePacket_SignatureCreationTimePacket($creation_time), new OpenPGP_SignaturePacket_IssuerPacket($keyidHex) ); $this->key_algorithm = ord($this->read_byte()); $this->hash_algorithm = ord($this->read_byte()); $this->hash_head = $this->read_unpacked(2, 'n'); $this->data = array(); while(strlen($this->input) > 0) { $this->data[] = $this->read_mpi(); } break; case 4: $this->signature_type = ord($this->read_byte()); $this->key_algorithm = ord($this->read_byte()); $this->hash_algorithm = ord($this->read_byte()); $this->trailer = chr(4).chr($this->signature_type).chr($this->key_algorithm).chr($this->hash_algorithm); $hashed_size = $this->read_unpacked(2, 'n'); $hashed_subpackets = $this->read_bytes($hashed_size); $this->trailer .= pack('n', $hashed_size).$hashed_subpackets; $this->hashed_subpackets = self::get_subpackets($hashed_subpackets); $this->trailer .= chr(4).chr(0xff).pack('N', 6 + $hashed_size); $unhashed_size = $this->read_unpacked(2, 'n'); $this->unhashed_subpackets = self::get_subpackets($this->read_bytes($unhashed_size)); $this->hash_head = $this->read_unpacked(2, 'n'); $this->data = array(); while(strlen($this->input) > 0) { $this->data[] = $this->read_mpi(); } break; } } function calculate_trailer() { // The trailer is just the top of the body plus some crap $body = $this->body_start(); return $body.chr(4).chr(0xff).pack('N', strlen($body)); } function body_start() { $body = chr(4).chr($this->signature_type).chr($this->key_algorithm).chr($this->hash_algorithm); $hashed_subpackets = ''; foreach((array)$this->hashed_subpackets as $p) { $hashed_subpackets .= $p->to_bytes(); } $body .= pack('n', strlen($hashed_subpackets)).$hashed_subpackets; } function body() { switch($this->version) { case 2: case 3: $body = chr($this->version) . chr(5) . chr($this->signature_type); foreach((array)$this->unhashed_subpackets as $p) { if($p instanceof OpenPGP_SignaturePacket_SignatureCreationTimePacket) { $body .= pack('N', $p->data); break; } } foreach((array)$this->unhashed_subpackets as $p) { if($p instanceof OpenPGP_SignaturePacket_IssuerPacket) { for($i = 0; $i < strlen($p->data); $i += 2) { $body .= chr(hexdec($p->data{$i}.$p->data{$i+1})); } break; } } $body .= chr($this->key_algorithm); $body .= chr($this->hash_algorithm); $body .= pack('n', $this->hash_head); foreach($this->data as $mpi) { $body .= pack('n', OpenPGP::bitlength($mpi)).$mpi; } return $body; case 4: if(!$this->trailer) $this->trailer = $this->calculate_trailer(); $body = substr($this->trailer, 0, -6); $unhashed_subpackets = ''; foreach((array)$this->unhashed_subpackets as $p) { $unhashed_subpackets .= $p->to_bytes(); } $body .= pack('n', strlen($unhashed_subpackets)).$unhashed_subpackets; $body .= pack('n', $this->hash_head); foreach($this->data as $mpi) { $body .= pack('n', OpenPGP::bitlength($mpi)).$mpi; } return $body; } } function key_algorithm_name() { return OpenPGP_PublicKeyPacket::$algorithms[$this->key_algorithm]; } function hash_algorithm_name() { return self::$hash_algorithms[$this->hash_algorithm]; } function issuer() { foreach($this->hashed_subpackets as $p) { if($p instanceof OpenPGP_SignaturePacket_IssuerPacket) return $p->data; } foreach($this->unhashed_subpackets as $p) { if($p instanceof OpenPGP_SignaturePacket_IssuerPacket) return $p->data; } return NULL; } /** * @see http://tools.ietf.org/html/rfc4880#section-5.2.3.1 */ static function get_subpackets($input) { $subpackets = array(); while(($length = strlen($input)) > 0) { $subpackets[] = self::get_subpacket($input); if($length == strlen($input)) { // Parsing stuck? break; } } return $subpackets; } static function get_subpacket(&$input) { $len = ord($input[0]); $length_of_length = 1; // if($len < 192) One octet length, no furthur processing if($len > 190 && $len < 255) { // Two octet length $length_of_length = 2; $len = (($len - 192) << 8) + ord($input[1]) + 192; } if($len == 255) { // Five octet length $length_of_length = 5; $unpacked = unpack('N', substr($input, 1, 4)); $len = array_pop($unpacked); } $input = substr($input, $length_of_length); // Chop off length header $tag = ord($input[0]); $class = self::class_for($tag); if($class) { $packet = new $class(); $packet->tag = $tag; $packet->input = substr($input, 1, $len-1); $packet->length = $len-1; $packet->read(); unset($packet->input); unset($packet->length); } $input = substr($input, $len); // Chop off the data from this packet return $packet; } static $hash_algorithms = array( 1 => 'MD5', 2 => 'SHA1', 3 => 'RIPEMD160', 8 => 'SHA256', 9 => 'SHA384', 10 => 'SHA512', 11 => 'SHA224' ); static $subpacket_types = array( //0 => 'Reserved', //1 => 'Reserved', 2 => 'SignatureCreationTime', 3 => 'SignatureExpirationTime', 4 => 'ExportableCertification', 5 => 'TrustSignature', 6 => 'RegularExpression', 7 => 'Revocable', //8 => 'Reserved', 9 => 'KeyExpirationTime', //10 => 'Placeholder for backward compatibility', 11 => 'PreferredSymmetricAlgorithms', 12 => 'RevocationKey', //13 => 'Reserved', //14 => 'Reserved', //15 => 'Reserved', 16 => 'Issuer', //17 => 'Reserved', //18 => 'Reserved', //19 => 'Reserved', 20 => 'NotationData', 21 => 'PreferredHashAlgorithms', 22 => 'PreferredCompressionAlgorithms', 23 => 'KeyServerPreferences', 24 => 'PreferredKeyServer', 25 => 'PrimaryUserID', 26 => 'PolicyURI', 27 => 'KeyFlags', 28 => 'SignersUserID', 29 => 'ReasonforRevocation', 30 => 'Features', 31 => 'SignatureTarget', 32 => 'EmbeddedSignature', ); static function class_for($tag) { if(!isset(self::$subpacket_types[$tag])) return 'OpenPGP_SignaturePacket_Subpacket'; return 'OpenPGP_SignaturePacket_'.self::$subpacket_types[$tag].'Packet'; } } class OpenPGP_SignaturePacket_Subpacket extends OpenPGP_Packet { function __construct($data=NULL) { parent::__construct($data); $this->tag = array_search(substr(substr(get_class($this), 8+16), 0, -6), OpenPGP_SignaturePacket::$subpacket_types); } function header_and_body() { $body = $this->body(); // Get body first, we will need it's length $size = chr(255).pack('N', strlen($body)+1); // Use 5-octet lengths + 1 for tag as first packet body octet $tag = chr($this->tag); return array('header' => $size.$tag, 'body' => $body); } /* Defaults for unsupported packets */ function read() { $this->data = $this->input; } function body() { return $this->data; } } /** * @see http://tools.ietf.org/html/rfc4880#section-5.2.3.4 */ class OpenPGP_SignaturePacket_SignatureCreationTimePacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = $this->read_timestamp(); } function body() { return pack('N', $this->data); } } class OpenPGP_SignaturePacket_SignatureExpirationTimePacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = $this->read_timestamp(); } function body() { return pack('N', $this->data); } } class OpenPGP_SignaturePacket_ExportableCertificationPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = (ord($this->input) == 0); } function body() { return chr($this->data ? 1 : 0); } } class OpenPGP_SignaturePacket_TrustSignaturePacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->depth = ord($this->input{0}); $this->trust = ord($this->input{1}); } function body() { return chr($this->depth) . chr($this->trust); } } class OpenPGP_SignaturePacket_RegularExpressionPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = substr($this->input, 0, -1); } function body() { return $this->data . chr(0); } } class OpenPGP_SignaturePacket_RevocablePacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = (ord($this->input) == 0); } function body() { return chr($this->data ? 1 : 0); } } class OpenPGP_SignaturePacket_KeyExpirationTimePacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = $this->read_timestamp(); } function body() { return pack('N', $this->data); } } class OpenPGP_SignaturePacket_PreferredSymmetricAlgorithmsPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = array(); while(strlen($this->input) > 0) { $this->data[] = ord($this->read_byte()); } } function body() { $bytes = ''; foreach($this->data as $algo) { $bytes .= chr($algo); } return $bytes; } } class OpenPGP_SignaturePacket_RevocationKeyPacket extends OpenPGP_SignaturePacket_Subpacket { public $key_algorithm, $fingerprint, $sensitive; function read() { // bitfield must have bit 0x80 set, says the spec $bitfield = ord($this->read_byte()); $this->sensitive = $bitfield & 0x40 == 0x40; $this->key_algorithm = ord($this->read_byte()); $this->fingerprint = ''; while(strlen($this->input) > 0) { $this->fingerprint .= sprintf('%02X',ord($this->read_byte())); } } function body() { $bytes = ''; $bytes .= chr(0x80 | ($this->sensitive ? 0x40 : 0x00)); $bytes .= chr($this->key_algorithm); for($i = 0; $i < strlen($this->fingerprint); $i += 2) { $bytes .= chr(hexdec($this->fingerprint{$i}.$this->fingerprint{$i+1})); } return $bytes; } } /** * @see http://tools.ietf.org/html/rfc4880#section-5.2.3.5 */ class OpenPGP_SignaturePacket_IssuerPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { for($i = 0; $i < 8; $i++) { // Store KeyID in Hex $this->data .= sprintf('%02X',ord($this->read_byte())); } } function body() { $bytes = ''; for($i = 0; $i < strlen($this->data); $i += 2) { $bytes .= chr(hexdec($this->data{$i}.$this->data{$i+1})); } return $bytes; } } class OpenPGP_SignaturePacket_NotationDataPacket extends OpenPGP_SignaturePacket_Subpacket { public $human_readable, $name; function read() { $flags = $this->read_bytes(4); $namelen = $this->read_unpacked(2, 'n'); $datalen = $this->read_unpacked(2, 'n'); $this->human_readable = $flags[0] & 0x80 == 0x80; $this->name = $this->read_bytes($namelen); $this->data = $this->read_bytes($datalen); } function body () { return chr($this->human_readable ? 0x80 : 0x00) . "\0\0\0" . pack('n', strlen($this->name)) . pack('n', strlen($this->data)) . $this->name . $this->data; } } class OpenPGP_SignaturePacket_PreferredHashAlgorithmsPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = array(); while(strlen($this->input) > 0) { $this->data[] = ord($this->read_byte()); } } function body() { $bytes = ''; foreach($this->data as $algo) { $bytes .= chr($algo); } return $bytes; } } class OpenPGP_SignaturePacket_PreferredCompressionAlgorithmsPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = array(); while(strlen($this->input) > 0) { $this->data[] = ord($this->read_byte()); } } function body() { $bytes = ''; foreach($this->data as $algo) { $bytes .= chr($algo); } return $bytes; } } class OpenPGP_SignaturePacket_KeyServerPreferencesPacket extends OpenPGP_SignaturePacket_Subpacket { public $no_modify; function read() { $flags = ord($this->input); $this->no_modify = $flags & 0x80 == 0x80; } function body() { return chr($this->no_modify ? 0x80 : 0x00); } } class OpenPGP_SignaturePacket_PreferredKeyServerPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = $this->input; } function body() { return $this->data; } } class OpenPGP_SignaturePacket_PrimaryUserIDPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = (ord($this->input) == 0); } function body() { return chr($this->data ? 1 : 0); } } class OpenPGP_SignaturePacket_PolicyURIPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = $this->input; } function body() { return $this->data; } } class OpenPGP_SignaturePacket_KeyFlagsPacket extends OpenPGP_SignaturePacket_Subpacket { function __construct($flags=array()) { parent::__construct(); $this->flags = $flags; } function read() { $this->flags = array(); while($this->input) { $this->flags[] = ord($this->read_byte()); } } function body() { $bytes = ''; foreach($this->flags as $f) { $bytes .= chr($f); } return $bytes; } } class OpenPGP_SignaturePacket_SignersUserIDPacket extends OpenPGP_SignaturePacket_Subpacket { function read() { $this->data = $this->input; } function body() { return $this->data; } } class OpenPGP_SignaturePacket_ReasonforRevocationPacket extends OpenPGP_SignaturePacket_Subpacket { public $code; function read() { $this->code = ord($this->read_byte()); $this->data = $this->input; } function body() { return chr($this->code) . $this->data; } } class OpenPGP_SignaturePacket_FeaturesPacket extends OpenPGP_SignaturePacket_KeyFlagsPacket { // Identical functionality to parent } class OpenPGP_SignaturePacket_SignatureTargetPacket extends OpenPGP_SignaturePacket_Subpacket { public $key_algorithm, $hash_algorithm; function read() { $this->key_algorithm = ord($this->read_byte()); $this->hash_algorithm = ord($this->read_byte()); $this->data = $this->input; } function body() { return chr($this->key_algorithm) . chr($this->hash_algorithm) . $this->data; } } class OpenPGP_SignaturePacket_EmbeddedSignaturePacket extends OpenPGP_SignaturePacket { // TODO: This is duplicated from subpacket... improve? function __construct($data=NULL) { parent::__construct($data); $this->tag = array_search(substr(substr(get_class($this), 8+16), 0, -6), OpenPGP_SignaturePacket::$subpacket_types); } function header_and_body() { $body = $this->body(); // Get body first, we will need it's length $size = chr(255).pack('N', strlen($body)+1); // Use 5-octet lengths + 1 for tag as first packet body octet $tag = chr($this->tag); return array('header' => $size.$tag, 'body' => $body); } } /** * OpenPGP Symmetric-Key Encrypted Session Key packet (tag 3). * * @see http://tools.ietf.org/html/rfc4880#section-5.3 */ class OpenPGP_SymmetricSessionKeyPacket extends OpenPGP_Packet { // TODO } /** * OpenPGP One-Pass Signature packet (tag 4). * * @see http://tools.ietf.org/html/rfc4880#section-5.4 */ class OpenPGP_OnePassSignaturePacket extends OpenPGP_Packet { public $version, $signature_type, $hash_algorithm, $key_algorithm, $key_id, $nested; function read() { $this->version = ord($this->read_byte()); $this->signature_type = ord($this->read_byte()); $this->hash_algorithm = ord($this->read_byte()); $this->key_algorithm = ord($this->read_byte()); for($i = 0; $i < 8; $i++) { // Store KeyID in Hex $this->key_id .= sprintf('%02X',ord($this->read_byte())); } $this->nested = ord($this->read_byte()); } function body() { $body = chr($this->version).chr($this->signature_type).chr($this->hash_algorithm).chr($this->key_algorithm); for($i = 0; $i < strlen($this->key_id); $i += 2) { $body .= chr(hexdec($this->key_id{$i}.$this->key_id{$i+1})); } $body .= chr((int)$this->nested); return $body; } } /** * OpenPGP Public-Key packet (tag 6). * * @see http://tools.ietf.org/html/rfc4880#section-5.5.1.1 * @see http://tools.ietf.org/html/rfc4880#section-5.5.2 * @see http://tools.ietf.org/html/rfc4880#section-11.1 * @see http://tools.ietf.org/html/rfc4880#section-12 */ class OpenPGP_PublicKeyPacket extends OpenPGP_Packet { public $version, $timestamp, $algorithm; public $key, $key_id, $fingerprint; public $v3_days_of_validity; function __construct($key=array(), $algorithm='RSA', $timestamp=NULL, $version=4) { parent::__construct(); $this->key = $key; if(is_string($this->algorithm = $algorithm)) { $this->algorithm = array_search($this->algorithm, self::$algorithms); } $this->timestamp = $timestamp ? $timestamp : time(); $this->version = $version; if(count($this->key) > 0) { $this->key_id = substr($this->fingerprint(), -8); } } // Find self signatures in a message, these often contain metadata about the key function self_signatures($message) { $sigs = array(); $keyid16 = strtoupper(substr($this->fingerprint, -16)); foreach($message as $p) { if($p instanceof OpenPGP_SignaturePacket) { if(strtoupper($p->issuer()) == $keyid16) { $sigs[] = $p; } else { foreach(array_merge($p->hashed_subpackets, $p->unhashed_subpackets) as $s) { if($s instanceof OpenPGP_SignaturePacket_EmbeddedSignaturePacket && strtoupper($s->issuer()) == $keyid16) { $sigs[] = $p; break; } } } } else if(count($sigs)) break; // After we've seen a self sig, the next non-sig stop all self-sigs } return $sigs; } // Find expiry time of this key based on the self signatures in a message function expires($message) { foreach($this->self_signatures($message) as $p) { foreach(array_merge($p->hashed_subpackets, $p->unhashed_subpackets) as $s) { if($s instanceof OpenPGP_SignaturePacket_KeyExpirationTimePacket) { return $this->timestamp + $s->data; } } } return NULL; // Never expires } /** * @see http://tools.ietf.org/html/rfc4880#section-5.5.2 */ function read() { switch ($this->version = ord($this->read_byte())) { case 3: $this->timestamp = $this->read_timestamp(); $this->v3_days_of_validity = $this->read_unpacked(2, 'n'); $this->algorithm = ord($this->read_byte()); $this->read_key_material(); break; case 4: $this->timestamp = $this->read_timestamp(); $this->algorithm = ord($this->read_byte()); $this->read_key_material(); } } /** * @see http://tools.ietf.org/html/rfc4880#section-5.5.2 */ function read_key_material() { foreach (self::$key_fields[$this->algorithm] as $field) { $this->key[$field] = $this->read_mpi(); } $this->key_id = substr($this->fingerprint(), -8); } function fingerprint_material() { switch ($this->version) { case 3: $material = array(); foreach (self::$key_fields[$this->algorithm] as $i) { $material[] = pack('n', OpenPGP::bitlength($this->key[$i])); $material[] = $this->key[$i]; } return $material; case 4: $head = array( chr(0x99), NULL, chr($this->version), pack('N', $this->timestamp), chr($this->algorithm), ); $material = array(); foreach (self::$key_fields[$this->algorithm] as $i) { $material[] = pack('n', OpenPGP::bitlength($this->key[$i])); $material[] = $this->key[$i]; } $material = implode('', $material); $head[1] = pack('n', 6 + strlen($material)); $head[] = $material; return $head; } } /** * @see http://tools.ietf.org/html/rfc4880#section-12.2 * @see http://tools.ietf.org/html/rfc4880#section-3.3 */ function fingerprint() { switch ($this->version) { case 2: case 3: return $this->fingerprint = md5(implode('', $this->fingerprint_material())); case 4: return $this->fingerprint = sha1(implode('', $this->fingerprint_material())); } } function body() { switch ($this->version) { case 2: case 3: return implode('', array_merge(array( chr($this->version) . pack('N', $this->timestamp) . pack('n', $this->v3_days_of_validity) . chr($this->algorithm) ), $this->fingerprint_material()) ); case 4: return implode('', array_slice($this->fingerprint_material(), 2)); } } static $key_fields = array( 1 => array('n', 'e'), // RSA 16 => array('p', 'g', 'y'), // ELG-E 17 => array('p', 'q', 'g', 'y'), // DSA ); static $algorithms = array( 1 => 'RSA', 2 => 'RSA', 3 => 'RSA', 16 => 'ELGAMAL', 17 => 'DSA', 18 => 'ECC', 19 => 'ECDSA', 21 => 'DH' ); } /** * OpenPGP Public-Subkey packet (tag 14). * * @see http://tools.ietf.org/html/rfc4880#section-5.5.1.2 * @see http://tools.ietf.org/html/rfc4880#section-5.5.2 * @see http://tools.ietf.org/html/rfc4880#section-11.1 * @see http://tools.ietf.org/html/rfc4880#section-12 */ class OpenPGP_PublicSubkeyPacket extends OpenPGP_PublicKeyPacket { // TODO } /** * OpenPGP Secret-Key packet (tag 5). * * @see http://tools.ietf.org/html/rfc4880#section-5.5.1.3 * @see http://tools.ietf.org/html/rfc4880#section-5.5.3 * @see http://tools.ietf.org/html/rfc4880#section-11.2 * @see http://tools.ietf.org/html/rfc4880#section-12 */ class OpenPGP_SecretKeyPacket extends OpenPGP_PublicKeyPacket { public $s2k_useage, $s2k_type, $s2k_hash_algorithm, $s2k_salt, $s2k_count, $symmetric_type, $private_hash, $encrypted_data; function read() { parent::read(); // All the fields from PublicKey $this->s2k_useage = ord($this->read_byte()); if($this->s2k_useage == 255 || $this->s2k_useage == 254) { $this->symmetric_type = ord($this->read_byte()); $this->s2k_type = ord($this->read_byte()); $this->s2k_hash_algorithm = ord($this->read_byte()); if($this->s2k_type == 1 || $this->s2k_type == 3) $this->s2k_salt = $this->read_bytes(8); if($this->s2k_type == 3) { $this->s2k_count = OpenPGP::decode_s2k_count(ord($this->read_byte())); } } else if($this->s2k_useage > 0) { $this->symmetric_type = $this->s2k_useage; } if($this->s2k_useage > 0) { // TODO: IV of the same length as cipher's block size $this->encrypted_data = $this->input; // Rest of input is MPIs and checksum (encrypted) } else { $this->data = $this->input; // Rest of input is MPIs and checksum $this->key_from_data(); } } static $secret_key_fields = array( 1 => array('d', 'p', 'q', 'u'), // RSA 2 => array('d', 'p', 'q', 'u'), // RSA-E 3 => array('d', 'p', 'q', 'u'), // RSA-S 16 => array('x'), // ELG-E 17 => array('x'), // DSA ); function key_from_data() { if(!$this->data) return NULL; // Not decrypted yet $this->input = $this->data; foreach(self::$secret_key_fields[$this->algorithm] as $field) { $this->key[$field] = $this->read_mpi(); } // TODO: Validate checksum? if($this->s2k_useage == 254) { // 20 octet sha1 hash $this->private_hash = $this->read_bytes(20); } else { // two-octet checksum $this->private_hash = $this->read_bytes(2); } unset($this->input); } function body() { $bytes = parent::body() . chr($this->s2k_useage); $secret_material = NULL; if($this->s2k_useage == 255 || $this->s2k_useage == 254) { $bytes .= chr($this->symmetric_type); $bytes .= chr($this->s2k_type); $bytes .= chr($this->s2k_hash_algorithm); if($this->s2k_type == 1 || $this->s2k_type == 3) { $bytes .= $this->s2k_salt; } if($this->s2k_type == 3) { $bytes .= chr(OpenPGP::encode_s2k_count($this->s2k_count)); } } if($this->s2k_useage > 0) { $bytes .= $this->encrypted_data; } else { $secret_material = ''; foreach(self::$secret_key_fields[$this->algorithm] as $f) { $f = $this->key[$f]; $secret_material .= pack('n', OpenPGP::bitlength($f)); $secret_material .= $f; } $bytes .= $secret_material; // 2-octet checksum $chk = 0; for($i = 0; $i < strlen($secret_material); $i++) { $chk = ($chk + ord($secret_material[$i])) % 65536; } $bytes .= pack('n', $chk); } return $bytes; } } /** * OpenPGP Secret-Subkey packet (tag 7). * * @see http://tools.ietf.org/html/rfc4880#section-5.5.1.4 * @see http://tools.ietf.org/html/rfc4880#section-5.5.3 * @see http://tools.ietf.org/html/rfc4880#section-11.2 * @see http://tools.ietf.org/html/rfc4880#section-12 */ class OpenPGP_SecretSubkeyPacket extends OpenPGP_SecretKeyPacket { // TODO } /** * OpenPGP Compressed Data packet (tag 8). * * @see http://tools.ietf.org/html/rfc4880#section-5.6 */ class OpenPGP_CompressedDataPacket extends OpenPGP_Packet implements IteratorAggregate, ArrayAccess { public $algorithm; /* see http://tools.ietf.org/html/rfc4880#section-9.3 */ static $algorithms = array(0 => 'Uncompressed', 1 => 'ZIP', 2 => 'ZLIB', 3 => 'BZip2'); function read() { $this->algorithm = ord($this->read_byte()); $this->data = $this->read_bytes($this->length); switch($this->algorithm) { case 0: $this->data = OpenPGP_Message::parse($this->data); break; case 1: $this->data = OpenPGP_Message::parse(gzinflate($this->data)); break; case 2: $this->data = OpenPGP_Message::parse(gzuncompress($this->data)); break; case 3: $this->data = OpenPGP_Message::parse(bzdecompress($this->data)); break; default: /* TODO error? */ } } function body() { $body = chr($this->algorithm); switch($this->algorithm) { case 0: $body .= $this->data->to_bytes(); break; case 1: $body .= gzdeflate($this->data->to_bytes()); break; case 2: $body .= gzcompress($this->data->to_bytes()); break; case 3: $body .= bzcompress($this->data->to_bytes()); break; default: /* TODO error? */ } return $body; } // IteratorAggregate interface function getIterator() { return new ArrayIterator($this->data->packets); } // ArrayAccess interface function offsetExists($offset) { return isset($this->data[$offset]); } function offsetGet($offset) { return $this->data[$offset]; } function offsetSet($offset, $value) { return is_null($offset) ? $this->data[] = $value : $this->data[$offset] = $value; } function offsetUnset($offset) { unset($this->data[$offset]); } } /** * OpenPGP Symmetrically Encrypted Data packet (tag 9). * * @see http://tools.ietf.org/html/rfc4880#section-5.7 */ class OpenPGP_EncryptedDataPacket extends OpenPGP_Packet { // TODO } /** * OpenPGP Marker packet (tag 10). * * @see http://tools.ietf.org/html/rfc4880#section-5.8 */ class OpenPGP_MarkerPacket extends OpenPGP_Packet { // TODO } /** * OpenPGP Literal Data packet (tag 11). * * @see http://tools.ietf.org/html/rfc4880#section-5.9 */ class OpenPGP_LiteralDataPacket extends OpenPGP_Packet { public $format, $filename, $timestamp; function __construct($data=NULL, $opt=array()) { parent::__construct(); $this->data = $data; $this->format = isset($opt['format']) ? $opt['format'] : 'b'; $this->filename = isset($opt['filename']) ? $opt['filename'] : 'data'; $this->timestamp = isset($opt['timestamp']) ? $opt['timestamp'] : time(); } function normalize() { if($this->format == 'u' || $this->format == 't') { // Normalize line endings $this->data = str_replace("\n", "\r\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $this->data))); } } function read() { $this->size = $this->length - 1 - 4; $this->format = $this->read_byte(); $filename_length = ord($this->read_byte()); $this->size -= $filename_length; $this->filename = $this->read_bytes($filename_length); $this->timestamp = $this->read_timestamp(); $this->data = $this->read_bytes($this->size); } function body() { return $this->format.chr(strlen($this->filename)).$this->filename.pack('N', $this->timestamp).$this->data; } } /** * OpenPGP Trust packet (tag 12). * * @see http://tools.ietf.org/html/rfc4880#section-5.10 */ class OpenPGP_TrustPacket extends OpenPGP_Packet { function read() { $this->data = $this->input; } function body() { return $this->data; } } /** * OpenPGP User ID packet (tag 13). * * @see http://tools.ietf.org/html/rfc4880#section-5.11 * @see http://tools.ietf.org/html/rfc2822 */ class OpenPGP_UserIDPacket extends OpenPGP_Packet { public $name, $comment, $email; function __construct($name='', $comment='', $email='') { parent::__construct(); if(!$comment && !$email) { $this->input = $name; $this->read(); } else { $this->name = $name; $this->comment = $comment; $this->email = $email; } } function read() { $this->text = $this->input; // User IDs of the form: "name (comment) " if (preg_match('/^([^\(]+)\(([^\)]+)\)\s+<([^>]+)>$/', $this->text, $matches)) { $this->name = trim($matches[1]); $this->comment = trim($matches[2]); $this->email = trim($matches[3]); } // User IDs of the form: "name " else if (preg_match('/^([^<]+)\s+<([^>]+)>$/', $this->text, $matches)) { $this->name = trim($matches[1]); $this->comment = NULL; $this->email = trim($matches[2]); } // User IDs of the form: "name" else if (preg_match('/^([^<]+)$/', $this->text, $matches)) { $this->name = trim($matches[1]); $this->comment = NULL; $this->email = NULL; } // User IDs of the form: "" else if (preg_match('/^<([^>]+)>$/', $this->text, $matches)) { $this->name = NULL; $this->comment = NULL; $this->email = trim($matches[2]); } } function __toString() { $text = array(); if ($this->name) { $text[] = $this->name; } if ($this->comment) { $text[] = "({$this->comment})"; } if ($this->email) { $text[] = "<{$this->email}>"; } return implode(' ', $text); } function body() { return ''.$this; // Convert to string is the body } } /** * OpenPGP User Attribute packet (tag 17). * * @see http://tools.ietf.org/html/rfc4880#section-5.12 * @see http://tools.ietf.org/html/rfc4880#section-11.1 */ class OpenPGP_UserAttributePacket extends OpenPGP_Packet { public $packets; // TODO } /** * OpenPGP Sym. Encrypted Integrity Protected Data packet (tag 18). * * @see http://tools.ietf.org/html/rfc4880#section-5.13 */ class OpenPGP_IntegrityProtectedDataPacket extends OpenPGP_Packet { // TODO } /** * OpenPGP Modification Detection Code packet (tag 19). * * @see http://tools.ietf.org/html/rfc4880#section-5.14 */ class OpenPGP_ModificationDetectionCodePacket extends OpenPGP_Packet { // TODO } /** * OpenPGP Private or Experimental packet (tags 60..63). * * @see http://tools.ietf.org/html/rfc4880#section-4.3 */ class OpenPGP_ExperimentalPacket extends OpenPGP_Packet {}