Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sudo: false
cache:
directories:
- $HOME/.composer/cache
- $HOME/libsodium

services:
- memcached
Expand All @@ -31,6 +32,11 @@ before_install:
- phpenv config-rm xdebug.ini || true
- echo "extension = memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- printf "\n" | pecl install -f redis
- sudo apt-get install -y software-properties-common
- sudo LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
- sudo apt-get update
- sudo apt-get install -y libsodium-dev
- pecl install -f libsodium
- travis_retry composer self-update

install:
Expand Down
90 changes: 90 additions & 0 deletions src/Illuminate/Hashing/Argon2Hasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace Illuminate\Hashing;

use InvalidArgumentException;
use RuntimeException;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;

class Argon2Hasher implements HasherContract
{
/**
* Hash the given value.
*
* @param string $value
* @param array $options
* @return string
*
* @throws \RuntimeException
*/
public function make($value, array $options = []): string
{
if (extension_loaded('sodium')) {
return sodium_crypto_pwhash_str(
$value,
$options['time_cost'] ?? SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
$options['memory_cost'] ?? SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
}

throw new RuntimeException('Argon2i hashing not supported.');
}

/**
* Check a plain text value against a hashed value.
*
* @param string $value
* @param string $hashedValue
* @param array $options
* @return bool
*
* @throws \RuntimeException
*/
public function check($value, $hashedValue, array $options = []): bool
{
if (extension_loaded('sodium')) {
$valid = sodium_crypto_pwhash_str_verify($hashedValue, $value);
sodium_memzero($value);
return $valid;
}

throw new RuntimeException('Argon2i hashing not supported.');
}

/**
* Check if the given hash has been hashed using the given options.
*
* @param string $hashedValue
* @param array $options
* @return bool
*
* @throws \RuntimeException
*/
public function needsRehash($hashedValue, array $options = []): bool
{
// Extract options from the hashed value
list($memoryCost, $timeCost) = sscanf($hashedValue, '$%*[argon2id]$v=%*ld$m=%d,t=%d');
$hashOptions = ['memory_cost' => $memoryCost, 'time_cost' => $timeCost];

if (empty(array_filter($hashOptions))) {
throw new InvalidArgumentException('Supplied hash is not a valid Argon2 hash');
}

// Filter unknown options from the options array
$options = array_filter($options, function ($key) use ($hashOptions) {
return isset($hashOptions[$key]);
}, ARRAY_FILTER_USE_KEY);

return ! empty(array_diff_assoc($options, $hashOptions));
}

/**
* Determine if the system supports Argon2i hashing.
*
* @return bool
*/
public function isSupported(): bool
{
return extension_loaded('sodium');
}
}
9 changes: 8 additions & 1 deletion src/Illuminate/Hashing/HashServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ class HashServiceProvider extends ServiceProvider
public function register()
{
$this->app->singleton('hash', function () {
return new BcryptHasher;
switch (config('hash.algorithm')) {
case 'argon2':
case 'argon2i':
return new Argon2Hasher;
case 'bcrypt':
default:
return new BcryptHasher;
}
});
}

Expand Down
54 changes: 54 additions & 0 deletions tests/Hashing/Argon2HasherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Illuminate\Tests\Hashing;

use PHPUnit\Framework\TestCase;
use Illuminate\Hashing\Argon2Hasher;

class Argon2HasherTest extends TestCase
{
const PLAINTEXT_PASSWORD = 'password';

public function setUp()
{
if (! (new Argon2Hasher)->isSupported()) {
$this->markTestSkipped('Argon2i hashing not supported.');
}
}

public function testHashPassword()
{
$hasher = new Argon2Hasher;
$hashedPassword = $hasher->make(self::PLAINTEXT_PASSWORD);

$this->assertNotSame(self::PLAINTEXT_PASSWORD, $hashedPassword);
$this->assertStringStartsWith(SODIUM_CRYPTO_PWHASH_STRPREFIX, $hashedPassword);
}

public function testVerifyPassword()
{
$hasher = new Argon2Hasher;
$hashedPassword = $hasher->make(self::PLAINTEXT_PASSWORD);

$this->assertTrue($hasher->check(self::PLAINTEXT_PASSWORD, $hashedPassword));
$this->assertFalse($hasher->check(strrev(self::PLAINTEXT_PASSWORD), $hashedPassword));
}

public function testNeedsRehash()
{
$hasher = new Argon2Hasher;
$hashedPassword = $hasher->make(self::PLAINTEXT_PASSWORD);

$this->assertFalse($hasher->needsRehash($hashedPassword));
$this->assertTrue($hasher->needsRehash($hashedPassword, ['time_cost' => 1]));
$this->assertTrue($hasher->needsRehash($hashedPassword, ['memory_cost' => 1]));
}

public function testNeedsRehashThrowsInvalidArgumentException()
{
$this->expectException(\InvalidArgumentException::class);

$hasher = new Argon2Hasher;
$hasher->needsRehash(self::PLAINTEXT_PASSWORD);
}
}