From 3a24f2f3836b5c66f8b8120fa50ca24a72d569a1 Mon Sep 17 00:00:00 2001 From: angryThom Date: Thu, 29 Sep 2016 14:57:41 +0200 Subject: [PATCH 01/31] Refactor hashing and add a new password hasher --- src/AngryBytes/Hash/Hash.php | 160 ++++++++++-------------- src/AngryBytes/Hash/Hasher/Blowfish.php | 31 +++-- src/AngryBytes/Hash/Hasher/MD5.php | 27 ++-- src/AngryBytes/Hash/Hasher/Password.php | 139 ++++++++++++++++++++ src/AngryBytes/Hash/HasherInterface.php | 12 +- 5 files changed, 253 insertions(+), 116 deletions(-) create mode 100644 src/AngryBytes/Hash/Hasher/Password.php diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index 408b45d..d67d48d 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -1,25 +1,20 @@ setHasher($hasher) @@ -79,7 +74,7 @@ public function setHasher(HasherInterface $hasher) /** * Get the salt * - * @return string + * @return string|bool */ public function getSalt() { @@ -89,17 +84,19 @@ public function getSalt() /** * Set the salt * - * @param string $salt + * @param string|bool $salt * @return Hash */ public function setSalt($salt) { - // Make sure it's of sufficient length - if (strlen($salt) < 20) { - throw new InvalidArgumentException(sprintf( - 'Provided salt "%s" is not long enough. A minimum length of 20 characters is required', - $salt - )); + if ($salt) { + // Make sure it's of sufficient length + if (strlen($salt) < 20) { + throw new InvalidArgumentException(sprintf( + 'Provided salt "%s" is not long enough. A minimum length of 20 characters is required', + $salt + )); + } } $this->salt = $salt; @@ -110,21 +107,31 @@ public function setSalt($salt) /** * Generate a hash * - * Will accept any number of (serializable) variables + * Accepts any type of variable. Non-scalar values will be serialized before hashing. * - * @param mixed $what1 - * @param mixed $what2 - * ... - * @param mixed $whatN + * @param mixed $data The data to hash * @return string **/ - public function hash() + public function hash($data) { return $this->getHasher()->hash( - call_user_func_array( - array($this, 'getDataString'), - func_get_args() - ), + $this->getDataString($data), + $this->getSalt() + ); + } + + /** + * Verify if the data matches the hash + * + * @param mixed $data + * @param string $hash + * @return bool + */ + public function verify($data, $hash) + { + return $this->getHasher()->verify( + $this->getDataString($data), + $hash, $this->getSalt() ); } @@ -132,68 +139,47 @@ public function hash() /** * Hash something into a short hash * - * This is simply a shortened version of hash(), returning a 7 character - * hash, which should be good enough for identification purposes. It is - * however *not* strong enough for cryptographical uses or password - * storage. Use only to create a fast, short string to compare or identify. + * This is simply a shortened version of hash(), returning a 7 character hash, which should be good + * enough for identification purposes. Therefore it MUST NOT be used for cryptographic purposes or + * password storage but only to create a fast, short string to compare or identify. + * + * It is RECOMMENDED to only use this method with the Hasher\MD5 hasher as hashes + * created by bcrypt/crypt() have a common beginning. * * @see Hash::hash() * - * @param mixed $what1 - * @param mixed $what2 - * ... - * @param mixed $whatN * @return string **/ - public function shortHash() + public function shortHash($data) { - $fullHash = call_user_func_array( - array($this, 'hash'), - func_get_args() - ); - - return substr($fullHash, 0, 7); + return substr($this->hash( + $this->getDataString($data) + ), 0, 7); } /** - * Does something match a short hash? - * - * First argument to this function is the short hash as it *should* be, all other arguments will - * be passed to shortHash() + * Verify if the data matches the shortHash * * @see Hash::shortHash() * - * @param mixed $what - * @param string $hash + * @param mixed $data + * @param string $shortHash * @return bool **/ - public function matchesShortHash() + public function verifyShortHash($data, $shortHash) { - // Full args to method - $args = func_get_args(); - - // Remove compareTo from the beginning - $compareTo = array_shift($args); + $data = $this->getDataString($data); - // Create short hash to compare to - $shortHash = call_user_func_array( - array($this, 'shortHash'), - $args + return self::compare( + $this->shortHash($data), + $shortHash ); - - // Compare and return - return self::compare($compareTo, $shortHash); } /** * Compare two hashes * - * This method is time-safe, i.e. it will take the same amount of time to - * execute for all inputs. This is critical to avoid timing attacks. - * - * NOTE: - * - * **This method only works on ASCII strings.** + * Uses the time-save `hash_equals()` function to compare 2 hashes. * * @param string $hashA * @param string $hashB @@ -201,38 +187,22 @@ public function matchesShortHash() **/ public static function compare($hashA, $hashB) { - // Compare length - if (strlen($hashA) !== strlen($hashB)) { - return false; - } - - // bitwise OR total for all characters - $result = 0; - - for ($charIndex = 0; $charIndex < strlen($hashA); $charIndex++) { - // XOR the value of the ASCII characters at $charIndex - // This value is then OR-ed into the total - $result |= ord($hashA[$charIndex]) ^ ord($hashB[$charIndex]); - } - - // If the result is anything but 0 there was a differing character at - // one or more of the positions - return $result === 0; + return hash_equals($hashA, $hashB); } /** - * Get the passed arguments as a string + * Get the data as a string * - * Accepts anything serializable + * Will serialize non-scalar values * - * @param mixed $what1 - * @param mixed $what2 - * ... - * @param mixed $whatN * @return string **/ - private function getDataString() + private function getDataString($data) { - return serialize(func_get_args()); + if (is_scalar($data)) { + return $data; + } + + return serialize($data); } } diff --git a/src/AngryBytes/Hash/Hasher/Blowfish.php b/src/AngryBytes/Hash/Hasher/Blowfish.php index 0759434..09e56b4 100644 --- a/src/AngryBytes/Hash/Hasher/Blowfish.php +++ b/src/AngryBytes/Hash/Hasher/Blowfish.php @@ -5,11 +5,12 @@ * @category AngryBytes * @package Hash * @subpackage Hasher - * @copyright Copyright (c) 2010 Angry Bytes BV (http://www.angrybytes.com) + * @copyright Copyright (c) 2007-2016 Angry Bytes BV (http://www.angrybytes.com) */ namespace AngryBytes\Hash\Hasher; +use AngryBytes\Hash\Hash; use AngryBytes\Hash\HasherInterface; use \RuntimeException; @@ -18,15 +19,11 @@ /** * Blowfish * - * Blowfish hasher - * - * Relies on bcrypt/crypt() for the heavy lifting + * Generate and verify Blowfish bcrypt/crypt() hashes using a salt * * @category AngryBytes * @package Hash * @subpackage Hasher - * - * @hootie */ class Blowfish implements HasherInterface { @@ -82,17 +79,26 @@ public function setWorkFactor($workFactor) } /** - * Hash password and salt - * - * @param string $data - * @param string $salt - * @return string - **/ + * {@inheritDoc} + */ public function hash($data, $salt) { return crypt($data, $this->bcryptSalt($salt)); } + /** + * {@inheritDoc} + * + * @see Hash::compare() + */ + public function verify($data, $hash, $salt) + { + return Hash::compare( + $this->hash($data, $salt), + $hash + ); + } + /** * Generate a bcrypt salt from a string salt * @@ -134,4 +140,3 @@ private static function getSaltSubstr($salt) ); } } - diff --git a/src/AngryBytes/Hash/Hasher/MD5.php b/src/AngryBytes/Hash/Hasher/MD5.php index 55a396f..faa25fe 100644 --- a/src/AngryBytes/Hash/Hasher/MD5.php +++ b/src/AngryBytes/Hash/Hasher/MD5.php @@ -10,14 +10,18 @@ namespace AngryBytes\Hash\Hasher; +use AngryBytes\Hash\Hash; use AngryBytes\Hash\HasherInterface; /** * MD5 * - * MD5 hasher. + * Generate and verify MD5 hashes using a salt * - * You probably shouldn't use this for passwords + * NOTE: + * + * This hasher MUST NOT be used for password storage. It is RECOMMENDED + * to use the Hasher\Password for this purpose * * @category AngryBytes * @package Hash @@ -26,14 +30,23 @@ class MD5 implements HasherInterface { /** - * Hash a value using md5 - * - * @param string $data - * @param string $salt - * @return string + * {@inheritDoc} */ public function hash($data, $salt) { return md5($data . '-' . $salt); } + + /** + * {@inheritDoc} + * + * @see Hash::compare() + */ + public function verify($data, $hash, $salt) + { + return Hash::compare( + $this->hash($data, $salt), + $hash + ); + } } diff --git a/src/AngryBytes/Hash/Hasher/Password.php b/src/AngryBytes/Hash/Hasher/Password.php new file mode 100644 index 0000000..38693db --- /dev/null +++ b/src/AngryBytes/Hash/Hasher/Password.php @@ -0,0 +1,139 @@ +cost; + } + + /** + * Set cost + * + * @throws InvalidArgumentException if the cost is too high or low + * @param int $cost + * @return $this + */ + public function setCost($cost) + { + if ($cost < 4 || $cost > 31) { + throw new InvalidArgumentException( + 'Cost needs to be greater than 3 and smaller than 32' + ); + } + $this->cost = (int) $cost; + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws RuntimeException If the hashing fails + * @param string|bool $salt (optional) When omitted `password_hash()` will generate it's own salt + */ + public function hash($data, $salt = false) + { + // Set hash options + $options = ['cost' => $this->cost]; + if ($salt) { + $options['salt'] = $salt; + } + + $hash = password_hash($data, self::ALGORITHM, $options); + + if (!$hash) { + throw RuntimeException('Failed to hash password'); + } + + return $hash; + } + + /** + * {@inheritDoc} + * + * NOTE `$salt` is not used, `password_verify()` retrieves the used salt from the `$hash` + */ + public function verify($data, $hash, $salt) + { + return password_verify($data, $hash); + } + + /** + * Determine if the password needs to be rehashed based on the hash options + * + * If true, rehashed the password after verifying it + * + * @param string $hash + * @param string|bool $salt (optional) When omitted `password_needs_rehash()` will generate it's own salt + * @return bool + */ + public function needsRehash($hash, $salt = false) + { + // Set hash options + $options = ['cost' => $this->cost]; + if ($salt) { + $options['salt'] = $salt; + } + + return password_needs_rehash($hash, self::ALGORITHM, $options); + } + + /** + * Get info for the given hash + * + * @param string $hash + * @return mixed[] + */ + public function getInfo($hash) + { + return password_get_info($hash); + } +} + diff --git a/src/AngryBytes/Hash/HasherInterface.php b/src/AngryBytes/Hash/HasherInterface.php index 0c903bf..109ddf4 100644 --- a/src/AngryBytes/Hash/HasherInterface.php +++ b/src/AngryBytes/Hash/HasherInterface.php @@ -5,7 +5,7 @@ * @category AngryBytes * @package Hash * @subpackage Hasher - * @copyright Copyright (c) 2010 Angry Bytes BV (http://www.angrybytes.com) + * @copyright Copyright (c) 2007-2016 Angry Bytes BV (http://www.angrybytes.com) */ namespace AngryBytes\Hash; @@ -31,4 +31,14 @@ interface HasherInterface * @return string **/ public function hash($data, $salt); + + /** + * Verify is the data string matches the given hash + * + * @param string $data + * @param string $hash + * @param string $salt + * @return bool + */ + public function verify($data, $hash, $salt); } From d98fcb0c8d1b4be0e704f89022826b32b019a82d Mon Sep 17 00:00:00 2001 From: angryThom Date: Thu, 29 Sep 2016 15:13:19 +0200 Subject: [PATCH 02/31] Fix HMAC namespace issue --- src/AngryBytes/Hash/HMAC.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/AngryBytes/Hash/HMAC.php b/src/AngryBytes/Hash/HMAC.php index e58afc8..88f9012 100644 --- a/src/AngryBytes/Hash/HMAC.php +++ b/src/AngryBytes/Hash/HMAC.php @@ -4,10 +4,10 @@ * * @category AngryBytes * @package Hash - * @copyright Copyright (c) 2010 Angry Bytes BV (http://www.angrybytes.com) + * @copyright Copyright (c) 2007-2016 Angry Bytes BV (http://www.angrybytes.com) */ -use Angrybytes\Hash\Hash; +namespace AngryBytes\Hash; use \InvalidArgumentException; @@ -33,8 +33,7 @@ class HMAC /** * Constructor * - * @param string $algorithm - * @return void + * @param string $algorithm **/ public function __construct($algorithm) { From 0ce09ab22624b60ba43379ea6026526de13927fa Mon Sep 17 00:00:00 2001 From: angryThom Date: Thu, 29 Sep 2016 15:13:50 +0200 Subject: [PATCH 03/31] Remove random string generator --- src/AngryBytes/Hash/RandomString.php | 67 ---------------------------- 1 file changed, 67 deletions(-) delete mode 100644 src/AngryBytes/Hash/RandomString.php diff --git a/src/AngryBytes/Hash/RandomString.php b/src/AngryBytes/Hash/RandomString.php deleted file mode 100644 index 080c81c..0000000 --- a/src/AngryBytes/Hash/RandomString.php +++ /dev/null @@ -1,67 +0,0 @@ - Date: Thu, 29 Sep 2016 15:18:17 +0200 Subject: [PATCH 04/31] Remove random string test --- .../AngryBytes/Hash/Test/RandomStringTest.php | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 tests/AngryBytes/Hash/Test/RandomStringTest.php diff --git a/tests/AngryBytes/Hash/Test/RandomStringTest.php b/tests/AngryBytes/Hash/Test/RandomStringTest.php deleted file mode 100644 index 7bae7c6..0000000 --- a/tests/AngryBytes/Hash/Test/RandomStringTest.php +++ /dev/null @@ -1,45 +0,0 @@ -assertEquals( - strlen($bytes), - $numberOfBytes, - 'Number of bytes returned should be ' . $numberOfBytes - ); - } - } -} From 588bb47a8894c2c8bf456b35117856dda2aab2e9 Mon Sep 17 00:00:00 2001 From: angryThom Date: Thu, 29 Sep 2016 15:41:12 +0200 Subject: [PATCH 05/31] Fix unitests --- tests/AngryBytes/Hash/Test/BlowfishTest.php | 18 ++++++++---------- tests/AngryBytes/Hash/Test/MD5Test.php | 12 +++++------- tests/AngryBytes/Hash/Test/TestCase.php | 4 ++-- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/tests/AngryBytes/Hash/Test/BlowfishTest.php b/tests/AngryBytes/Hash/Test/BlowfishTest.php index d1c0504..64b93aa 100644 --- a/tests/AngryBytes/Hash/Test/BlowfishTest.php +++ b/tests/AngryBytes/Hash/Test/BlowfishTest.php @@ -5,20 +5,18 @@ * @category AngryBytes * @package Hash * @subpackage Test - * @copyright Copyright (c) 2010 Angry Bytes BV (http://www.angrybytes.com) + * @copyright Copyright (c) 2007-2016 Angry Bytes BV (http://www.angrybytes.com) */ namespace AngryBytes\Hash\Test; -use AngryBytes\Hash\Test\TestCase; - use AngryBytes\Hash\Hash; use AngryBytes\Hash\Hasher\Blowfish as BlowfishHasher; /** * BlowfishTest * - * Testing file adapter + * Testing blowfish hasher * * @category AngryBytes * @package Hash @@ -37,11 +35,11 @@ public function testString() // Simple string $this->assertEquals( - '$2y$15$aa5c57dda7634fc90a92duWE0jEXBsxhrZrjtIDNJxqSVAgleehYW', + '$2y$15$aa5c57dda7634fc90a92duQSfz3E1u39Z6s63i6l5QpvgJK5tSKri', $hasher->hash('foo') ); $this->assertNotEquals( - '$2y$15$aa5c57dda7634fc90a92duWE0jEXBsxhrZrjtIDNJxqSVAgleehYW', + '$2y$15$aa5c57dda7634fc90a92duQSfz3E1u39Z6s63i6l5QpvgJK5tSKri', $hasher->hash('bar') ); } @@ -62,7 +60,7 @@ public function testSerialized() 12345 ); $this->assertEquals( - '$2y$15$aa5c57dda7634fc90a92duEeKPUIy2mlag3NxVWnd1S.pWl0l6Vkq', + '$2y$15$aa5c57dda7634fc90a92duDv2OoNSn8R.p3.GSoaEZd6/vdiiq9lG', $hasher->hash($data) ); @@ -71,7 +69,7 @@ public function testSerialized() // Should no longer match $this->assertNotEquals( - '$2y$15$aa5c57dda7634fc90a92duEeKPUIy2mlag3NxVWnd1S.pWl0l6Vkq', + '$2y$15$aa5c57dda7634fc90a92duDv2OoNSn8R.p3.GSoaEZd6/vdiiq9lG', $hasher->hash($data) ); } @@ -115,13 +113,13 @@ public function testWorkFactor() // Simple string $this->assertEquals( - '$2y$05$aa5c57dda7634fc90a92duqnvmZIAm8fd3YauHfd2Lyt.5Rlz6BsC', + '$2y$05$aa5c57dda7634fc90a92duCIqZ6agXYH9mOnF/It6sfh3MAJAkKXe', $hasher->hash('foo') ); $hasher->getHasher()->setWorkFactor(10); $this->assertEquals( - '$2y$10$aa5c57dda7634fc90a92duaIgY20lEZW.nomcy7J7xN3jNAn5pvge', + '$2y$10$aa5c57dda7634fc90a92duoe.XRVTsrN1oW9P.qnaa.W0BGQ9olPy', $hasher->hash('foo') ); } diff --git a/tests/AngryBytes/Hash/Test/MD5Test.php b/tests/AngryBytes/Hash/Test/MD5Test.php index c3a9e25..e0f5f5e 100644 --- a/tests/AngryBytes/Hash/Test/MD5Test.php +++ b/tests/AngryBytes/Hash/Test/MD5Test.php @@ -5,13 +5,11 @@ * @category AngryBytes * @package Hash * @subpackage Test - * @copyright Copyright (c) 2010 Angry Bytes BV (http://www.angrybytes.com) + * @copyright Copyright (c) 2007-2016 Angry Bytes BV (http://www.angrybytes.com) */ namespace AngryBytes\Hash\Test; -use AngryBytes\Hash\Test\TestCase; - use AngryBytes\Hash\Hash; use AngryBytes\Hash\Hasher\MD5 as MD5Hasher; @@ -36,12 +34,12 @@ public function testHashString() $hasher = $this->createHasher(); $this->assertEquals( - '4dc664001bbbbf88d2b59eeda6855a6b', + 'f149d11c9f6d8e899772b855588722f2', $hasher->hash('foo') ); $this->assertEquals( - 'b5ab8f853032ce68de22035736209e75', + '7b84dc4faca7b93ea519eade24a5f634', $hasher->hash('bar') ); } @@ -59,14 +57,14 @@ public function testHashObject() $obj->foo = 'bar'; $this->assertEquals( - '8ff1610fc9c2607f7d8e9175080f7311', + '2f4162ad4b8774272e452efafd25972f', $hasher->hash($obj) ); $obj->bar = 'foo'; $this->assertEquals( - '458ee16d8b79287fb8cf8700469cc634', + '0d8c867eedd83575b44c62f8caac142b', $hasher->hash($obj) ); } diff --git a/tests/AngryBytes/Hash/Test/TestCase.php b/tests/AngryBytes/Hash/Test/TestCase.php index 14b8bfb..25095f4 100644 --- a/tests/AngryBytes/Hash/Test/TestCase.php +++ b/tests/AngryBytes/Hash/Test/TestCase.php @@ -5,7 +5,7 @@ * @category AngryBytes * @package Hash * @subpackage Test - * @copyright Copyright (c) 2010 Angry Bytes BV (http://www.angrybytes.com) + * @copyright Copyright (c) 2007-2016 Angry Bytes BV (http://www.angrybytes.com) */ namespace AngryBytes\Hash\Test; @@ -15,7 +15,7 @@ /** * TestCase * - * Testing memcached + * Testing hashing * * @category AngryBytes * @package Hash From a71d1cc1a6b3482cb47112905da33d92ee145237 Mon Sep 17 00:00:00 2001 From: angryThom Date: Thu, 29 Sep 2016 16:32:54 +0200 Subject: [PATCH 06/31] Add HMAC unittest --- tests/AngryBytes/Hash/Test/HMACTest.php | 99 +++++++++++++++++++++++++ tests/AngryBytes/Hash/Test/MD5Test.php | 37 +++++++++ 2 files changed, 136 insertions(+) create mode 100644 tests/AngryBytes/Hash/Test/HMACTest.php diff --git a/tests/AngryBytes/Hash/Test/HMACTest.php b/tests/AngryBytes/Hash/Test/HMACTest.php new file mode 100644 index 0000000..e41c3d6 --- /dev/null +++ b/tests/AngryBytes/Hash/Test/HMACTest.php @@ -0,0 +1,99 @@ +createHasher(); + + $this->assertEquals( + '304692bc0cd231dc337107a7964752858a78140e9696b1ffc2e9a646961ed812f890395480b7c2e8650d55c7d0b0bfe4551b5ce52fae0d73ee5e2c0b0609e164', + $hasher->hmac($this->secret, 'my message') + + ); + + $this->assertNotEquals( + '304692bc0cd231dc337107a7964752858a78140e9696b1ffc2e9a646961ed812f890395480b7c2e8650d55c7d0b0bfe4551b5ce52fae0d73ee5e2c0b0609e164', + $hasher->hmac('foo', 'my message') + ); + + $this->assertNotEquals( + '304692bc0cd231dc337107a7964752858a78140e9696b1ffc2e9a646961ed812f890395480b7c2e8650d55c7d0b0bfe4551b5ce52fae0d73ee5e2c0b0609e164', + $hasher->hmac($this->secret, 'some other message') + ); + } + + /** + * Test validation of HMAC hashes + */ + public function testValidateHmac() + { + $hasher = $this->createHasher(); + + $this->assertTrue( + $hasher->validHmac( + 'my message', + '304692bc0cd231dc337107a7964752858a78140e9696b1ffc2e9a646961ed812f890395480b7c2e8650d55c7d0b0bfe4551b5ce52fae0d73ee5e2c0b0609e164', + $this->secret + ) + ); + + $this->assertFalse( + $hasher->validHmac( + 'my message', + '304692bc0cd231dc337107a7964752858a78140e9696b1ffc2e9a646961ed812f890395480b7c2e8650d55c7d0b0bfe4551b5ce52fae0d73ee5e2c0b0609e164', + 'foo' + ) + ); + + $this->assertFalse( + $hasher->validHmac( + 'some other message', + '304692bc0cd231dc337107a7964752858a78140e9696b1ffc2e9a646961ed812f890395480b7c2e8650d55c7d0b0bfe4551b5ce52fae0d73ee5e2c0b0609e164', + $this->secret + ) + ); + } + + /** + * Create hasher + * + * @return HMAC + */ + private function createHasher() + { + return new HMAC('sha512'); + } +} + diff --git a/tests/AngryBytes/Hash/Test/MD5Test.php b/tests/AngryBytes/Hash/Test/MD5Test.php index e0f5f5e..5a06f28 100644 --- a/tests/AngryBytes/Hash/Test/MD5Test.php +++ b/tests/AngryBytes/Hash/Test/MD5Test.php @@ -69,6 +69,43 @@ public function testHashObject() ); } + /** + * Test verification of string hashes + */ + public function testVerifyString() + { + $hasher = $this->createHasher(); + + $this->assertTrue( + $hasher->verify('foo', 'f149d11c9f6d8e899772b855588722f2') + ); + + $this->assertFalse( + $hasher->verify('bar', 'f149d11c9f6d8e899772b855588722f2') + ); + } + + /** + * Test verification of object hashes + */ + public function testVerifyHashObject() + { + $hasher = $this->createHasher(); + + $obj = new \stdClass; + $obj->foo = 'bar'; + + $this->assertTrue( + $hasher->verify($obj, '2f4162ad4b8774272e452efafd25972f') + ); + + $obj->bar = 'foo'; + + $this->assertFalse( + $hasher->verify($obj, '2f4162ad4b8774272e452efafd25972f') + ); + } + /** * Create hasher * From 7093fef291d94afb3e93c05a815ed4011b7b78c8 Mon Sep 17 00:00:00 2001 From: angryThom Date: Thu, 29 Sep 2016 16:38:44 +0200 Subject: [PATCH 07/31] Add bloffish verify tests --- tests/AngryBytes/Hash/Test/BlowfishTest.php | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/AngryBytes/Hash/Test/BlowfishTest.php b/tests/AngryBytes/Hash/Test/BlowfishTest.php index 64b93aa..1d3e8f2 100644 --- a/tests/AngryBytes/Hash/Test/BlowfishTest.php +++ b/tests/AngryBytes/Hash/Test/BlowfishTest.php @@ -74,6 +74,48 @@ public function testSerialized() ); } + /** + * Test verification of string hashes + */ + public function testStringVerify() + { + $hasher = $this->createHasher(); + + $this->assertTrue( + $hasher->verify('foo', '$2y$15$aa5c57dda7634fc90a92duQSfz3E1u39Z6s63i6l5QpvgJK5tSKri') + ); + + $this->assertFalse( + $hasher->verify('bar', '$2y$15$aa5c57dda7634fc90a92duQSfz3E1u39Z6s63i6l5QpvgJK5tSKri') + ); + } + + /** + * Test verification of object hashes + */ + public function testObjectVerify() + { + $hasher = $this->createHasher(); + + // Complex data + $data = array( + new \stdClass, + array('foo', 'bar'), + 12345 + ); + + $this->assertTrue( + $hasher->verify($data, '$2y$15$aa5c57dda7634fc90a92duDv2OoNSn8R.p3.GSoaEZd6/vdiiq9lG') + ); + + // Append to data + $data[] = 'foo'; + + $this->assertFalse( + $hasher->verify($data, '$2y$15$aa5c57dda7634fc90a92duDv2OoNSn8R.p3.GSoaEZd6/vdiiq9lG') + ); + } + /** * Test invalid work factor * From 1f0b235b1a9e56da025b67f0342cfad3f134fb48 Mon Sep 17 00:00:00 2001 From: angryThom Date: Thu, 29 Sep 2016 17:08:43 +0200 Subject: [PATCH 08/31] Add password hasher unittest --- tests/AngryBytes/Hash/Test/PasswordTest.php | 116 ++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/AngryBytes/Hash/Test/PasswordTest.php diff --git a/tests/AngryBytes/Hash/Test/PasswordTest.php b/tests/AngryBytes/Hash/Test/PasswordTest.php new file mode 100644 index 0000000..83be540 --- /dev/null +++ b/tests/AngryBytes/Hash/Test/PasswordTest.php @@ -0,0 +1,116 @@ +createHasher(); + + // Even though the password are equal the hashes should not be + $this->assertNotEquals( + $hasher->hash('foo'), + $hasher->hash('foo') + ); + } + + /** + * Test password hash verification + */ + public function testStringVerify() + { + $hasher = $this->createHasher(); + + $this->assertTrue( + $hasher->verify('foo', '$2y$15$l7l05fKprnFw8Lpgg2TZwuxqf2bKLolUxpJDruR6YiasGNkFZgfae') + ); + + $this->assertFalse( + $hasher->verify('bar', '$2y$15$l7l05fKprnFw8Lpgg2TZwuxqf2bKLolUxpJDruR6YiasGNkFZgfae') + ); + } + + /** + * Test password hash rehash + */ + public function testRehash() + { + $hasher = $this->createHasher(); + + // Create hash + $hash = $hasher->hash('foo'); + + $this->assertFalse( + $hasher->getHasher()->needsRehash($hash) + ); + + // Adjust the hash cost, this should require a rehash + $hasher->getHasher()->setCost(16); + + $this->assertTrue( + $hasher->getHasher()->needsRehash($hash) + ); + } + + /** + * Test invalid cost factor + * + * @expectedException \InvalidArgumentException + */ + public function testCostTooLow() + { + $hasher = $this->createHasher(); + + $hasher->getHasher()->setCost(2); + } + + /** + * Test invalid cost factor + * + * @expectedException \InvalidArgumentException + */ + public function testCostTooHigh() + { + $hasher = $this->createHasher(); + + $hasher->getHasher()->setCost(42); + } + + /** + * Create hasher + * + * @return Hash + **/ + private function createHasher() + { + // Hasher + return new Hash( + new PasswordHasher + ); + } +} + From fb666db0cbd877811b2539be4366abfcd0401ec2 Mon Sep 17 00:00:00 2001 From: angryThom Date: Fri, 30 Sep 2016 11:43:01 +0200 Subject: [PATCH 09/31] Use default cost factor for password hasher --- src/AngryBytes/Hash/Hasher/Password.php | 16 +++++++++++----- tests/AngryBytes/Hash/Test/PasswordTest.php | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/AngryBytes/Hash/Hasher/Password.php b/src/AngryBytes/Hash/Hasher/Password.php index 38693db..9a3c339 100644 --- a/src/AngryBytes/Hash/Hasher/Password.php +++ b/src/AngryBytes/Hash/Hasher/Password.php @@ -37,11 +37,9 @@ class Password implements HasherInterface /** * Cost factor for the algorithm * - * Defaults to '15' (32768 iterations) - * * @var int */ - private $cost = 15; + private $cost; /** * Get cost @@ -81,7 +79,11 @@ public function setCost($cost) public function hash($data, $salt = false) { // Set hash options - $options = ['cost' => $this->cost]; + $options = []; + + if (is_int($this->cost)) { + $options['cost'] = $this->cost; + } if ($salt) { $options['salt'] = $salt; } @@ -117,7 +119,11 @@ public function verify($data, $hash, $salt) public function needsRehash($hash, $salt = false) { // Set hash options - $options = ['cost' => $this->cost]; + $options = []; + + if (is_int($this->cost)) { + $options['cost'] = $this->cost; + } if ($salt) { $options['salt'] = $salt; } diff --git a/tests/AngryBytes/Hash/Test/PasswordTest.php b/tests/AngryBytes/Hash/Test/PasswordTest.php index 83be540..3383bb5 100644 --- a/tests/AngryBytes/Hash/Test/PasswordTest.php +++ b/tests/AngryBytes/Hash/Test/PasswordTest.php @@ -90,7 +90,7 @@ public function testCostTooLow() /** * Test invalid cost factor - * + * * @expectedException \InvalidArgumentException */ public function testCostTooHigh() From d67d4dc08179067e329417c3b435768c8f18cb07 Mon Sep 17 00:00:00 2001 From: angryThom Date: Fri, 30 Sep 2016 11:48:24 +0200 Subject: [PATCH 10/31] Update password unit test for new cost --- tests/AngryBytes/Hash/Test/PasswordTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/AngryBytes/Hash/Test/PasswordTest.php b/tests/AngryBytes/Hash/Test/PasswordTest.php index 3383bb5..d52fc9f 100644 --- a/tests/AngryBytes/Hash/Test/PasswordTest.php +++ b/tests/AngryBytes/Hash/Test/PasswordTest.php @@ -46,11 +46,11 @@ public function testStringVerify() $hasher = $this->createHasher(); $this->assertTrue( - $hasher->verify('foo', '$2y$15$l7l05fKprnFw8Lpgg2TZwuxqf2bKLolUxpJDruR6YiasGNkFZgfae') + $hasher->verify('foo', '$2y$10$Ch4CoMWaM6KR0ZfC3ZKvc.FAW1U30luCURlyahm/XgzJ4TWDwWPFW') ); $this->assertFalse( - $hasher->verify('bar', '$2y$15$l7l05fKprnFw8Lpgg2TZwuxqf2bKLolUxpJDruR6YiasGNkFZgfae') + $hasher->verify('bar', '$2y$10$Ch4CoMWaM6KR0ZfC3ZKvc.FAW1U30luCURlyahm/XgzJ4TWDwWPFW') ); } @@ -69,7 +69,7 @@ public function testRehash() ); // Adjust the hash cost, this should require a rehash - $hasher->getHasher()->setCost(16); + $hasher->getHasher()->setCost(15); $this->assertTrue( $hasher->getHasher()->needsRehash($hash) From c6ac261fd1087ad64ac02f83a168e4819723eb15 Mon Sep 17 00:00:00 2001 From: angryThom Date: Fri, 30 Sep 2016 11:54:38 +0200 Subject: [PATCH 11/31] Skip cost check if cost is null --- src/AngryBytes/Hash/Hasher/Password.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/AngryBytes/Hash/Hasher/Password.php b/src/AngryBytes/Hash/Hasher/Password.php index 9a3c339..9107dd8 100644 --- a/src/AngryBytes/Hash/Hasher/Password.php +++ b/src/AngryBytes/Hash/Hasher/Password.php @@ -55,11 +55,16 @@ public function getCost() * Set cost * * @throws InvalidArgumentException if the cost is too high or low - * @param int $cost + * @param int|null $cost * @return $this */ public function setCost($cost) { + if (is_null($cost)) { + // Use default cost + return $this; + } + if ($cost < 4 || $cost > 31) { throw new InvalidArgumentException( 'Cost needs to be greater than 3 and smaller than 32' From 66a54a9eac670f2faab35c793eb3ff91b8bf7fc3 Mon Sep 17 00:00:00 2001 From: angryThom Date: Fri, 30 Sep 2016 14:34:47 +0200 Subject: [PATCH 12/31] Fix double serialization for short hash --- src/AngryBytes/Hash/Hash.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index d67d48d..10e6140 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -152,9 +152,7 @@ public function verify($data, $hash) **/ public function shortHash($data) { - return substr($this->hash( - $this->getDataString($data) - ), 0, 7); + return substr($this->hash($data), 0, 7); } /** @@ -168,8 +166,6 @@ public function shortHash($data) **/ public function verifyShortHash($data, $shortHash) { - $data = $this->getDataString($data); - return self::compare( $this->shortHash($data), $shortHash From 4f0d8e4fdcb111ff2f0b9daf65916f2596150ef8 Mon Sep 17 00:00:00 2001 From: angryThom Date: Mon, 3 Oct 2016 11:45:32 +0200 Subject: [PATCH 13/31] Add changelog and upgrading files --- CHANGELOG.md | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 21 +++++++++-- UPGRADING.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 UPGRADING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..debb399 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,101 @@ +# Changelog + +## 2.0.0 + +### New Password Hasher + +Version `2.0.0` comes with the new `AngryBytes\Hash\Hasher\Password` hasher. This hasher is intended to be used for +hashing passwords and other secure tokens. The hasher utilises PHP's native password hashing functions like +`password_hash()` and `password_verify()`, see [Password Hashing](http://php.net/manual/en/book.password.php). + +The password hasher has been setup to use PHP's default cost and algorithm. The default cost can however be overwritten +via the `setCost()` method, like so: + +```php +$hasher = new \AngryBytes\Hash\Hasher\Password; +$hasher->setCost(15); +``` + +The password hasher offers a method to check if an existing hash needs to be rehashed. This can be the case if +the cost and/or algorithm of the hasher has changed. If this is the case you'll want to rehash and store your existing hashes. + +#### Example + +In this example we will check if an existing password hash needs to be rehashed, if so we'll hash the password value. +Note, in order to rehash the password you will need access to the plaintext value. It's advised to to this after +a user has successfully entered their password. + +```php + +use AngryBytes\Hash\Hasher\Password; +use AngryBytes\Hash\Hash; + +// Create a password hasher +$hasher = new Hash( + new Password() +); + +$password = 'plaintext password' +$passwordHash = 'password hash'; + +// Verify the password against the hash +if ($hasher->verify($password, $passwordHash)) { + // Check if the hash needs to be rehashed + if ($hasher->getHasher()->needsRehash($passwordHash)) { + // Rehash the password + $passwordHash = $hasher->hash($password); + } +} + +``` + +### Refactored AngryBytes\Hash\HasherInterface + +`AngryBytes\Hash\HasherInterface::verify()` is a new method for verify an existing hash. The method accepts the +following three arguments: + +* `$data` - The data that needs to be hashed. +* `$hash` - The hash that needs to match the hashed value of `$data`. +* `$salt` - The salt used for hashing. + +### Refactored AngryBytes\Hash\Hash + +`AngryBytes\Hash\Hash::construct()` second argument (`$salt`) has become optional to accommodate for hashers that +handle their own salt, like `AngryBytes\Hash\Hasher\Password`. + +`AngryBytes\Hash\Hash::hash()` and `AngryBytes\Hash\Hash::shortHash()` no longer accept any number of arguments but +only one. This single argument can however be of any type. All non-scalar types will be serialized before hashing. + +`AngryBytes\Hash\Hash::verify()` is a new method that's available to validate a hash in a time-safe manner. +The method accepts the following two arguments: + +* `$data` - The data that needs to be hashed. This data can be of any type, all non-scalar types will be + serialized before hashing. +* `$hash` - The hash that needs to match the hashed value of `$data`. + +`AngryBytes\Hash\Hash::matchesShortHash()` is replaced by `AngryBytes\Hash\Hash::verifyShortHash()` this methods +accepts two arguments (`$data` and `$hash`) just like `AngryBytes\Hash\Hash::verify()`. + +### Minor Changes + +* Scalar values passed to `hash()` and `shortHash()` are no longer serialized. +* `compare()` Now uses PHP native `hash_equals()` function. +* Fixed namespace issues for `AngryBytes\Hash\HMAC`. + +### Upgrading + +Please refer to the [upgrade notes](UPGRADING.md). + +### Deprecated & Removed Components + +`AngryBytes\Hash\RandomString` has been removed. + +## 1.0.2 +Valid Blowfish salts (22 char long composed of ./A-Za-z0-9 characters only) are now used as the salt as-is instead +of md5-ed and substring-ed. + +## 1.0.1 +Adding travis build status and scrutinizer code qual. img. to readme + +## 1.0.0 +Initial release diff --git a/README.md b/README.md index b8e6a11..b2dbb39 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ # AngryBytes Hash +[![Author](http://img.shields.io/badge/author-@angrybytes-blue.svg?style=flat-square)](https://twitter.com/angrybytes) +[![Software License](https://img.shields.io/badge/license-proprietary-brightgreen.svg?style=flat-square)](LICENSE.md) [![Build Status](https://travis-ci.org/AngryBytes/hash.svg?branch=master)](https://travis-ci.org/AngryBytes/hash) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/AngryBytes/hash/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/AngryBytes/hash/?branch=master) A simple PHP library that simplifies cryptographical hashing. It provides an object oriented interface to a variety of hashing methods. +## Requirements + +* PHP `5.6.0` or `PHP 7.0` (recommended) + +## Installation + +Installation is done via Composer: `composer require angrybytes/hash`. + ## Contents ### Hash @@ -16,6 +26,7 @@ Available hashers are: * `AngryBytes\Hash\Hasher\BlowFish` * `AngryBytes\Hash\Hasher\MD5` + * `AngryBytes\Hash\Hasher\Password` In addition this class can compare strings/hashes using a time-safe method. @@ -25,8 +36,10 @@ In addition this class can compare strings/hashes using a time-safe method. [HMAC's](http://en.wikipedia.org/wiki/Hash-based_message_authentication_code) for string messages. -### Random Strings +## Contributing + +Before contributing to this project, please read the [contributing notes](CONTRIBUTING.md). + +## License -Also included is a basic random string generator in -`AngryBytes\Hash\RandomString`. It targets Unix systems with `/dev/urandom` -available for now. +Please refer to the [license file](LICENSE.md). diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..60f57aa --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,98 @@ +# Upgrade Notes + +This document lists important upgrade nodes and BC breaks per version. + +## 2.0.0 + +### Update hashing + +If you are hashing multiple values you will need to change the way their are passed to the hasher. Instead of passing +each variable separately you will need to pass them as an array. Like so: + +```php + +use AngryBytes\Hash\Hasher\Password; +use AngryBytes\Hash\Hash; + +// Create a hasher +$hasher = new Hash( + new Password() +); + +/** Old way */ + +$hash = $hasher->hash($foo, $bar, $foobar); + +/** New way */ + +$hash = $hasher->hash([ + $foo, + $bar, + $foobar +]); + +/** Old way */ + +$hash = $hasher->shortHash($foo, $bar, $foobar); + +/** New way */ + +$hash = $hasher->shortHash([ + $foo, + $bar, + $foobar +]); + +``` + +### Update hash validation + +In the previous version hash validation would be done by creating a hash and comparing it to the existing hash. +Now you can simple pass the value(s) to hash and the existing hash to a method, `verify()` or `verfiyShortHash()`. + +```php + +use AngryBytes\Hash\Hasher\Password; +use AngryBytes\Hash\Hash; + +// Create a hasher +$hasher = new Hash( + new Password() +); + +// The origin and hashed values +$value = '...' +$existingHash = '...'; + +/** Old way */ + +// Create a hash +$hash = $hasher->hash($value); + +// Validate hash +if (Hash::compare($hash, $existingHash)) { + // Hash is valid +} + +/** New way */ + +if ($hasher->verify($value, $existingHash)) { + // Hash is valid +} + + +/** Old way */ + +$hash = $hasher->shortHash($value); + +if (Hash::matchShortHash($hash, $existingHash)) { + // Hash is valid +} + +/** New way * + +if ($hasher->verifyShortHash($value, $existingHash)) { + // Hash is valid +} + +``` From 9555885acf6641cbb3dabffb2af9cecd4ddb12d9 Mon Sep 17 00:00:00 2001 From: angryThom Date: Mon, 3 Oct 2016 11:47:47 +0200 Subject: [PATCH 14/31] Fix code example --- UPGRADING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADING.md b/UPGRADING.md index 60f57aa..aa57243 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -89,7 +89,7 @@ if (Hash::matchShortHash($hash, $existingHash)) { // Hash is valid } -/** New way * +/** New way */ if ($hasher->verifyShortHash($value, $existingHash)) { // Hash is valid From 62469629e06819815b7cc9ff5af672491e377650 Mon Sep 17 00:00:00 2001 From: angryThom Date: Mon, 3 Oct 2016 12:02:49 +0200 Subject: [PATCH 15/31] Fix doc typo --- src/AngryBytes/Hash/HMAC.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AngryBytes/Hash/HMAC.php b/src/AngryBytes/Hash/HMAC.php index 88f9012..b27cdeb 100644 --- a/src/AngryBytes/Hash/HMAC.php +++ b/src/AngryBytes/Hash/HMAC.php @@ -43,6 +43,7 @@ public function __construct($algorithm) /** * Does this platform support an algorithm? * + * @param string $algorithm * @return bool **/ public static function platformSupports($algorithm) From 5c4653d64485e0eaaeeb3dac6f6f9dd1348f729c Mon Sep 17 00:00:00 2001 From: angryThom Date: Mon, 3 Oct 2016 14:11:12 +0200 Subject: [PATCH 16/31] Set php requirement and update phpunit --- composer.json | 5 +- composer.lock | 1151 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 1019 insertions(+), 137 deletions(-) diff --git a/composer.json b/composer.json index bd8dacc..c398d06 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,10 @@ "AngryBytes\\Hash": "src" } }, + "require": { + "php": "~5.6 || ~7.0" + }, "require-dev": { - "phpunit/phpunit": "3.7.x" + "phpunit/phpunit" : "~5.4.0" } } diff --git a/composer.lock b/composer.lock index 3615367..502a540 100644 --- a/composer.lock +++ b/composer.lock @@ -1,55 +1,361 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" - ], - "hash": "0469325c580a878c65a7e5f2ab661967", - "packages": [ - + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" ], + "hash": "b706f018cb6669669b76a3c5099a0a14", + "content-hash": "e8a906c6aca8a7ab6b52815b181a359a", + "packages": [], "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "myclabs/deep-copy", + "version": "1.5.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/ea74994a3dc7f8d2f65a06009348f2d63c81e61f", + "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2016-09-16 13:37:59" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-09-30 07:12:33" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-06-10 07:14:17" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-06-07 08:13:47" + }, { "name": "phpunit/php-code-coverage", - "version": "1.2.12", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "0e9958c459d675fb497d8dc5001c91d335734e48" + "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0e9958c459d675fb497d8dc5001c91d335734e48", - "reference": "0e9958c459d675fb497d8dc5001c91d335734e48", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5f3f7e736d6319d5f1fc402aff8b026da26709a3", + "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3", "shasum": "" }, "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": ">=1.3.0@stable", - "phpunit/php-text-template": ">=1.1.1@stable", - "phpunit/php-token-stream": ">=1.1.3@stable" + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "^1.4.2", + "sebastian/code-unit-reverse-lookup": "~1.0", + "sebastian/environment": "^1.3.2 || ^2.0", + "sebastian/version": "~1.0|~2.0" }, "require-dev": { - "phpunit/phpunit": "3.7.*@dev" + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "^5.4" }, "suggest": { "ext-dom": "*", - "ext-xdebug": ">=2.0.5" + "ext-xdebug": ">=2.4.0", + "ext-xmlwriter": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "4.0.x-dev" } }, "autoload": { "classmap": [ - "PHP/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], "license": [ "BSD-3-Clause" ], @@ -67,35 +373,37 @@ "testing", "xunit" ], - "time": "2013-07-06 06:26:16" + "time": "2016-07-26 14:39:29" }, { "name": "phpunit/php-file-iterator", - "version": "1.3.3", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "16a78140ed2fc01b945cfa539665fadc6a038029" + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/16a78140ed2fc01b945cfa539665fadc6a038029", - "reference": "16a78140ed2fc01b945cfa539665fadc6a038029", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", "shasum": "" }, "require": { "php": ">=5.3.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, "autoload": { "classmap": [ - "File/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], "license": [ "BSD-3-Clause" ], @@ -107,25 +415,25 @@ } ], "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "http://www.phpunit.de/", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ "filesystem", "iterator" ], - "time": "2012-10-11 11:44:38" + "time": "2015-06-21 13:08:43" }, { "name": "phpunit/php-text-template", - "version": "1.1.4", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5180896f51c5b3648ac946b05f9ec02be78a0b23" + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5180896f51c5b3648ac946b05f9ec02be78a0b23", - "reference": "5180896f51c5b3648ac946b05f9ec02be78a0b23", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", "shasum": "" }, "require": { @@ -134,20 +442,17 @@ "type": "library", "autoload": { "classmap": [ - "Text/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -156,35 +461,35 @@ "keywords": [ "template" ], - "time": "2012-10-31 18:15:28" + "time": "2015-06-21 13:50:34" }, { "name": "phpunit/php-timer", - "version": "1.0.5", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", - "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, "type": "library", "autoload": { "classmap": [ - "PHP/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], "license": [ "BSD-3-Clause" ], @@ -200,49 +505,48 @@ "keywords": [ "timer" ], - "time": "2013-08-02 07:42:54" + "time": "2016-05-12 18:03:57" }, { "name": "phpunit/php-token-stream", - "version": "1.2.0", + "version": "1.4.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "31babf400e5b5868573bf49a000a3519d3978233" + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/31babf400e5b5868573bf49a000a3519d3978233", - "reference": "31babf400e5b5868573bf49a000a3519d3978233", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", "shasum": "" }, "require": { "ext-tokenizer": "*", "php": ">=5.3.3" }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.4-dev" } }, "autoload": { "classmap": [ - "PHP/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], "description": "Wrapper around PHP's tokenizer extension.", @@ -250,63 +554,67 @@ "keywords": [ "tokenizer" ], - "time": "2013-08-04 05:57:48" + "time": "2015-09-15 10:49:45" }, { "name": "phpunit/phpunit", - "version": "3.7.24", + "version": "5.4.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "af7b77ccb5c64458bdfca95665d29558d1df7d08" + "reference": "3132365e1430c091f208e120b8845d39c25f20e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/af7b77ccb5c64458bdfca95665d29558d1df7d08", - "reference": "af7b77ccb5c64458bdfca95665d29558d1df7d08", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3132365e1430c091f208e120b8845d39c25f20e6", + "reference": "3132365e1430c091f208e120b8845d39c25f20e6", "shasum": "" }, "require": { "ext-dom": "*", + "ext-json": "*", "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", - "php": ">=5.3.3", - "phpunit/php-code-coverage": "~1.2.1", - "phpunit/php-file-iterator": ">=1.3.1", - "phpunit/php-text-template": ">=1.1.1", - "phpunit/php-timer": ">=1.0.4", - "phpunit/phpunit-mock-objects": "~1.2.0", - "symfony/yaml": "~2.0" + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "^4.0.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "^1.3 || ^2.0", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/object-enumerator": "~1.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0|~2.0", + "symfony/yaml": "~2.1|~3.0" }, - "require-dev": { - "pear-pear/pear": "1.9.4" + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" }, "suggest": { - "ext-json": "*", - "ext-simplexml": "*", - "ext-tokenizer": "*", - "phpunit/php-invoker": ">=1.1.0,<1.2.0" + "phpunit/php-invoker": "~1.1" }, "bin": [ - "composer/bin/phpunit" + "phpunit" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.7.x-dev" + "dev-master": "5.4.x-dev" } }, "autoload": { "classmap": [ - "PHPUnit/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "", - "../../symfony/yaml/" - ], "license": [ "BSD-3-Clause" ], @@ -318,45 +626,55 @@ } ], "description": "The PHP Unit Testing framework.", - "homepage": "http://www.phpunit.de/", + "homepage": "https://phpunit.de/", "keywords": [ "phpunit", "testing", "xunit" ], - "time": "2013-08-09 06:58:24" + "time": "2016-07-26 14:48:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "1.2.3", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875" + "reference": "7462c19bdb9814f6e6bdeb5cad3eb3ce72c6e0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5794e3c5c5ba0fb037b11d8151add2a07fa82875", - "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/7462c19bdb9814f6e6bdeb5cad3eb3ce72c6e0da", + "reference": "7462c19bdb9814f6e6bdeb5cad3eb3ce72c6e0da", "shasum": "" }, "require": { - "php": ">=5.3.3", - "phpunit/php-text-template": ">=1.1.1@stable" + "doctrine/instantiator": "^1.0.2", + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" }, "suggest": { "ext-soap": "*" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, "autoload": { "classmap": [ - "PHPUnit/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], "license": [ "BSD-3-Clause" ], @@ -373,67 +691,628 @@ "mock", "xunit" ], - "time": "2013-01-13 10:24:48" + "time": "2016-09-27 03:17:40" }, { - "name": "symfony/yaml", - "version": "v2.3.4", - "target-dir": "Symfony/Component/Yaml", + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/Yaml.git", - "reference": "5a279f1b5f5e1045a6c432354d9ea727ff3a9847" + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/5a279f1b5f5e1045a6c432354d9ea727ff3a9847", - "reference": "5a279f1b5f5e1045a6c432354d9ea727ff3a9847", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "psr-0": { - "Symfony\\Component\\Yaml\\": "" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Symfony Yaml Component", - "homepage": "http://symfony.com", - "time": "2013-08-24 15:26:22" - } - ], - "aliases": [ - - ], - "minimum-stability": "stable", - "stability-flags": [ - - ], - "platform": [ - - ], - "platform-dev": [ - - ] + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2016-02-13 06:45:14" + }, + { + "name": "sebastian/comparator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-07-26 15:48:44" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08 07:14:41" + }, + { + "name": "sebastian/environment", + "version": "1.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-08-18 05:49:44" + }, + { + "name": "sebastian/exporter", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-06-17 09:04:28" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/object-enumerator", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2016-01-28 13:25:10" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-11-11 19:50:13" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28 20:34:47" + }, + { + "name": "sebastian/version", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-02-04 12:56:52" + }, + { + "name": "symfony/yaml", + "version": "v3.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "f291ed25eb1435bddbe8a96caaef16469c2a092d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f291ed25eb1435bddbe8a96caaef16469c2a092d", + "reference": "f291ed25eb1435bddbe8a96caaef16469c2a092d", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-09-02 02:12:52" + }, + { + "name": "webmozart/assert", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bb2d123231c095735130cc8f6d31385a44c7b308" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308", + "reference": "bb2d123231c095735130cc8f6d31385a44c7b308", + "shasum": "" + }, + "require": { + "php": "^5.3.3|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-08-09 15:02:57" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "~5.6 || ~7.0" + }, + "platform-dev": [] } From 72d80b9c5fb171c67bf6bb67ceeedc01fc4a5e2c Mon Sep 17 00:00:00 2001 From: angryThom Date: Tue, 4 Oct 2016 16:24:44 +0200 Subject: [PATCH 17/31] Update php version for travis --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b60908f..66d4d68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: php php: - - 5.5 - - 5.4 - - 5.3 + - 7.1 + - 7.0 + - 5.6 # Notifications notifications: From 25b0e1c512c94ea425e399494b5189649527ce82 Mon Sep 17 00:00:00 2001 From: Ivar van der Burg Date: Wed, 5 Oct 2016 10:10:22 +0200 Subject: [PATCH 18/31] Update HMAC lib for a slightly improved API --- src/AngryBytes/Hash/HMAC.php | 81 +++++++++++++----------------------- 1 file changed, 30 insertions(+), 51 deletions(-) diff --git a/src/AngryBytes/Hash/HMAC.php b/src/AngryBytes/Hash/HMAC.php index b27cdeb..3e38d03 100644 --- a/src/AngryBytes/Hash/HMAC.php +++ b/src/AngryBytes/Hash/HMAC.php @@ -12,8 +12,6 @@ use \InvalidArgumentException; /** - * HMAC - * * HMAC creator * * This class will generate hashes to be used as HMAC @@ -46,41 +44,11 @@ public function __construct($algorithm) * @param string $algorithm * @return bool **/ - public static function platformSupports($algorithm) + public static function platformSupportsAlgorithm($algorithm) { return in_array($algorithm, hash_algos()); } - /** - * Get the algorithm to use - * - * @return string - */ - public function getAlgorithm() - { - return $this->algorithm; - } - - /** - * Set the algorithm to use - * - * @param string $algorithm - * @return HMAC - */ - public function setAlgorithm($algorithm) - { - // Sanity check - if (!self::platformSupports($algorithm)) { - throw new InvalidArgumentException(sprintf( - '"%s" is not a supported hash algorithm on this platform' - )); - } - - $this->algorithm = $algorithm; - - return $this; - } - /** * Create an HMAC * @@ -88,24 +56,14 @@ public function setAlgorithm($algorithm) * strings. All input will be concatenated before hashing. * * @param string $sharedSecret - * @param mixed $what1 - * @param mixed $what2 - * ... - * @param mixed $whatN + * @param array $args * @return string - **/ - public function hmac($sharedSecret) + */ + public function hmac($sharedSecret, ...$args) { - // Get data as a string of passed args - $args = func_get_args(); - - // Shift the shared secret off - array_shift($args); - // Get the data concatenated $data = ''; foreach ($args as $index => $arg) { - // Sanity check if (!is_string($arg)) { throw new InvalidArgumentException(sprintf( @@ -118,7 +76,7 @@ public function hmac($sharedSecret) } return hash_hmac( - $this->getAlgorithm(), + $this->algorithm, $data, $sharedSecret ); @@ -134,10 +92,31 @@ public function hmac($sharedSecret) **/ public function validHmac($message, $hmac, $sharedSecret) { - // The HMAC as it should be for our shared secret - $shouldHave = $this->hmac($sharedSecret, $message); + // Compare HMAC with received message + return Hash::compare( + $hmac, + // The HMAC as it should be for our shared secret + $this->hmac($sharedSecret, $message) + ); + } + + /** + * Set the algorithm to use + * + * @param string $algorithm + * @return HMAC + */ + protected function setAlgorithm($algorithm) + { + // Sanity check + if (!self::platformSupportsAlgorithm($algorithm)) { + throw new InvalidArgumentException(sprintf( + '"%s" is not a supported hash algorithm on this platform' + )); + } - // Compare - return Hash::compare($hmac, $shouldHave); + $this->algorithm = $algorithm; + + return $this; } } From 4b2b1d9c0a0365dd61d4f83d3a4a5ecfce9863a0 Mon Sep 17 00:00:00 2001 From: angryThom Date: Wed, 5 Oct 2016 12:05:37 +0200 Subject: [PATCH 19/31] PR feedback --- src/AngryBytes/Hash/Hash.php | 22 ++++++++++------------ src/AngryBytes/Hash/Hasher/Blowfish.php | 8 ++++---- src/AngryBytes/Hash/Hasher/MD5.php | 8 ++++---- src/AngryBytes/Hash/Hasher/Password.php | 19 ++++++------------- src/AngryBytes/Hash/HasherInterface.php | 8 ++++---- 5 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index 10e6140..b8a796b 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -24,7 +24,7 @@ class Hash /** * Salt for hashing * - * @var string|bool + * @var string|null **/ private $salt; @@ -41,7 +41,7 @@ class Hash * @param HasherInterface $hasher The hasher to be used * @param string|bool $salt (optional) Omit if the hasher creates its own salt **/ - public function __construct(HasherInterface $hasher, $salt = false) + public function __construct(HasherInterface $hasher, $salt = null) { $this ->setHasher($hasher) @@ -84,19 +84,17 @@ public function getSalt() /** * Set the salt * - * @param string|bool $salt + * @param string|null $salt * @return Hash */ public function setSalt($salt) { - if ($salt) { - // Make sure it's of sufficient length - if (strlen($salt) < 20) { - throw new InvalidArgumentException(sprintf( - 'Provided salt "%s" is not long enough. A minimum length of 20 characters is required', - $salt - )); - } + // Make sure it's of sufficient length + if (is_string($salt) && strlen($salt) < 20) { + throw new InvalidArgumentException(sprintf( + 'Provided salt "%s" is not long enough. A minimum length of 20 characters is required', + $salt + )); } $this->salt = $salt; @@ -196,7 +194,7 @@ public static function compare($hashA, $hashB) private function getDataString($data) { if (is_scalar($data)) { - return $data; + return (string) $data; } return serialize($data); diff --git a/src/AngryBytes/Hash/Hasher/Blowfish.php b/src/AngryBytes/Hash/Hasher/Blowfish.php index 09e56b4..a1ab108 100644 --- a/src/AngryBytes/Hash/Hasher/Blowfish.php +++ b/src/AngryBytes/Hash/Hasher/Blowfish.php @@ -81,9 +81,9 @@ public function setWorkFactor($workFactor) /** * {@inheritDoc} */ - public function hash($data, $salt) + public function hash($string, $salt) { - return crypt($data, $this->bcryptSalt($salt)); + return crypt($string, $this->bcryptSalt($salt)); } /** @@ -91,10 +91,10 @@ public function hash($data, $salt) * * @see Hash::compare() */ - public function verify($data, $hash, $salt) + public function verify($string, $hash, $salt) { return Hash::compare( - $this->hash($data, $salt), + $this->hash($string, $salt), $hash ); } diff --git a/src/AngryBytes/Hash/Hasher/MD5.php b/src/AngryBytes/Hash/Hasher/MD5.php index faa25fe..fe42870 100644 --- a/src/AngryBytes/Hash/Hasher/MD5.php +++ b/src/AngryBytes/Hash/Hasher/MD5.php @@ -32,9 +32,9 @@ class MD5 implements HasherInterface /** * {@inheritDoc} */ - public function hash($data, $salt) + public function hash($string, $salt) { - return md5($data . '-' . $salt); + return md5($string . '-' . $salt); } /** @@ -42,10 +42,10 @@ public function hash($data, $salt) * * @see Hash::compare() */ - public function verify($data, $hash, $salt) + public function verify($string, $hash, $salt) { return Hash::compare( - $this->hash($data, $salt), + $this->hash($string, $salt), $hash ); } diff --git a/src/AngryBytes/Hash/Hasher/Password.php b/src/AngryBytes/Hash/Hasher/Password.php index 9107dd8..795b5e6 100644 --- a/src/AngryBytes/Hash/Hasher/Password.php +++ b/src/AngryBytes/Hash/Hasher/Password.php @@ -27,13 +27,6 @@ */ class Password implements HasherInterface { - /** - * The hashing algorithm - * - * @var int - */ - const ALGORITHM = PASSWORD_DEFAULT; - /** * Cost factor for the algorithm * @@ -81,7 +74,7 @@ public function setCost($cost) * @throws RuntimeException If the hashing fails * @param string|bool $salt (optional) When omitted `password_hash()` will generate it's own salt */ - public function hash($data, $salt = false) + public function hash($string, $salt = false) { // Set hash options $options = []; @@ -93,9 +86,9 @@ public function hash($data, $salt = false) $options['salt'] = $salt; } - $hash = password_hash($data, self::ALGORITHM, $options); + $hash = password_hash($string, PASSWORD_DEFAULT, $options); - if (!$hash) { + if ($hash === false) { throw RuntimeException('Failed to hash password'); } @@ -107,9 +100,9 @@ public function hash($data, $salt = false) * * NOTE `$salt` is not used, `password_verify()` retrieves the used salt from the `$hash` */ - public function verify($data, $hash, $salt) + public function verify($string, $hash, $salt) { - return password_verify($data, $hash); + return password_verify($string, $hash); } /** @@ -133,7 +126,7 @@ public function needsRehash($hash, $salt = false) $options['salt'] = $salt; } - return password_needs_rehash($hash, self::ALGORITHM, $options); + return password_needs_rehash($hash, PASSWORD_DEFAULT, $options); } /** diff --git a/src/AngryBytes/Hash/HasherInterface.php b/src/AngryBytes/Hash/HasherInterface.php index 109ddf4..e394561 100644 --- a/src/AngryBytes/Hash/HasherInterface.php +++ b/src/AngryBytes/Hash/HasherInterface.php @@ -26,19 +26,19 @@ interface HasherInterface * * Implementation is supposed to salt the hashing method using $salt * - * @param string $data + * @param string $string * @param string $salt * @return string **/ - public function hash($data, $salt); + public function hash($string, $salt); /** * Verify is the data string matches the given hash * - * @param string $data + * @param string $string * @param string $hash * @param string $salt * @return bool */ - public function verify($data, $hash, $salt); + public function verify($string, $hash, $salt); } From b59b89badf1c2a3870b61249e638984c5e98757a Mon Sep 17 00:00:00 2001 From: Ivar van der Burg Date: Wed, 5 Oct 2016 12:37:22 +0200 Subject: [PATCH 20/31] Update hasher and Hash lib - port calls, preventing Hash::getHasher() - Update public API to prevent superfluous methods --- src/AngryBytes/Hash/Hash.php | 104 +++++++++++------------- src/AngryBytes/Hash/Hasher/Blowfish.php | 5 +- src/AngryBytes/Hash/Hasher/MD5.php | 6 +- src/AngryBytes/Hash/Hasher/Password.php | 65 +++++++-------- src/AngryBytes/Hash/HasherInterface.php | 4 +- 5 files changed, 87 insertions(+), 97 deletions(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index 10e6140..a28d363 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -12,10 +12,14 @@ use \InvalidArgumentException; /** - * Hash - * * A collection of hash generation and comparison methods. * + * This Hash library must receive a \AngryBytes\HasherInterface compatible + * instance to work with. + * + * Providing a salt is strictly optional, and should not be provided when the + * hasher provides better salt generation methods. + * * @category AngryBytes * @package Hash */ @@ -24,7 +28,7 @@ class Hash /** * Salt for hashing * - * @var string|bool + * @var string|null **/ private $salt; @@ -39,69 +43,34 @@ class Hash * Constructor * * @param HasherInterface $hasher The hasher to be used - * @param string|bool $salt (optional) Omit if the hasher creates its own salt + * @param string|bool $salt (optional) Omit if the hasher creates its own (better) salt **/ - public function __construct(HasherInterface $hasher, $salt = false) - { - $this - ->setHasher($hasher) - ->setSalt($salt); - } - - /** - * Get the hasher to use for the actual hashing - * - * @return HasherInterface - */ - public function getHasher() - { - return $this->hasher; - } - - /** - * Set the hasher to use for the actual hashing - * - * @param HasherInterface $hasher - * @return Hash - */ - public function setHasher(HasherInterface $hasher) + public function __construct(HasherInterface $hasher, $salt = null) { $this->hasher = $hasher; - return $this; + $this->setSalt($salt); } /** - * Get the salt + * Dynamically pass methods to the active hasher * - * @return string|bool + * @param string $method + * @param array $parameters */ - public function getSalt() + public function __call($method, $parameters) { - return $this->salt; + $this->hasher->$method(...$parameters); } /** - * Set the salt + * Get the hasher to use for the actual hashing * - * @param string|bool $salt - * @return Hash + * @return HasherInterface */ - public function setSalt($salt) + public function getHasher() { - if ($salt) { - // Make sure it's of sufficient length - if (strlen($salt) < 20) { - throw new InvalidArgumentException(sprintf( - 'Provided salt "%s" is not long enough. A minimum length of 20 characters is required', - $salt - )); - } - } - - $this->salt = $salt; - - return $this; + return $this->hasher; } /** @@ -114,9 +83,9 @@ public function setSalt($salt) **/ public function hash($data) { - return $this->getHasher()->hash( + return $this->hasher->hash( $this->getDataString($data), - $this->getSalt() + $this->salt ); } @@ -129,10 +98,10 @@ public function hash($data) */ public function verify($data, $hash) { - return $this->getHasher()->verify( + return $this->hasher->verify( $this->getDataString($data), $hash, - $this->getSalt() + $this->salt ); } @@ -148,8 +117,9 @@ public function verify($data, $hash) * * @see Hash::hash() * + * @param string $data * @return string - **/ + */ public function shortHash($data) { return substr($this->hash($data), 0, 7); @@ -186,13 +156,35 @@ public static function compare($hashA, $hashB) return hash_equals($hashA, $hashB); } + /** + * Set the salt + * + * @param string|null $salt + * @return Hash + */ + protected function setSalt($salt) + { + if (is_string($salt) && strlen($salt) < 20) { + // Make sure it's of sufficient length + throw new InvalidArgumentException(sprintf( + 'Provided salt "%s" is not long enough. A minimum length of 20 characters is required.', + $salt + )); + } + + $this->salt = $salt; + + return $this; + } + /** * Get the data as a string * * Will serialize non-scalar values * + * @param mixed $data * @return string - **/ + */ private function getDataString($data) { if (is_scalar($data)) { diff --git a/src/AngryBytes/Hash/Hasher/Blowfish.php b/src/AngryBytes/Hash/Hasher/Blowfish.php index 09e56b4..634c6ed 100644 --- a/src/AngryBytes/Hash/Hasher/Blowfish.php +++ b/src/AngryBytes/Hash/Hasher/Blowfish.php @@ -17,10 +17,11 @@ use \InvalidArgumentException; /** - * Blowfish + * Blowfish Hasher * - * Generate and verify Blowfish bcrypt/crypt() hashes using a salt + * Generate and verify Blowfish bcrypt/crypt() hashes. * + * @see AngryBytes\Hasher\Password For a password hasher * @category AngryBytes * @package Hash * @subpackage Hasher diff --git a/src/AngryBytes/Hash/Hasher/MD5.php b/src/AngryBytes/Hash/Hasher/MD5.php index faa25fe..e43cc52 100644 --- a/src/AngryBytes/Hash/Hasher/MD5.php +++ b/src/AngryBytes/Hash/Hasher/MD5.php @@ -14,15 +14,17 @@ use AngryBytes\Hash\HasherInterface; /** - * MD5 + * MD5 Hasher * - * Generate and verify MD5 hashes using a salt + * Generate and verify MD5 hashes. * * NOTE: * * This hasher MUST NOT be used for password storage. It is RECOMMENDED * to use the Hasher\Password for this purpose * + * @see AngryBytes\Hasher\Password For a password hasher + * * @category AngryBytes * @package Hash * @subpackage Hasher diff --git a/src/AngryBytes/Hash/Hasher/Password.php b/src/AngryBytes/Hash/Hasher/Password.php index 9107dd8..aea7a48 100644 --- a/src/AngryBytes/Hash/Hasher/Password.php +++ b/src/AngryBytes/Hash/Hasher/Password.php @@ -16,7 +16,7 @@ use \RuntimeException; /** - * Password hasher + * Password Hasher Using Native PHP Hash Methods * * Generate and verify hashes using the `password_*` functions. * The hashing algorithm and salting is handled by these functions. @@ -42,37 +42,13 @@ class Password implements HasherInterface private $cost; /** - * Get cost + * Password constructor. * - * @return int + * @param null|int $cost */ - public function getCost() + public function __construct($cost = null) { - return $this->cost; - } - - /** - * Set cost - * - * @throws InvalidArgumentException if the cost is too high or low - * @param int|null $cost - * @return $this - */ - public function setCost($cost) - { - if (is_null($cost)) { - // Use default cost - return $this; - } - - if ($cost < 4 || $cost > 31) { - throw new InvalidArgumentException( - 'Cost needs to be greater than 3 and smaller than 32' - ); - } - $this->cost = (int) $cost; - - return $this; + $this->setCost($cost); } /** @@ -94,9 +70,8 @@ public function hash($data, $salt = false) } $hash = password_hash($data, self::ALGORITHM, $options); - if (!$hash) { - throw RuntimeException('Failed to hash password'); + throw new RuntimeException('Failed to hash password'); } return $hash; @@ -115,7 +90,7 @@ public function verify($data, $hash, $salt) /** * Determine if the password needs to be rehashed based on the hash options * - * If true, rehashed the password after verifying it + * If true, the password should be rehashed after verification. * * @param string $hash * @param string|bool $salt (optional) When omitted `password_needs_rehash()` will generate it's own salt @@ -125,11 +100,10 @@ public function needsRehash($hash, $salt = false) { // Set hash options $options = []; - if (is_int($this->cost)) { $options['cost'] = $this->cost; } - if ($salt) { + if (is_string($salt)) { $options['salt'] = $salt; } @@ -146,5 +120,28 @@ public function getInfo($hash) { return password_get_info($hash); } + + /** + * Set cost + * + * @throws InvalidArgumentException if the cost is too high or low + * @param int|null $cost + */ + public function setCost($cost) + { + if (is_null($cost)) { + $this->cost = $cost; + + return; + } + + if ($cost < 4 || $cost > 31) { + throw new InvalidArgumentException(sprintf( + 'Cost value "%d" needs to be greater than 3 and smaller than 32', (int) $cost + )); + } + + $this->cost = (int) $cost; + } } diff --git a/src/AngryBytes/Hash/HasherInterface.php b/src/AngryBytes/Hash/HasherInterface.php index 109ddf4..6564c70 100644 --- a/src/AngryBytes/Hash/HasherInterface.php +++ b/src/AngryBytes/Hash/HasherInterface.php @@ -11,9 +11,7 @@ namespace AngryBytes\Hash; /** - * HasherInterface - * - * Interface for hashers + * Common Contract For Hashers * * @category AngryBytes * @package Hash From 2007572c75868b18a4816eb36f58b76e299babc9 Mon Sep 17 00:00:00 2001 From: Ivar van der Burg Date: Wed, 5 Oct 2016 12:57:22 +0200 Subject: [PATCH 21/31] Update password hasher interface and public API --- src/AngryBytes/Hash/Hasher/Blowfish.php | 23 +++++++--- src/AngryBytes/Hash/Hasher/MD5.php | 8 ++-- src/AngryBytes/Hash/Hasher/Password.php | 60 ++++++++++++------------- src/AngryBytes/Hash/HasherInterface.php | 8 ++-- 4 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/AngryBytes/Hash/Hasher/Blowfish.php b/src/AngryBytes/Hash/Hasher/Blowfish.php index 634c6ed..939cbd2 100644 --- a/src/AngryBytes/Hash/Hasher/Blowfish.php +++ b/src/AngryBytes/Hash/Hasher/Blowfish.php @@ -38,17 +38,24 @@ class Blowfish implements HasherInterface private $workFactor = 15; /** + * Construct + * * Detect Blowfish support * - * @throws RuntimeException - **/ - public function __construct() + * @throws \RuntimeException + * @param null|int $workFactor Override workfactor + */ + public function __construct($workFactor = null) { if (!defined("CRYPT_BLOWFISH") || CRYPT_BLOWFISH !== 1) { throw new RuntimeException( 'Blowfish hashing not available on this installation' ); } + + if (is_int($workFactor)) { + $this->setWorkFactor($workFactor); + } } /** @@ -82,9 +89,11 @@ public function setWorkFactor($workFactor) /** * {@inheritDoc} */ - public function hash($data, $salt) + public function hash($data, array $options = []) { - return crypt($data, $this->bcryptSalt($salt)); + $salt = isset($options['salt']) ? $this->bcryptSalt($options['salt']) : null; + + return crypt($data, $salt); } /** @@ -92,10 +101,10 @@ public function hash($data, $salt) * * @see Hash::compare() */ - public function verify($data, $hash, $salt) + public function verify($data, $hash, array $options = []) { return Hash::compare( - $this->hash($data, $salt), + $this->hash($data, $options), $hash ); } diff --git a/src/AngryBytes/Hash/Hasher/MD5.php b/src/AngryBytes/Hash/Hasher/MD5.php index e43cc52..9e9ab53 100644 --- a/src/AngryBytes/Hash/Hasher/MD5.php +++ b/src/AngryBytes/Hash/Hasher/MD5.php @@ -34,8 +34,10 @@ class MD5 implements HasherInterface /** * {@inheritDoc} */ - public function hash($data, $salt) + public function hash($data, array $options = []) { + $salt = isset($options['salt']) ? $options['salt'] : ''; + return md5($data . '-' . $salt); } @@ -44,10 +46,10 @@ public function hash($data, $salt) * * @see Hash::compare() */ - public function verify($data, $hash, $salt) + public function verify($data, $hash, array $options = []) { return Hash::compare( - $this->hash($data, $salt), + $this->hash($data, $options), $hash ); } diff --git a/src/AngryBytes/Hash/Hasher/Password.php b/src/AngryBytes/Hash/Hasher/Password.php index aea7a48..bdf4ee7 100644 --- a/src/AngryBytes/Hash/Hasher/Password.php +++ b/src/AngryBytes/Hash/Hasher/Password.php @@ -54,23 +54,16 @@ public function __construct($cost = null) /** * {@inheritDoc} * + * Supported options in $options array: + * - 'salt': Override the salt generated by password_hash() (discouraged) + * - 'cost': Override the default cost (not advised) + * * @throws RuntimeException If the hashing fails - * @param string|bool $salt (optional) When omitted `password_hash()` will generate it's own salt */ - public function hash($data, $salt = false) + public function hash($data, array $options = []) { - // Set hash options - $options = []; - - if (is_int($this->cost)) { - $options['cost'] = $this->cost; - } - if ($salt) { - $options['salt'] = $salt; - } - - $hash = password_hash($data, self::ALGORITHM, $options); - if (!$hash) { + $hash = password_hash($data, self::ALGORITHM, $this->parsePasswordOptions($options)); + if ($hash === false) { throw new RuntimeException('Failed to hash password'); } @@ -79,12 +72,10 @@ public function hash($data, $salt = false) /** * {@inheritDoc} - * - * NOTE `$salt` is not used, `password_verify()` retrieves the used salt from the `$hash` */ - public function verify($data, $hash, $salt) + public function verify($data, $hash, array $options = []) { - return password_verify($data, $hash); + return password_verify($data, $options); } /** @@ -93,26 +84,18 @@ public function verify($data, $hash, $salt) * If true, the password should be rehashed after verification. * * @param string $hash - * @param string|bool $salt (optional) When omitted `password_needs_rehash()` will generate it's own salt + * @param array $options Password options, @see hash() * @return bool */ - public function needsRehash($hash, $salt = false) + public function needsRehash($hash, array $options = []) { - // Set hash options - $options = []; - if (is_int($this->cost)) { - $options['cost'] = $this->cost; - } - if (is_string($salt)) { - $options['salt'] = $salt; - } - - return password_needs_rehash($hash, self::ALGORITHM, $options); + return password_needs_rehash($hash, self::ALGORITHM, $this->parsePasswordOptions($options)); } /** * Get info for the given hash * + * @see password_get_info() * @param string $hash * @return mixed[] */ @@ -143,5 +126,20 @@ public function setCost($cost) $this->cost = (int) $cost; } -} + /** + * Parse password options for hash methods + * + * @param array $options + * @return array + */ + private function parsePasswordOptions(array $options) + { + // Parse options + if (!isset($options['cost']) && is_int($this->cost)) { + $options['cost'] = $this->cost; + } + + return $options; + } +} diff --git a/src/AngryBytes/Hash/HasherInterface.php b/src/AngryBytes/Hash/HasherInterface.php index 6564c70..06c73c8 100644 --- a/src/AngryBytes/Hash/HasherInterface.php +++ b/src/AngryBytes/Hash/HasherInterface.php @@ -25,18 +25,18 @@ interface HasherInterface * Implementation is supposed to salt the hashing method using $salt * * @param string $data - * @param string $salt + * @param array $options Additional hasher options * @return string **/ - public function hash($data, $salt); + public function hash($data, array $options = []); /** * Verify is the data string matches the given hash * * @param string $data * @param string $hash - * @param string $salt + * @param array $options Additional hasher options * @return bool */ - public function verify($data, $hash, $salt); + public function verify($data, $hash, array $options = []); } From 14246b1c9d7a0bdf113df61afb76a49f6171a334 Mon Sep 17 00:00:00 2001 From: angryThom Date: Wed, 5 Oct 2016 13:36:10 +0200 Subject: [PATCH 22/31] Check salt max length --- src/AngryBytes/Hash/Hash.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index b8a796b..c9e7c92 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -74,7 +74,7 @@ public function setHasher(HasherInterface $hasher) /** * Get the salt * - * @return string|bool + * @return string|null */ public function getSalt() { @@ -90,7 +90,7 @@ public function getSalt() public function setSalt($salt) { // Make sure it's of sufficient length - if (is_string($salt) && strlen($salt) < 20) { + if (is_string($salt) && strlen($salt) < 20 && strlen($salt) < CRYPT_SALT_LENGTH) { throw new InvalidArgumentException(sprintf( 'Provided salt "%s" is not long enough. A minimum length of 20 characters is required', $salt @@ -113,7 +113,7 @@ public function setSalt($salt) public function hash($data) { return $this->getHasher()->hash( - $this->getDataString($data), + self::getDataString($data), $this->getSalt() ); } @@ -128,7 +128,7 @@ public function hash($data) public function verify($data, $hash) { return $this->getHasher()->verify( - $this->getDataString($data), + self::getDataString($data), $hash, $this->getSalt() ); @@ -191,7 +191,7 @@ public static function compare($hashA, $hashB) * * @return string **/ - private function getDataString($data) + private static function getDataString($data) { if (is_scalar($data)) { return (string) $data; From f3e8e73685eff03509d86bf1884f35e628e6b168 Mon Sep 17 00:00:00 2001 From: angryThom Date: Wed, 5 Oct 2016 15:19:48 +0200 Subject: [PATCH 23/31] Add hasher options, fix __call method --- src/AngryBytes/Hash/Hash.php | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index c8b7256..3b17f34 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -60,7 +60,7 @@ public function __construct(HasherInterface $hasher, $salt = null) */ public function __call($method, $parameters) { - $this->hasher->$method(...$parameters); + return $this->hasher->$method(...$parameters); } /** @@ -79,13 +79,14 @@ public function getHasher() * Accepts any type of variable. Non-scalar values will be serialized before hashing. * * @param mixed $data The data to hash + * @param mixed[] $options Additional hasher options * @return string **/ - public function hash($data) + public function hash($data, array $options = []) { return $this->hasher->hash( self::getDataString($data), - $this->salt + $this->parseHashOptions($options) ); } @@ -96,12 +97,12 @@ public function hash($data) * @param string $hash * @return bool */ - public function verify($data, $hash) + public function verify($data, $hash, array $options = []) { return $this->hasher->verify( self::getDataString($data), $hash, - $this->salt + $this->parseHashOptions($options) ); } @@ -118,11 +119,12 @@ public function verify($data, $hash) * @see Hash::hash() * * @param string $data + * @param mixed[] $options Additional hasher options * @return string */ - public function shortHash($data) + public function shortHash($data, array $options = []) { - return substr($this->hash($data), 0, 7); + return substr($this->hash($data, $options), 0, 7); } /** @@ -132,12 +134,13 @@ public function shortHash($data) * * @param mixed $data * @param string $shortHash + * @param mixed[] $options Additional hasher options * @return bool **/ - public function verifyShortHash($data, $shortHash) + public function verifyShortHash($data, $shortHash, array $options = []) { return self::compare( - $this->shortHash($data), + $this->shortHash($data, $options), $shortHash ); } @@ -194,4 +197,15 @@ private static function getDataString($data) return serialize($data); } + + private function parseHashOptions(array $options = []) + { + $defaultOptions = []; + + if (is_null($this->salt) === false) { + $defaultOptions['salt'] = $this->salt; + } + + return array_merge($defaultOptions, $options); + } } From 093c43461c9243bb77d86e7fc7fc899f0ca9253e Mon Sep 17 00:00:00 2001 From: angryThom Date: Wed, 5 Oct 2016 15:55:12 +0200 Subject: [PATCH 24/31] Fix unit tests --- src/AngryBytes/Hash/Hash.php | 12 ++++++++++-- tests/AngryBytes/Hash/Test/BlowfishTest.php | 20 ++++++++++++-------- tests/AngryBytes/Hash/Test/HMACTest.php | 4 +--- tests/AngryBytes/Hash/Test/MD5Test.php | 4 +--- tests/AngryBytes/Hash/Test/PasswordTest.php | 4 +--- tests/AngryBytes/Hash/Test/TestCase.php | 4 +--- 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index 3b17f34..8986e17 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -95,6 +95,7 @@ public function hash($data, array $options = []) * * @param mixed $data * @param string $hash + * @param mixed[] $options * @return bool */ public function verify($data, $hash, array $options = []) @@ -119,7 +120,7 @@ public function verify($data, $hash, array $options = []) * @see Hash::hash() * * @param string $data - * @param mixed[] $options Additional hasher options + * @param mixed[] $options * @return string */ public function shortHash($data, array $options = []) @@ -134,7 +135,7 @@ public function shortHash($data, array $options = []) * * @param mixed $data * @param string $shortHash - * @param mixed[] $options Additional hasher options + * @param mixed[] $options * @return bool **/ public function verifyShortHash($data, $shortHash, array $options = []) @@ -198,10 +199,17 @@ private static function getDataString($data) return serialize($data); } + /** + * Merge the default and provided hash options + * + * @param mixed[] $options + * @return mixed[] + */ private function parseHashOptions(array $options = []) { $defaultOptions = []; + // Pass the salt if set if (is_null($this->salt) === false) { $defaultOptions['salt'] = $this->salt; } diff --git a/tests/AngryBytes/Hash/Test/BlowfishTest.php b/tests/AngryBytes/Hash/Test/BlowfishTest.php index 1d3e8f2..70948f4 100644 --- a/tests/AngryBytes/Hash/Test/BlowfishTest.php +++ b/tests/AngryBytes/Hash/Test/BlowfishTest.php @@ -14,9 +14,7 @@ use AngryBytes\Hash\Hasher\Blowfish as BlowfishHasher; /** - * BlowfishTest - * - * Testing blowfish hasher + * Test the blowfish haser * * @category AngryBytes * @package Hash @@ -25,7 +23,7 @@ class BlowfishTest extends TestCase { /** - * Test hashing of blowfish + * Test simple string hashing * * @return void **/ @@ -45,7 +43,7 @@ public function testString() } /** - * Test complex serialized data + * Test complex serialized data hashing * * @return void **/ @@ -143,7 +141,7 @@ public function testWorkFactorTooHigh() } /** - * Test hashing of blowfish + * Test work factor alteration * * @return void **/ @@ -178,14 +176,20 @@ public function testSalt() $this->assertEquals( // Pre-generated hash outcome for password 'foo' and given salt '$2y$05$./A1aaaaaaaaaaaaaaaaaOZW9OJaO6Alj4.ZDbOi6Jrbn.bGZfYRK', - $hasher->getHasher()->hash('foo', './A1aaaaaaaaaaaaaaaaaa') + $hasher->getHasher()->hash( + 'foo', + ['salt' => './A1aaaaaaaaaaaaaaaaaa'] + ) ); // Test salt with less invalid characters $this->assertEquals( // Pre-generated hash outcome for password 'foo' and given salt (md5'ed) '$2y$05$ceb20772e0c9d240c75ebugm2AOmnuR5.LsdpDZGAjkE1DupDTPFW', - $hasher->getHasher()->hash('foo', 'salt') + $hasher->getHasher()->hash( + 'foo', + ['salt' => 'salt'] + ) ); } diff --git a/tests/AngryBytes/Hash/Test/HMACTest.php b/tests/AngryBytes/Hash/Test/HMACTest.php index e41c3d6..73bcb10 100644 --- a/tests/AngryBytes/Hash/Test/HMACTest.php +++ b/tests/AngryBytes/Hash/Test/HMACTest.php @@ -13,9 +13,7 @@ use AngryBytes\Hash\HMAC; /** - * HMACTest - * - * Testing HMAC hasher + * Test the HMAC hasher * * @category AngryBytes * @package Hash diff --git a/tests/AngryBytes/Hash/Test/MD5Test.php b/tests/AngryBytes/Hash/Test/MD5Test.php index 5a06f28..5dd9f3a 100644 --- a/tests/AngryBytes/Hash/Test/MD5Test.php +++ b/tests/AngryBytes/Hash/Test/MD5Test.php @@ -14,9 +14,7 @@ use AngryBytes\Hash\Hasher\MD5 as MD5Hasher; /** - * MD5Test - * - * Testing md5 hasher + * Test the md5 hasher * * @category AngryBytes * @package Hash diff --git a/tests/AngryBytes/Hash/Test/PasswordTest.php b/tests/AngryBytes/Hash/Test/PasswordTest.php index d52fc9f..e1da63d 100644 --- a/tests/AngryBytes/Hash/Test/PasswordTest.php +++ b/tests/AngryBytes/Hash/Test/PasswordTest.php @@ -14,9 +14,7 @@ use AngryBytes\Hash\Hasher\Password as PasswordHasher; /** - * PasswordTest - * - * Testing password hasher + * Test the password hasher * * @category AngryBytes * @package Hash diff --git a/tests/AngryBytes/Hash/Test/TestCase.php b/tests/AngryBytes/Hash/Test/TestCase.php index 25095f4..9eba27a 100644 --- a/tests/AngryBytes/Hash/Test/TestCase.php +++ b/tests/AngryBytes/Hash/Test/TestCase.php @@ -13,9 +13,7 @@ use \PHPUnit_Framework_TestCase as PUTestCase; /** - * TestCase - * - * Testing hashing + * Hashing test case * * @category AngryBytes * @package Hash From 27cd096ff123d3c53f09d9383733bfb95fb237cb Mon Sep 17 00:00:00 2001 From: angryThom Date: Wed, 5 Oct 2016 16:13:25 +0200 Subject: [PATCH 25/31] Update changelog --- CHANGELOG.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index debb399..4680402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,17 @@ hashing passwords and other secure tokens. The hasher utilises PHP's native pass `password_hash()` and `password_verify()`, see [Password Hashing](http://php.net/manual/en/book.password.php). The password hasher has been setup to use PHP's default cost and algorithm. The default cost can however be overwritten -via the `setCost()` method, like so: +by providing a cost to the the constructor, like so ```php -$hasher = new \AngryBytes\Hash\Hasher\Password; -$hasher->setCost(15); +use AngryBytes\Hash\Hasher\Password; +use AngryBytes\Hash\Hash; + +// Create a password hasher with an cost factor of 15 +$hasher = new Hash( + new Password(15) +); + ``` The password hasher offers a method to check if an existing hash needs to be rehashed. This can be the case if @@ -41,7 +47,7 @@ $passwordHash = 'password hash'; // Verify the password against the hash if ($hasher->verify($password, $passwordHash)) { // Check if the hash needs to be rehashed - if ($hasher->getHasher()->needsRehash($passwordHash)) { + if ($hasher->needsRehash($passwordHash)) { // Rehash the password $passwordHash = $hasher->hash($password); } @@ -56,7 +62,7 @@ following three arguments: * `$data` - The data that needs to be hashed. * `$hash` - The hash that needs to match the hashed value of `$data`. -* `$salt` - The salt used for hashing. +* `$options` (optional) - An array with addition hasher options. What these options are depends on the active hasher. ### Refactored AngryBytes\Hash\Hash @@ -64,23 +70,31 @@ following three arguments: handle their own salt, like `AngryBytes\Hash\Hasher\Password`. `AngryBytes\Hash\Hash::hash()` and `AngryBytes\Hash\Hash::shortHash()` no longer accept any number of arguments but -only one. This single argument can however be of any type. All non-scalar types will be serialized before hashing. +only following two: + +* `$data` - The data that needs to be hashed. This data can be of any type, all non-scalar types will be + serialized before hashing. +* `$options` (optional) - An array with options that will be passed to the hasher. What these options are depends + on the active hasher. `AngryBytes\Hash\Hash::verify()` is a new method that's available to validate a hash in a time-safe manner. -The method accepts the following two arguments: +The method accepts the following arguments: * `$data` - The data that needs to be hashed. This data can be of any type, all non-scalar types will be serialized before hashing. * `$hash` - The hash that needs to match the hashed value of `$data`. +* `$options` (optional) - An array with options that will be passed to the hasher. What these options are depends + on the active hasher. `AngryBytes\Hash\Hash::matchesShortHash()` is replaced by `AngryBytes\Hash\Hash::verifyShortHash()` this methods -accepts two arguments (`$data` and `$hash`) just like `AngryBytes\Hash\Hash::verify()`. +accepts three arguments (`$data`, `$hash` and `$options`) just like `AngryBytes\Hash\Hash::verify()`. ### Minor Changes * Scalar values passed to `hash()` and `shortHash()` are no longer serialized. * `compare()` Now uses PHP native `hash_equals()` function. * Fixed namespace issues for `AngryBytes\Hash\HMAC`. +* `AngryBytes\Hash\Hash` now implements a `__call()` method that dynamically passes methods to the active hasher. ### Upgrading From 3fef7ee6f71149118cc339a456b124bf267abe13 Mon Sep 17 00:00:00 2001 From: Ivar vd Burg Date: Thu, 6 Oct 2016 08:31:06 +0200 Subject: [PATCH 26/31] Update method --- src/AngryBytes/Hash/Hash.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index 8986e17..06b173a 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -78,7 +78,7 @@ public function getHasher() * * Accepts any type of variable. Non-scalar values will be serialized before hashing. * - * @param mixed $data The data to hash + * @param mixed $data The data to hash. This can either be a scalar value or a serializable value. * @param mixed[] $options Additional hasher options * @return string **/ @@ -93,7 +93,7 @@ public function hash($data, array $options = []) /** * Verify if the data matches the hash * - * @param mixed $data + * @param mixed $data The data to verify against the hash string. This can either be a scalar value or a serializable value. * @param string $hash * @param mixed[] $options * @return bool @@ -202,6 +202,9 @@ private static function getDataString($data) /** * Merge the default and provided hash options * + * Automatically sets the salt as an option when set in this + * component. + * * @param mixed[] $options * @return mixed[] */ @@ -210,7 +213,7 @@ private function parseHashOptions(array $options = []) $defaultOptions = []; // Pass the salt if set - if (is_null($this->salt) === false) { + if (!is_null($this->salt)) { $defaultOptions['salt'] = $this->salt; } From da27d68af0061c51b2912c01fe95bac2fd606f07 Mon Sep 17 00:00:00 2001 From: Ivar vd Burg Date: Thu, 6 Oct 2016 08:32:47 +0200 Subject: [PATCH 27/31] Docblock --- src/AngryBytes/Hash/Hash.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index 06b173a..c141f4f 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -76,10 +76,8 @@ public function getHasher() /** * Generate a hash * - * Accepts any type of variable. Non-scalar values will be serialized before hashing. - * * @param mixed $data The data to hash. This can either be a scalar value or a serializable value. - * @param mixed[] $options Additional hasher options + * @param mixed[] $options Additional hasher options (the actual options depend on the registered Hasher) * @return string **/ public function hash($data, array $options = []) From 82e78267fb6894a11d54e89e5b0ed5e13823eac4 Mon Sep 17 00:00:00 2001 From: Ivar vd Burg Date: Thu, 6 Oct 2016 08:56:06 +0200 Subject: [PATCH 28/31] Fix salt length test --- src/AngryBytes/Hash/Hash.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AngryBytes/Hash/Hash.php b/src/AngryBytes/Hash/Hash.php index c141f4f..99f1fb4 100644 --- a/src/AngryBytes/Hash/Hash.php +++ b/src/AngryBytes/Hash/Hash.php @@ -166,7 +166,7 @@ public static function compare($hashA, $hashB) */ protected function setSalt($salt) { - if (is_string($salt) && strlen($salt) < 20 && strlen($salt) > CRYPT_SALT_LENGTH) { + if (is_string($salt) && (strlen($salt) < 20 || strlen($salt) > CRYPT_SALT_LENGTH)) { // Make sure it's of sufficient length throw new InvalidArgumentException(sprintf( 'Provided salt "%s" does not match the length requirements. A length between 20 en %d characters is required.', From 2939d3fdefa794eb7533da663fb2950c1db1dce3 Mon Sep 17 00:00:00 2001 From: Ivar vd Burg Date: Thu, 6 Oct 2016 08:56:14 +0200 Subject: [PATCH 29/31] Add salt length unit test --- tests/AngryBytes/Hash/Test/BlowfishTest.php | 4 +- tests/AngryBytes/Hash/Test/HashLibTest.php | 63 +++++++++++++++++++++ tests/AngryBytes/Hash/Test/MD5Test.php | 2 +- tests/AngryBytes/Hash/Test/PasswordTest.php | 2 +- tests/AngryBytes/Hash/Test/TestCase.php | 5 +- 5 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 tests/AngryBytes/Hash/Test/HashLibTest.php diff --git a/tests/AngryBytes/Hash/Test/BlowfishTest.php b/tests/AngryBytes/Hash/Test/BlowfishTest.php index 70948f4..55bb258 100644 --- a/tests/AngryBytes/Hash/Test/BlowfishTest.php +++ b/tests/AngryBytes/Hash/Test/BlowfishTest.php @@ -14,13 +14,13 @@ use AngryBytes\Hash\Hasher\Blowfish as BlowfishHasher; /** - * Test the blowfish haser + * Test the blowfish hasher * * @category AngryBytes * @package Hash * @subpackage Test */ -class BlowfishTest extends TestCase +class BlowfishTest extends \PHPUnit_Framework_TestCase { /** * Test simple string hashing diff --git a/tests/AngryBytes/Hash/Test/HashLibTest.php b/tests/AngryBytes/Hash/Test/HashLibTest.php new file mode 100644 index 0000000..14e67a3 --- /dev/null +++ b/tests/AngryBytes/Hash/Test/HashLibTest.php @@ -0,0 +1,63 @@ + Date: Thu, 6 Oct 2016 09:46:36 +0200 Subject: [PATCH 30/31] Update docs --- CHANGELOG.md | 85 ++++++++++++++++++++++-------------------- UPGRADING.md | 102 ++++++++++++++++++++------------------------------- 2 files changed, 84 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4680402..4a87575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,61 +4,63 @@ ### New Password Hasher -Version `2.0.0` comes with the new `AngryBytes\Hash\Hasher\Password` hasher. This hasher is intended to be used for -hashing passwords and other secure tokens. The hasher utilises PHP's native password hashing functions like +Version `2.0.0` comes with a new password hasher component: `AngryBytes\Hash\Hasher\Password`. +This hasher is intended to be used for hashing of passwords (and other secure tokens). + +The hasher utilises PHP's native cryptographically strong password hashing functions: `password_hash()` and `password_verify()`, see [Password Hashing](http://php.net/manual/en/book.password.php). -The password hasher has been setup to use PHP's default cost and algorithm. The default cost can however be overwritten -by providing a cost to the the constructor, like so +The password hasher has been setup to use PHP's default cost and algorithm. +The default cost can however be overwritten by providing a cost to the the constructor, like so: ```php -use AngryBytes\Hash\Hasher\Password; -use AngryBytes\Hash\Hash; - -// Create a password hasher with an cost factor of 15 -$hasher = new Hash( - new Password(15) -); - +// Create a password hasher with n cost factor of 15 instead of the default (10). +$passwordHasher = new \AngryBytes\Hash\Hasher\Password(15); ``` -The password hasher offers a method to check if an existing hash needs to be rehashed. This can be the case if -the cost and/or algorithm of the hasher has changed. If this is the case you'll want to rehash and store your existing hashes. - -#### Example +#### Password Rehashing +The password hasher offers a method to check if an existing hash needs to be **rehashed**. +For example, this can be the case when the cost and/or algorithm of the password +hasher has changed. If this is the case, you **should** rehash the password +and update the stored hash with the rehashed value. -In this example we will check if an existing password hash needs to be rehashed, if so we'll hash the password value. -Note, in order to rehash the password you will need access to the plaintext value. It's advised to to this after -a user has successfully entered their password. +**Example** -```php +In this example, we check whether an existing password is outdated and should be rehashed. -use AngryBytes\Hash\Hasher\Password; -use AngryBytes\Hash\Hash; +Note: in order to rehash the password, you will need access to its original plaintext value. +Therefore, it's probably a best practice to check for and update a stale hash +during login procedure, where the plaintext password is available after a login form +submit. +```php // Create a password hasher -$hasher = new Hash( - new Password() +$hasher = new \AngryBytes\Hash\Hash( + new \AngryBytes\Hash\Hasher\Password ); -$password = 'plaintext password' -$passwordHash = 'password hash'; +// Plaintext password received via form submit. +$password = '...'; + +// Persisted password hash for the User +$userPasswordHash = '...'; // Verify the password against the hash -if ($hasher->verify($password, $passwordHash)) { - // Check if the hash needs to be rehashed - if ($hasher->needsRehash($passwordHash)) { - // Rehash the password - $passwordHash = $hasher->hash($password); +if ($hasher->verify($password, $userPasswordHash)) { + + // Check if the hash needs to be rehashed? + if ($hasher->needsRehash($userPasswordHash)) { + // Rehash password and update the user. + $user->hash = $hasher->hash($password); + $user->save(); } } - ``` -### Refactored AngryBytes\Hash\HasherInterface +### Refactored "AngryBytes\Hash\HasherInterface" -`AngryBytes\Hash\HasherInterface::verify()` is a new method for verify an existing hash. The method accepts the -following three arguments: +Added new verification method `AngryBytes\Hash\HasherInterface::verify()` hash +verification. This method accepts the following three arguments: * `$data` - The data that needs to be hashed. * `$hash` - The hash that needs to match the hashed value of `$data`. @@ -66,8 +68,8 @@ following three arguments: ### Refactored AngryBytes\Hash\Hash -`AngryBytes\Hash\Hash::construct()` second argument (`$salt`) has become optional to accommodate for hashers that -handle their own salt, like `AngryBytes\Hash\Hasher\Password`. +`AngryBytes\Hash\Hash::construct()` second argument (`$salt`) has become optional +to accommodate for hashers that handle their own salt, like `AngryBytes\Hash\Hasher\Password`. `AngryBytes\Hash\Hash::hash()` and `AngryBytes\Hash\Hash::shortHash()` no longer accept any number of arguments but only following two: @@ -92,9 +94,11 @@ accepts three arguments (`$data`, `$hash` and `$options`) just like `AngryBytes\ ### Minor Changes * Scalar values passed to `hash()` and `shortHash()` are no longer serialized. -* `compare()` Now uses PHP native `hash_equals()` function. +* `AngryBytes\Hash::compare()` now uses PHP native (timing attack safe) `hash_equals()` function. * Fixed namespace issues for `AngryBytes\Hash\HMAC`. -* `AngryBytes\Hash\Hash` now implements a `__call()` method that dynamically passes methods to the active hasher. +* `AngryBytes\Hash\Hash` now implements a `__call()` method that dynamically passes + methods to the active hasher. This allows you to perform calls such as `AngryBytes\Hash::hash($string, ['cost' => 15]);` + without having to call `AngryBytes\Hash::getHasher()` first. ### Upgrading @@ -102,7 +106,8 @@ Please refer to the [upgrade notes](UPGRADING.md). ### Deprecated & Removed Components -`AngryBytes\Hash\RandomString` has been removed. +* `AngryBytes\Hash\RandomString` has been removed. Better open-source random string generation + libraries are available to do this. ## 1.0.2 Valid Blowfish salts (22 char long composed of ./A-Za-z0-9 characters only) are now used as the salt as-is instead diff --git a/UPGRADING.md b/UPGRADING.md index aa57243..b87d0c6 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -4,95 +4,71 @@ This document lists important upgrade nodes and BC breaks per version. ## 2.0.0 -### Update hashing +### Update Hashing Values -If you are hashing multiple values you will need to change the way their are passed to the hasher. Instead of passing -each variable separately you will need to pass them as an array. Like so: +If you are hashing multiple values you will need to change the way their are passed +to the hasher. Instead of passing each variable separately you will need to pass +them as an array. Like so: ```php -use AngryBytes\Hash\Hasher\Password; -use AngryBytes\Hash\Hash; - -// Create a hasher -$hasher = new Hash( - new Password() +// Create a new Password Hasher +$hasher = new \AngryBytes\Hash\Hash( + new \AngryBytes\Hash\Hasher\Password ); -/** Old way */ - -$hash = $hasher->hash($foo, $bar, $foobar); - -/** New way */ +// Old +$hash = $hasher->hash('foo', 'bar', ['foo', 'bar']); +// New $hash = $hasher->hash([ - $foo, - $bar, - $foobar + 'foo', 'bar', ['foo', 'bar'] ]); - -/** Old way */ - -$hash = $hasher->shortHash($foo, $bar, $foobar); - -/** New way */ - -$hash = $hasher->shortHash([ - $foo, - $bar, - $foobar -]); - ``` -### Update hash validation - -In the previous version hash validation would be done by creating a hash and comparing it to the existing hash. -Now you can simple pass the value(s) to hash and the existing hash to a method, `verify()` or `verfiyShortHash()`. +The same principle applies to `Hash::shortHash()`. -```php +### Update Hash Validation -use AngryBytes\Hash\Hasher\Password; -use AngryBytes\Hash\Hash; +In the previous version hash validation would be done by creating a hash and comparing +it to the existing hash. Now, you can simple pass the value(s) to hash and the +existing hash to methods: `verify()` or `verfiyShortHash()`. -// Create a hasher -$hasher = new Hash( - new Password() +```php +// Create a new Password Hasher +$hasher = new \AngryBytes\Hash\Hash( + new \AngryBytes\Hash\Hasher\Password ); // The origin and hashed values -$value = '...' +$valueToHash = '...' $existingHash = '...'; -/** Old way */ - -// Create a hash -$hash = $hasher->hash($value); - -// Validate hash -if (Hash::compare($hash, $existingHash)) { - // Hash is valid -} - -/** New way */ - -if ($hasher->verify($value, $existingHash)) { - // Hash is valid -} +// Old +$isValid = \AngryBytes\Hash\Hash::compare( + $hasher->hash($valueToHash), + $existingHash +); +// New +$isValid = $hasher->verify($valueToHash, $existingHash) +``` -/** Old way */ +And for short hash comparison: -$hash = $hasher->shortHash($value); +```php +// Create a new Password Hasher +$hasher = new \AngryBytes\Hash\Hash( + new \AngryBytes\Hash\Hasher\Password +); -if (Hash::matchShortHash($hash, $existingHash)) { +// Old +if (Hash::matchShortHash($hasher->shortHash($value), $existingShortHash)) { // Hash is valid } -/** New way */ - -if ($hasher->verifyShortHash($value, $existingHash)) { +// New +if ($hasher->verifyShortHash($value, $existingShortHash)) { // Hash is valid } - ``` From 5b98eb347671dd483d32431cc7ab17d7c2b4e1fe Mon Sep 17 00:00:00 2001 From: Ivar vd Burg Date: Thu, 6 Oct 2016 09:51:58 +0200 Subject: [PATCH 31/31] Update readme --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b2dbb39..1fad1f1 100644 --- a/README.md +++ b/README.md @@ -16,20 +16,29 @@ object oriented interface to a variety of hashing methods. Installation is done via Composer: `composer require angrybytes/hash`. -## Contents +## Components ### Hash -`AngryBytes\Hash\Hash` is the main hasher class. It uses one of the `Hashers` to do the work. +`AngryBytes\Hash\Hash` is the main hasher class and acts as a helper wrapper +around hashers (i.e. `AngryBytes\Hash\HasherInterface` implementations). -Available hashers are: +Some of the main features of this component: + +* Hash strings and/or passwords. +* Create short hashes (e.g. used for identification). +* Compare strings/hashes using a time-safe method. +* Verify a string against a hash using the configured hasher. + +### Hashers + +This library comes with a set of hashers to be utilised by this hash component (or +to be used on their own): * `AngryBytes\Hash\Hasher\BlowFish` * `AngryBytes\Hash\Hasher\MD5` * `AngryBytes\Hash\Hasher\Password` -In addition this class can compare strings/hashes using a time-safe method. - ### HMAC `AngryBytes\Hash\HMAC` can be used to generate