Skip to content

Commit f7cc6ee

Browse files
committed
Added Argon2 hash support
1 parent d610151 commit f7cc6ee

4 files changed

Lines changed: 150 additions & 1 deletion

File tree

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ sudo: false
2222
cache:
2323
directories:
2424
- $HOME/.composer/cache
25+
- $HOME/libsodium
2526

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

3642
install:
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace Illuminate\Hashing;
4+
5+
use InvalidArgumentException;
6+
use RuntimeException;
7+
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
8+
9+
class Argon2Hasher implements HasherContract
10+
{
11+
/**
12+
* Hash the given value.
13+
*
14+
* @param string $value
15+
* @param array $options
16+
* @return string
17+
*
18+
* @throws \RuntimeException
19+
*/
20+
public function make($value, array $options = []): string
21+
{
22+
if (extension_loaded('sodium')) {
23+
return sodium_crypto_pwhash_str(
24+
$value,
25+
$options['time_cost'] ?? SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
26+
$options['memory_cost'] ?? SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
27+
);
28+
}
29+
30+
throw new RuntimeException('Argon2i hashing not supported.');
31+
}
32+
33+
/**
34+
* Check a plain text value against a hashed value.
35+
*
36+
* @param string $value
37+
* @param string $hashedValue
38+
* @param array $options
39+
* @return bool
40+
*
41+
* @throws \RuntimeException
42+
*/
43+
public function check($value, $hashedValue, array $options = []): bool
44+
{
45+
if (extension_loaded('sodium')) {
46+
$valid = sodium_crypto_pwhash_str_verify($hashedValue, $value);
47+
sodium_memzero($value);
48+
return $valid;
49+
}
50+
51+
throw new RuntimeException('Argon2i hashing not supported.');
52+
}
53+
54+
/**
55+
* Check if the given hash has been hashed using the given options.
56+
*
57+
* @param string $hashedValue
58+
* @param array $options
59+
* @return bool
60+
*
61+
* @throws \RuntimeException
62+
*/
63+
public function needsRehash($hashedValue, array $options = []): bool
64+
{
65+
// Extract options from the hashed value
66+
list($memoryCost, $timeCost) = sscanf($hashedValue, '$%*[argon2id]$v=%*ld$m=%d,t=%d');
67+
$hashOptions = ['memory_cost' => $memoryCost, 'time_cost' => $timeCost];
68+
69+
if (empty(array_filter($hashOptions))) {
70+
throw new InvalidArgumentException('Supplied hash is not a valid Argon2 hash');
71+
}
72+
73+
// Filter unknown options from the options array
74+
$options = array_filter($options, function ($key) use ($hashOptions) {
75+
return isset($hashOptions[$key]);
76+
}, ARRAY_FILTER_USE_KEY);
77+
78+
return ! empty(array_diff_assoc($options, $hashOptions));
79+
}
80+
81+
/**
82+
* Determine if the system supports Argon2i hashing.
83+
*
84+
* @return bool
85+
*/
86+
public function isSupported(): bool
87+
{
88+
return extension_loaded('sodium');
89+
}
90+
}

src/Illuminate/Hashing/HashServiceProvider.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ class HashServiceProvider extends ServiceProvider
2121
public function register()
2222
{
2323
$this->app->singleton('hash', function () {
24-
return new BcryptHasher;
24+
switch (config('hash.algorithm')) {
25+
case 'argon2':
26+
case 'argon2i':
27+
return new Argon2Hasher;
28+
case 'bcrypt':
29+
default:
30+
return new BcryptHasher;
31+
}
2532
});
2633
}
2734

tests/Hashing/Argon2HasherTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Hashing;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Illuminate\Hashing\Argon2Hasher;
7+
8+
class Argon2HasherTest extends TestCase
9+
{
10+
const PLAINTEXT_PASSWORD = 'password';
11+
12+
public function setUp()
13+
{
14+
if (! (new Argon2Hasher)->isSupported()) {
15+
$this->markTestSkipped('Argon2i hashing not supported.');
16+
}
17+
}
18+
19+
public function testHashPassword()
20+
{
21+
$hasher = new Argon2Hasher;
22+
$hashedPassword = $hasher->make(self::PLAINTEXT_PASSWORD);
23+
24+
$this->assertNotSame(self::PLAINTEXT_PASSWORD, $hashedPassword);
25+
$this->assertStringStartsWith(SODIUM_CRYPTO_PWHASH_STRPREFIX, $hashedPassword);
26+
}
27+
28+
public function testVerifyPassword()
29+
{
30+
$hasher = new Argon2Hasher;
31+
$hashedPassword = $hasher->make(self::PLAINTEXT_PASSWORD);
32+
33+
$this->assertTrue($hasher->check(self::PLAINTEXT_PASSWORD, $hashedPassword));
34+
$this->assertFalse($hasher->check(strrev(self::PLAINTEXT_PASSWORD), $hashedPassword));
35+
}
36+
37+
public function testNeedsRehash()
38+
{
39+
$hasher = new Argon2Hasher;
40+
$hashedPassword = $hasher->make(self::PLAINTEXT_PASSWORD);
41+
42+
$this->assertFalse($hasher->needsRehash($hashedPassword));
43+
$this->assertTrue($hasher->needsRehash($hashedPassword, ['time_cost' => 1]));
44+
$this->assertTrue($hasher->needsRehash($hashedPassword, ['memory_cost' => 1]));
45+
}
46+
}

0 commit comments

Comments
 (0)