diff --git a/src/Password/Argon2i.php b/src/Password/Argon2i.php new file mode 100644 index 0000000..92b6b47 --- /dev/null +++ b/src/Password/Argon2i.php @@ -0,0 +1,212 @@ + $value) { + switch (strtolower($key)) { + case 'memory_cost': + $this->setMemoryCost($value); + break; + case 'time_cost': + $this->setTimeCost($value); + break; + case 'threads': + $this->setThreads($value); + break; + } + } + } + + /** + * Returns the maximum memory (in kibibytes) that may be used to compute the Argon2i hash + * + * @return int + */ + public function getMemoryCost() + { + return $this->memoryCost; + } + + /** + * Sets the maximum memory (in kibibytes) that may be used to compute the Argon2i hash + * + * @param int $memoryCost + * + * @return self + */ + public function setMemoryCost($memoryCost) + { + if (!is_scalar($memoryCost) || !is_numeric($memoryCost) || ($memoryCost < 0)) { + throw new Exception\InvalidArgumentException( + 'The memory cost parameter of argon2i must be an integer greater 0' + ); + } + $this->memoryCost = (int) $memoryCost; + return $this; + } + + /** + * Returns the maximum amount of time it may take to compute the Argon2i hash + * + * @return int + */ + public function getTimeCost() + { + return $this->timeCost; + } + + /** + * Sets the maximum amount of time it may take to compute the Argon2i hash + * + * @param int $timeCost + * + * @return self + */ + public function setTimeCost($timeCost) + { + if (!is_scalar($timeCost) || !is_numeric($timeCost) || ($timeCost < 0)) { + throw new Exception\InvalidArgumentException( + 'The time cost parameter of argon2i must be an integer greater 0' + ); + } + $this->timeCost = (int) $timeCost; + return $this; + } + + /** + * Returns the number of threads to use for computing the Argon2i hash + * + * @return int + */ + public function getThreads() + { + return $this->threads; + } + + /** + * Sets the number of threads to use for computing the Argon2i hash + * + * @param int $threads + * + * @return self + */ + public function setThreads($threads) + { + if (!is_scalar($threads) || !is_numeric($threads) || ($threads < 0)) { + throw new Exception\InvalidArgumentException( + 'The thread-count parameter of argon2i must be an integer greater 0' + ); + } + $this->threads = (int) $threads; + return $this; + } + + /** + * @param string $password + * @throws Exception\RuntimeException + * @return string + */ + public function create($password) + { + $options = []; + + if ($this->memoryCost !== null) { + $options['memory_cost'] = $this->memoryCost; + } + + if ($this->timeCost !== null) { + $options['time_cost'] = $this->timeCost; + } + + if ($this->threads !== null) { + $options['threads'] = $this->threads; + } + + return password_hash($password, PASSWORD_ARGON2I, $options); + } + + /** + * Verify if a password is correct against a hash value + * + * @param string $password + * @param string $hash + * @return bool + */ + public function verify($password, $hash) + { + return password_verify($password, $hash); + } +} diff --git a/test/Password/Argon2iTest.php b/test/Password/Argon2iTest.php new file mode 100644 index 0000000..f815438 --- /dev/null +++ b/test/Password/Argon2iTest.php @@ -0,0 +1,137 @@ +expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('The Argon2i password hash is only nativly available on PHP7.2+'); + } + + $argon2i = new Argon2i(); + + // This will only be executed on PHP7.2+ + $this->assertEquals(PASSWORD_ARGON2_DEFAULT_MEMORY_COST, $argon2i->getMemoryCost()); + $this->assertEquals(PASSWORD_ARGON2_DEFAULT_TIME_COST, $argon2i->getTimeCost()); + $this->assertEquals(PASSWORD_ARGON2_DEFAULT_THREADS, $argon2i->getThreads()); + } + + /** + * @requires PHP 7.2 + */ + public function testConstructByOptions() + { + $options = [ + 'memory_cost' => 512, + 'time_cost' => 5, + 'threads' => 1, + ]; + $argon2i = new Argon2i($options); + $this->assertEquals(512, $argon2i->getMemoryCost()); + $this->assertEquals(5, $argon2i->getTimeCost()); + $this->assertEquals(1, $argon2i->getThreads()); + } + + /** + * This test uses ArrayObject to simulate a Zend\Config\Config instance; + * the class itself only tests for Traversable. + * + * @requires PHP 7.2 + */ + public function testConstructByConfig() + { + $options = [ + 'memory_cost' => 512, + 'time_cost' => 5, + 'threads' => 1, + ]; + $config = new ArrayObject($options); + $argon2i = new Argon2i($config); + $this->assertEquals(512, $argon2i->getMemoryCost()); + $this->assertEquals(5, $argon2i->getTimeCost()); + $this->assertEquals(1, $argon2i->getThreads()); + } + + /** + * @requires PHP 7.2 + */ + public function testWrongConstruct() + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('The options parameter must be an array or a Traversable'); + new Argon2i('test'); + } + + /** + * @requires PHP 7.2 + */ + public function testSetMemoryCost() + { + $argon2i = new Argon2i(); + $argon2i->setMemoryCost(512); + $this->assertEquals(512, $argon2i->getMemoryCost()); + } + + /** + * @requires PHP 7.2 + */ + public function testSetTimeCost() + { + $argon2i = new Argon2i(); + $argon2i->setTimeCost(10); + $this->assertEquals(10, $argon2i->getTimeCost()); + } + + /** + * @requires PHP 7.2 + */ + public function testCreate() + { + $argon2i = new Argon2i(); + $password = $argon2i->create('test'); + $this->assertNotEmpty($password); + $this->assertGreaterThanOrEqual(95, strlen($password)); + } + + /** + * @requires PHP 7.2 + */ + public function testVerify() + { + $argon2i = new Argon2i(); + $hash = '$argon2i$v=19$m=1024,t=2,p=2$eFlUWVhOMWRjY2swVW1neQ$QASXjIL0nEMfep30FGefpWPdh0wISQpljW3FYPy5m0U'; + $this->assertTrue($argon2i->verify('test', $hash)); + $this->assertFalse($argon2i->verify('other', $hash)); + } + + /** + * The class should also positivly verify bcrypt hashes + * + * @requires PHP 7.2 + */ + public function testVerifyBcryptHashes() + { + $argon2i = new Argon2i(); + $hash = '$2y$10$123456789012345678901uIcehzOq0s9RvVtyXJFIsuuxuE2XZRMq'; + $this->assertTrue($argon2i->verify('test', $hash)); + $this->assertFalse($argon2i->verify('other', $hash)); + } +}