From e9e52d7d3e7648dfb31b1454546de97b4ca17d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=BChne?= Date: Tue, 8 May 2018 13:31:20 +0200 Subject: [PATCH 1/2] Add class for Argon2i --- src/Password/Argon2i.php | 195 ++++++++++++++++++++++++++++++++++ test/Password/Argon2iTest.php | 137 ++++++++++++++++++++++++ 2 files changed, 332 insertions(+) create mode 100644 src/Password/Argon2i.php create mode 100644 test/Password/Argon2iTest.php diff --git a/src/Password/Argon2i.php b/src/Password/Argon2i.php new file mode 100644 index 0000000..d766323 --- /dev/null +++ b/src/Password/Argon2i.php @@ -0,0 +1,195 @@ + $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 Argon2 hash + * + * @return int + */ + public function getMemoryCost() + { + return $this->memoryCost; + } + + /** + * Sets the maximum memory (in kibibytes) that may be used to compute the Argon2 hash + * + * @param int $memoryCost + * + * @return Argon2i + */ + public function setMemoryCost($memoryCost) + { + $this->memoryCost = $memoryCost; + return $this; + } + + /** + * Returns the maximum amount of time it may take to compute the Argon2 hash + * + * @return int + */ + public function getTimeCost() + { + return $this->timeCost; + } + + /** + * Sets the maximum amount of time it may take to compute the Argon2 hash + * + * @param int $timeCost + * + * @return Argon2i + */ + public function setTimeCost($timeCost) + { + $this->timeCost = $timeCost; + return $this; + } + + /** + * Returns the number of threads to use for computing the Argon2 hash + * + * @return int + */ + public function getThreads() + { + return $this->threads; + } + + /** + * Sets the number of threads to use for computing the Argon2 hash + * + * @param int $threads + * + * @return Argon2i + */ + public function setThreads($threads) + { + $this->threads = $threads; + return $this; + } + + /** + * @param string $password + * @throws Exception\RuntimeException + * @return string + */ + public function create($password) + { + $options = []; + + if ($this->memoryCost !== null) { + $options['memory_cost'] = (int) $this->memoryCost; + } + + if ($this->timeCost !== null) { + $options['time_cost'] = (int) $this->timeCost; + } + + if ($this->threads !== null) { + $options['threads'] = (int) $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)); + } +} From 013d10bca68256483c0eb9755bbd16dff091f65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=BChne?= Date: Tue, 17 Jul 2018 11:34:23 +0200 Subject: [PATCH 2/2] Validate parameters on setter instead of during use --- src/Password/Argon2i.php | 53 ++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/Password/Argon2i.php b/src/Password/Argon2i.php index d766323..92b6b47 100644 --- a/src/Password/Argon2i.php +++ b/src/Password/Argon2i.php @@ -14,6 +14,8 @@ use const PHP_VERSION_ID; use function is_array; +use function is_numeric; +use function is_scalar; use function password_hash; use function password_verify; use function strtolower; @@ -24,21 +26,21 @@ class Argon2i implements PasswordInterface { /** - * Maximum memory (in kibibytes) that may be used to compute the Argon2 hash + * Maximum memory (in kibibytes) that may be used to compute the Argon2i hash * * @var int|null */ protected $memoryCost; /** - * Maximum amount of time it may take to compute the Argon2 hash + * Maximum amount of time it may take to compute the Argon2i hash * * @var int|null */ protected $timeCost; /** - * Number of threads to use for computing the Argon2 hash + * Number of threads to use for computing the Argon2i hash * * @var int|null */ @@ -89,7 +91,7 @@ public function __construct($options = []) } /** - * Returns the maximum memory (in kibibytes) that may be used to compute the Argon2 hash + * Returns the maximum memory (in kibibytes) that may be used to compute the Argon2i hash * * @return int */ @@ -99,20 +101,25 @@ public function getMemoryCost() } /** - * Sets the maximum memory (in kibibytes) that may be used to compute the Argon2 hash + * Sets the maximum memory (in kibibytes) that may be used to compute the Argon2i hash * * @param int $memoryCost * - * @return Argon2i + * @return self */ public function setMemoryCost($memoryCost) { - $this->memoryCost = $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 Argon2 hash + * Returns the maximum amount of time it may take to compute the Argon2i hash * * @return int */ @@ -122,20 +129,25 @@ public function getTimeCost() } /** - * Sets the maximum amount of time it may take to compute the Argon2 hash + * Sets the maximum amount of time it may take to compute the Argon2i hash * * @param int $timeCost * - * @return Argon2i + * @return self */ public function setTimeCost($timeCost) { - $this->timeCost = $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 Argon2 hash + * Returns the number of threads to use for computing the Argon2i hash * * @return int */ @@ -145,15 +157,20 @@ public function getThreads() } /** - * Sets the number of threads to use for computing the Argon2 hash + * Sets the number of threads to use for computing the Argon2i hash * * @param int $threads * - * @return Argon2i + * @return self */ public function setThreads($threads) { - $this->threads = $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; } @@ -167,15 +184,15 @@ public function create($password) $options = []; if ($this->memoryCost !== null) { - $options['memory_cost'] = (int) $this->memoryCost; + $options['memory_cost'] = $this->memoryCost; } if ($this->timeCost !== null) { - $options['time_cost'] = (int) $this->timeCost; + $options['time_cost'] = $this->timeCost; } if ($this->threads !== null) { - $options['threads'] = (int) $this->threads; + $options['threads'] = $this->threads; } return password_hash($password, PASSWORD_ARGON2I, $options);