From 360005b3684c16a7ff1923b4abe646bdf091b695 Mon Sep 17 00:00:00 2001 From: Arto Bendiken Date: Fri, 22 Jan 2010 11:32:59 +0100 Subject: [PATCH] Created repository. --- .gitignore | 4 + AUTHORS | 1 + README | 1 + README.md | 47 ++++ UNLICENSE | 24 ++ VERSION | 1 + lib/openpgp.php | 579 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 657 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 120000 README create mode 100644 README.md create mode 100644 UNLICENSE create mode 100644 VERSION create mode 100644 lib/openpgp.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e48cc98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.tmp +pkg +tmp diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..0134601 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +* Arto Bendiken diff --git a/README b/README new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/README @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e54d083 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +OpenPGP.php: OpenPGP for PHP +============================ + +This is a pure-PHP implementation of the OpenPGP Message Format (RFC 4880). + +* + +### About OpenPGP + +OpenPGP is the most widely-used e-mail encryption standard in the world. It +is defined by the OpenPGP Working Group of the Internet Engineering Task +Force (IETF) Proposed Standard RFC 4880. The OpenPGP standard was originally +derived from PGP (Pretty Good Privacy), first created by Phil Zimmermann in +1991. + +* +* + +Features +-------- + +* Encodes and decodes ASCII-armored OpenPGP messages. +* Parses OpenPGP messages into their constituent packets. + * Supports both old-format (PGP 2.6.x) and new-format (RFC 4880) packets. + +Download +-------- + +To get a local working copy of the development repository, do: + + % git clone git://github.com/bendiken/openpgp-php.git + +Alternatively, you can download the latest development version as a tarball +as follows: + + % wget http://github.com/bendiken/openpgp-php/tarball/master + +Authors +------- + +* [Arto Bendiken](mailto:arto.bendiken@gmail.com) - + +License +------- + +OpenPGP.php is free and unencumbered public domain software. For more +information, see or the accompanying UNLICENSE file. diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..8acdd82 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1 diff --git a/lib/openpgp.php b/lib/openpgp.php new file mode 100644 index 0000000..a465ef9 --- /dev/null +++ b/lib/openpgp.php @@ -0,0 +1,579 @@ + + * @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 .= '=' . 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; + } +} + +////////////////////////////////////////////////////////////////////////////// +// 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; + } + + // 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); + } + $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; + // TODO + } + + /** + * 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() {} + + function read() { + } + + /** + * @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 $unpacked[1]; + } + + 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). + * + * @see http://tools.ietf.org/html/rfc4880#section-5.2 + */ +class OpenPGP_SignaturePacket extends OpenPGP_Packet { + // TODO +} + +/** + * 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 { + // TODO +} + +/** + * 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; + + /** + * @see http://tools.ietf.org/html/rfc4880#section-5.5.2 + */ + function read() { + switch ($this->version = ord($this->read_byte())) { + case 2: + case 3: + return FALSE; // TODO + case 4: + $this->timestamp = $this->read_timestamp(); + $this->algorithm = ord($this->read_byte()); + $this->read_key_material(); + return TRUE; + } + } + + /** + * @see http://tools.ietf.org/html/rfc4880#section-5.5.2 + */ + function read_key_material() { + static $key_fields = array( + 1 => array('n', 'e'), // RSA + 16 => array('p', 'g', 'y'), // ELG-E + 17 => array('p', 'q', 'g', 'y'), // DSA + ); + foreach ($key_fields[$this->algorithm] as $field) { + $this->key[$field] = $this->read_mpi(); + } + $this->key_id = substr($this->fingerprint(), -8); + } + + /** + * @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($this->key['n'] . $this->key['e']); + case 4: + $material = array( + chr(0x99), pack('n', $this->length), + chr($this->version), pack('N', $this->timestamp), + chr($this->algorithm), + ); + foreach ($this->key as $data) { + $material[] = pack('n', OpenPGP::bitlength($data)); + $material[] = $data; + } + return $this->fingerprint = sha1(implode('', $material)); + } + } +} + +/** + * 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 { + // TODO +} + +/** + * 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 { + // TODO +} + +/** + * 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 { + // TODO +} + +/** + * OpenPGP Trust packet (tag 12). + * + * @see http://tools.ietf.org/html/rfc4880#section-5.10 + */ +class OpenPGP_TrustPacket extends OpenPGP_Packet { + // TODO +} + +/** + * 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 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); + } +} + +/** + * 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 {}