diff --git a/.travis.yml b/.travis.yml index 7d48e602f13e..42d9dbb7088b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ sudo: false cache: directories: - $HOME/.composer/cache + - $HOME/libsodium services: - memcached @@ -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: diff --git a/src/Illuminate/Hashing/Argon2Hasher.php b/src/Illuminate/Hashing/Argon2Hasher.php new file mode 100644 index 000000000000..1ea41a015269 --- /dev/null +++ b/src/Illuminate/Hashing/Argon2Hasher.php @@ -0,0 +1,90 @@ + $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'); + } +} diff --git a/src/Illuminate/Hashing/HashServiceProvider.php b/src/Illuminate/Hashing/HashServiceProvider.php index 85581f40cf65..10b99223d1b3 100755 --- a/src/Illuminate/Hashing/HashServiceProvider.php +++ b/src/Illuminate/Hashing/HashServiceProvider.php @@ -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; + } }); } diff --git a/tests/Hashing/Argon2HasherTest.php b/tests/Hashing/Argon2HasherTest.php new file mode 100644 index 000000000000..4b1779b35a8d --- /dev/null +++ b/tests/Hashing/Argon2HasherTest.php @@ -0,0 +1,54 @@ +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); + } +}