Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Commit

Permalink
Merge 5c57cb9 into d67dd45
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthiasKuehneEllerhold committed May 9, 2018
2 parents d67dd45 + 5c57cb9 commit b149e80
Show file tree
Hide file tree
Showing 2 changed files with 334 additions and 0 deletions.
197 changes: 197 additions & 0 deletions src/Password/Argon2i.php
@@ -0,0 +1,197 @@
<?php
/**
* @see https://github.com/zendframework/zend-crypt for the canonical source repository
* @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-crypt/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Crypt\Password;

use Traversable;
use Zend\Stdlib\ArrayUtils;

use const PASSWORD_ARGON2I;
use const PHP_VERSION_ID;

use function is_array;
use function password_hash;
use function password_verify;
use function strtolower;

/**
* Argon2i algorithm using password_hash() function of PHP
*/
class Argon2i implements PasswordInterface
{
/**
* Maximum memory (in kibibytes) that may be used to compute the Argon2 hash
*
* @var int|null
*/
protected $memoryCost;

/**
* Maximum amount of time it may take to compute the Argon2 hash
*
* @var int|null
*/
protected $timeCost;

/**
* Number of threads to use for computing the Argon2 hash
*
* @var int|null
*/
protected $threads;

/**
* @var string
*/
protected $salt;

/**
* Constructor
*
* @param array|Traversable $options
* @throws Exception\InvalidArgumentException
*/
public function __construct($options = [])
{
if (PHP_VERSION_ID < 70200) {
throw new Exception\InvalidArgumentException(
'The Argon2i password hash is only nativly available on PHP7.2+'
);
}

if (! empty($options)) {
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
}

if (! is_array($options)) {
throw new Exception\InvalidArgumentException(
'The options parameter must be an array or a Traversable'
);
}

foreach ($options as $key => $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);
}
}
137 changes: 137 additions & 0 deletions test/Password/Argon2iTest.php
@@ -0,0 +1,137 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/

namespace ZendTest\Crypt\Password;

use ArrayObject;
use PHPUnit\Framework\TestCase;
use Zend\Crypt\Password\Argon2i;
use Zend\Crypt\Password\Exception;

/**
* @group Zend_Crypt
*/
class Argon2iTest extends TestCase
{
public function testConstruct()
{
if (PHP_VERSION_ID < 70200) {
$this->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));
}
}

0 comments on commit b149e80

Please sign in to comment.