diff --git a/res/invalid.phar b/res/invalid.phar new file mode 100644 index 0000000..696b7ad Binary files /dev/null and b/res/invalid.phar differ diff --git a/res/md5.phar b/res/md5.phar new file mode 100644 index 0000000..d2bb8d4 Binary files /dev/null and b/res/md5.phar differ diff --git a/res/missing.phar b/res/missing.phar new file mode 100644 index 0000000..90c4ac3 Binary files /dev/null and b/res/missing.phar differ diff --git a/res/openssl.phar b/res/openssl.phar new file mode 100644 index 0000000..a2cd9b8 Binary files /dev/null and b/res/openssl.phar differ diff --git a/res/openssl.phar.pubkey b/res/openssl.phar.pubkey new file mode 100644 index 0000000..811450e --- /dev/null +++ b/res/openssl.phar.pubkey @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKuZkrHT54KtuBCTrR36+4tibd+2un9b +aLFs3X+RHc/jDCXL8pJATz049ckfcfd2ZCMIzH1PHew8H+EMhy4CbSECAwEAAQ== +-----END PUBLIC KEY----- diff --git a/res/sha1.phar b/res/sha1.phar new file mode 100644 index 0000000..d417151 Binary files /dev/null and b/res/sha1.phar differ diff --git a/res/sha256.phar b/res/sha256.phar new file mode 100644 index 0000000..2d1a1c3 Binary files /dev/null and b/res/sha256.phar differ diff --git a/res/sha512.phar b/res/sha512.phar new file mode 100644 index 0000000..4be90d3 Binary files /dev/null and b/res/sha512.phar differ diff --git a/src/lib/Herrera/Box/Box.php b/src/lib/Herrera/Box/Box.php index 30fa1ab..3481454 100644 --- a/src/lib/Herrera/Box/Box.php +++ b/src/lib/Herrera/Box/Box.php @@ -9,9 +9,11 @@ use Herrera\Box\Exception\OpenSslException; use Herrera\Box\Exception\UnexpectedValueException; use Phar; +use PharException; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RegexIterator; +use RuntimeException; use SplFileInfo; use SplObjectStorage; use Traversable; @@ -44,6 +46,19 @@ class Box */ private $phar; + /** + * The available signature types. + * + * @var array + */ + private static $types = array( + 0x01 => array('MD5', 16), + 0x02 => array('SHA1', 20), + 0x03 => array('SHA256', 32), + 0x04 => array('SHA512', 64), + 0x10 => array('OpenSSL', null), + ); + /** * The placeholder values. * @@ -254,6 +269,71 @@ public function getPhar() return $this->phar; } + /** + * Returns the signature of the phar. + * + * This method does not use the extension to extract the phar's signature. + * + * @param string $path The phar file path. + * + * @return array The signature. + * + * @throws PharException If the phar is not valid. + * @throws RuntimeException If the file could not be read. + */ + public static function getSignature($path) + { + $signature = array(); + + $raw = self::readEnd($path, -12, 12); + + $flag = substr($raw, -4, 4); + + $type = unpack('V', substr($raw, -8, 4)); + $type = $type[1]; + + $size = unpack('V', substr($raw, 0, 4)); + $size = $size[1]; + + if ('GBMB' === $flag) { + if (isset(self::$types[$type])) { + $signature['hash_type'] = self::$types[$type][0]; + + if (self::$types[$type][1]) { + $size = self::$types[$type][1]; + } + + $signature['hash'] = self::readEnd( + $path, + self::$types[$type][1] ? -8 - $size : -12 - $size, + $size + ); + + $signature['hash'] = unpack('H*', $signature['hash']); + $signature['hash'] = strtoupper($signature['hash'][1]); + } else { + throw new PharException( + sprintf( + 'The signature type (%x) of "%s" is not recognized.', + $type, + $path + ) + ); + } + } else { + if (ini_get('phar.require_hash')) { + throw new PharException( + sprintf( + 'The phar "%s" is not signed.', + $path + ) + ); + } + } + + return $signature; + } + /** * Replaces the placeholders with their values. * @@ -392,4 +472,59 @@ public function signUsingFile($file, $password = null) $this->sign($key, $password); } + + /** + * Reads from the end of a file. + * + * @param string $file The file handle. + * @param integer $offset The end offset. + * @param integer $bytes The number of bytes to read. + * + * @return string The read bytes. + * + * @throws RuntimeException If the file could not be read. + */ + private static function readEnd($file, $offset, $bytes) + { + if (false === ($size = @filesize($file))) { + $error = error_get_last(); + + throw new RuntimeException( + sprintf( + 'The size of the file "%s" could not be retrieved: %s', + $file, + $error['message'] + ) + ); + } + + $read = @file_get_contents($file, false, null, $size + $offset, $bytes); + + if (false === $read) { + $error = error_get_last(); + + throw new RuntimeException( + sprintf( + 'The file "%s" could not be read at offset %d for %d bytes: %s', + $file, + $size + $offset, + $bytes, + $error['message'] + ) + ); + } + + if (($actual = strlen($read)) !== $bytes) { + throw new RuntimeException( + sprintf( + 'Could only read %d of %d bytes from "%s".', + $actual, + $bytes, + $file + ) + ); + } + + return $read; + } } diff --git a/src/tests/Herrera/Box/Tests/BoxTest.php b/src/tests/Herrera/Box/Tests/BoxTest.php index 2481cda..15fd37c 100644 --- a/src/tests/Herrera/Box/Tests/BoxTest.php +++ b/src/tests/Herrera/Box/Tests/BoxTest.php @@ -60,6 +60,37 @@ public function getPrivateKey() ); } + public function getSignatures() + { + return array( + array( + RES_DIR . '/md5.phar', + 'MD5', + '6772FC5AB8AB8D72BB69B6A0C323C796' + ), + array( + RES_DIR . '/sha1.phar', + 'SHA1', + 'A158DF4720C619E84A777982402B5D32394805E5' + ), + array( + RES_DIR . '/sha256.phar', + 'SHA256', + 'D0A6AB31437CA8485779E7603670ABDE05C911EA7FCEC051B8D3FB14A1480B7D' + ), + array( + RES_DIR . '/sha512.phar', + 'SHA512', + 'B4CAE177138A773283A748C8770A7142F0CC36D6EE88E37900BCF09A92D840D237CE3F3B47C2C7B39AC2D2C0F9A16D63FE70E1A455723DD36840B6E2E64E2130' + ), + array( + RES_DIR . '/openssl.phar', + 'OpenSSL', + '54AF1D4E5459D3A77B692E46FDB9C965D1C7579BD1F2AD2BECF4973677575444FE21E104B7655BA3D088090C28DF63D14876B277C423C8BFBCDB9E3E63F9D61A' + ), + ); + } + public function testAddCompactor() { $compactor = new Compactor(); @@ -319,6 +350,46 @@ public function testGetPhar() $this->assertSame($this->phar, $this->box->getPhar()); } + /** + * @dataProvider getSignatures + */ + public function testGetSignature($file, $type, $hash) + { + $signature = Box::getSignature($file); + + $this->assertEquals( + array( + 'hash_type' => $type, + 'hash' => $hash, + ), + $signature + ); + } + + public function testGetSignatureInvalid() + { + $path = RES_DIR . '/invalid.phar'; + + $this->setExpectedException( + 'PharException', + "The signature type (ffffffff) of \"$path\" is not recognized." + ); + + Box::getSignature($path); + } + + public function testGetSignatureMissing() + { + $path = RES_DIR . '/missing.phar'; + + $this->setExpectedException( + 'PharException', + "The phar \"$path\" is not signed." + ); + + Box::getSignature($path); + } + public function testReplaceValues() { $this->setPropertyValue(