diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..27b765f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/tests export-ignore +/.github export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0250909 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +composer.lock +*.cache +*.log +.idea/ +.DS_Store diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..32e3cbb --- /dev/null +++ b/.php_cs @@ -0,0 +1,92 @@ +setRiskyAllowed(true) + ->setRules([ + '@PSR2' => true, + '@Symfony' => true, + '@DoctrineAnnotation' => true, + '@PhpCsFixer' => true, + 'header_comment' => [ + 'commentType' => 'PHPDoc', + 'header' => $header, + 'separate' => 'none', + 'location' => 'after_declare_strict', + ], + 'array_syntax' => [ + 'syntax' => 'short' + ], + 'list_syntax' => [ + 'syntax' => 'short' + ], + 'concat_space' => [ + 'spacing' => 'one' + ], + 'blank_line_before_statement' => [ + 'statements' => [ + 'declare', + ], + ], + 'general_phpdoc_annotation_remove' => [ + 'annotations' => [ + 'author' + ], + ], + 'ordered_imports' => [ + 'imports_order' => [ + 'class', 'function', 'const', + ], + 'sort_algorithm' => 'alpha', + ], + 'single_line_comment_style' => [ + 'comment_types' => [ + ], + ], + 'yoda_style' => [ + 'always_move_variable' => false, + 'equal' => false, + 'identical' => false, + ], + 'phpdoc_align' => [ + 'align' => 'left', + ], + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line', + ], + 'constant_case' => [ + 'case' => 'lower', + ], + 'class_attributes_separation' => true, + 'combine_consecutive_unsets' => true, + 'declare_strict_types' => true, + 'linebreak_after_opening_tag' => true, + 'lowercase_static_reference' => true, + 'no_useless_else' => true, + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'not_operator_with_space' => false, + 'ordered_class_elements' => true, + 'php_unit_strict' => false, + 'phpdoc_separation' => false, + 'single_quote' => true, + 'standardize_not_equals' => true, + 'multiline_comment_opening_closing' => true, + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->exclude('bin') + ->exclude('public') + ->exclude('runtime') + ->exclude('vendor') + ->in(__DIR__) + ) + ->setUsingCache(false); diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c1ce37a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,38 @@ +language: php + +sudo: required + +matrix: + include: + - php: 7.2 + env: SW_VERSION="4.5.3RC1" + - php: 7.3 + env: SW_VERSION="4.5.3RC1" + - php: 7.4 + env: SW_VERSION="4.5.3RC1" + + allow_failures: + - php: master + +services: + - docker + +before_install: + - export PHP_MAJOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 1)" + - export PHP_MINOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 2)" + - echo $PHP_MAJOR + - echo $PHP_MINOR + +install: + - cd $TRAVIS_BUILD_DIR + - bash .travis/swoole.install.sh + - phpenv config-rm xdebug.ini || echo "xdebug not available" + - phpenv config-add .travis/ci.ini + +before_script: + - cd $TRAVIS_BUILD_DIR + - composer config -g process-timeout 900 && composer update + +script: + - composer analyse + - composer test \ No newline at end of file diff --git a/.travis/ci.ini b/.travis/ci.ini new file mode 100644 index 0000000..101b1e3 --- /dev/null +++ b/.travis/ci.ini @@ -0,0 +1,5 @@ +[opcache] +opcache.enable_cli=1 + +[swoole] +extension = "swoole.so" diff --git a/.travis/swoole.install.sh b/.travis/swoole.install.sh new file mode 100644 index 0000000..0067690 --- /dev/null +++ b/.travis/swoole.install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +wget https://github.com/swoole/swoole-src/archive/v"${SW_VERSION}".tar.gz -O swoole.tar.gz +mkdir -p swoole +tar -xf swoole.tar.gz -C swoole --strip-components=1 +rm swoole.tar.gz +cd swoole || exit +phpize +./configure --enable-openssl --enable-mysqlnd --enable-http2 +make -j "$(nproc)" +make install diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf732d0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Taylor Otwell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f49c114 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# Hyperf 哈希组件 + +该组件为存储用户密码提供了安全的 Bcrypt 和 Argon2 哈希加密方式。 + +> 移植自 [illuminate/hashing](https://github.com/illuminate/hashing )。 + +## 安装 + +```shell script +composer require hyperf-ext/hashing +``` + +## 发布配置 + +```shell script +php bin/hyperf.php vendor:publish hyperf-ext/hashing +``` + +> 配置文件位于 `config/autoload/ext-hashing.php`。 + +## 默认配置 + +```php + 'bcrypt', + 'driver' => [ + 'bcrypt' => [ + 'class' => \HyperfExt\Hashing\Driver\BcryptDriver::class, + 'rounds' => env('BCRYPT_ROUNDS', 10), + ], + 'argon' => [ + 'class' => \HyperfExt\Hashing\Driver\Argon2IDriver::class, + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + 'argon2id' => [ + 'class' => \HyperfExt\Hashing\Driver\Argon2IdDriver::class, + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + ], +]; +``` + +你可以在 `config/autoload/ext-hashing.php` 配置文件中配置默认哈希驱动程序。目前支持三种驱动程序: Bcrypt 和 Argon2(Argon2i 和 Argon2id variants)。 + +> 注意:Argon2i 驱动程序需要 PHP 7.2.0 或更高版本,而 Argon2id 驱动程序则需要 PHP 7.3.0 或更高版本。 + +## 使用 + +你可以通过 `\HyperfExt\Hashing\Hash` 类来加密你的密码: + +```php +fill([ + 'password' => Hash::make($request->input('new_password')) + ])->save(); + } +} +``` + +### 调整 Bcrypt 加密系数 + +如果使用 Bcrypt 算法,你可以在 `make` 方法中使用 `rounds` 选项来配置该算法的加密系数。然而,对大多数应用程序来说,默认值就足够了: + +```php +$hashed = Hash::make('password', [ + 'rounds' => 12 +]); +``` + +### 调整 Argon2 加密系数 + +如果使用 Argon2 算法,你可以在 `make` 方法中使用 `memory`,`time` 和 `threads` 选项来配置该算法的加密系数。然后,对大多数应用程序来说,默认值就足够了: + +```php +$hashed = Hash::make('password', [ + 'memory' => 1024, + 'time' => 2, + 'threads' => 2, +]); +``` + +> 有关这些选项的更多信息,请查阅 [PHP 官方文档](https://secure.php.net/manual/en/function.password-hash.php )。 + +### 密码哈希验证 + +`check` 方法能为您验证一段给定的未加密字符串与给定的哈希值是否一致: + +```php +if (Hash::check('plain-text', $hashedPassword)) { + // 密码匹配… +} +``` + +### 检查密码是否需要重新哈希 + +`needsRehash` 方法可以为您检查当哈希的加密系数改变时,您的密码是否被新的加密系数重新加密过: + +```php +if (Hash::needsRehash($hashed)) { + $hashed = Hash::make('plain-text'); +} +``` + +### 使用指定驱动 + +```php +$hasher = Hash::getDriver('argon2i'); +$hasher->make('plain-text'); +``` + +### 使用自定义哈希类 + +实现 `\HyperfExt\Hashing\Contract\DriverInterface` 接口,并参照配置文件中的其他算法进行配置。 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1cd9487 --- /dev/null +++ b/composer.json @@ -0,0 +1,57 @@ +{ + "name": "hyperf-ext/hashing", + "type": "library", + "license": "MIT", + "keywords": [ + "php", + "hyperf", + "hashing" + ], + "description": "The unofficial Hyperf Hashing package.", + "authors": [ + { + "name": "Eric Zhu", + "email": "eric@zhu.email" + }, + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "autoload": { + "psr-4": { + "HyperfExt\\Hashing\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "HyperfTest\\": "tests" + } + }, + "require": { + "php": ">=7.2", + "ext-swoole": ">=4.5", + "hyperf/config": "^2.0", + "hyperf/di": "^2.0", + "hyperf/framework": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.14", + "hyperf/testing": "^2.0", + "phpstan/phpstan": "^0.12", + "swoole/ide-helper": "dev-master" + }, + "config": { + "sort-packages": true + }, + "scripts": { + "test": "co-phpunit -c phpunit.xml --colors=always", + "analyse": "phpstan analyse --memory-limit 1024M -l 0 ./src", + "cs-fix": "php-cs-fixer fix $1" + }, + "extra": { + "hyperf": { + "config": "HyperfExt\\Hashing\\ConfigProvider" + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d2c615a --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,15 @@ + + + + ./tests/ + + \ No newline at end of file diff --git a/publish/ext-hashing.php b/publish/ext-hashing.php new file mode 100644 index 0000000..5b587d6 --- /dev/null +++ b/publish/ext-hashing.php @@ -0,0 +1,69 @@ + 'bcrypt', + + 'driver' => [ + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'class' => \HyperfExt\Hashing\Driver\BcryptDriver::class, + 'options'=> [ + 'rounds' => env('BCRYPT_ROUNDS', 10), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon2i' => [ + 'class' => \HyperfExt\Hashing\Driver\Argon2IDriver::class, + 'options'=> [ + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + ], + + 'argon2id' => [ + 'class' => \HyperfExt\Hashing\Driver\Argon2IdDriver::class, + 'options'=> [ + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + ], + ], + +]; diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php new file mode 100644 index 0000000..10fdbf8 --- /dev/null +++ b/src/ConfigProvider.php @@ -0,0 +1,41 @@ + [ + HashInterface::class => HashManager::class, + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'publish' => [ + [ + 'id' => 'config', + 'description' => 'The config for HyperfExt\\Hashing.', + 'source' => __DIR__ . '/../publish/ext-hashing.php', + 'destination' => BASE_PATH . '/config/autoload/ext-hashing.php', + ], + ], + ]; + } +} diff --git a/src/Contract/DriverInterface.php b/src/Contract/DriverInterface.php new file mode 100644 index 0000000..b48eaef --- /dev/null +++ b/src/Contract/DriverInterface.php @@ -0,0 +1,48 @@ +time = $options['time'] ?? $this->time; + $this->memory = $options['memory'] ?? $this->memory; + $this->threads = $options['threads'] ?? $this->threads; + $this->verifyAlgorithm = $options['verify'] ?? $this->verifyAlgorithm; + } + + /** + * Hash the given value. + * + * @param string $value + * @param array $options + * + * @return string + * + * @throws \RuntimeException + */ + public function make(string $value, array $options = []): string + { + $hash = password_hash($value, $this->algorithm(), [ + 'memory_cost' => $this->memory($options), + 'time_cost' => $this->time($options), + 'threads' => $this->threads($options), + ]); + + if ($hash === false) { + throw new RuntimeException('Argon2 hashing not supported.'); + } + + return $hash; + } + + /** + * Get the algorithm that should be used for hashing. + * + * @return int + */ + protected function algorithm() + { + return PASSWORD_ARGON2I; + } + + /** + * Check the given plain value against a hash. + * + * @param string $value + * @param string $hashedValue + * @param array $options + * + * @return bool + * + * @throws \RuntimeException + */ + public function check(string $value, string $hashedValue, array $options = []): bool + { + if ($this->verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'argon2i') { + throw new RuntimeException('This password does not use the Argon2i algorithm.'); + } + + return parent::check($value, $hashedValue, $options); + } + + /** + * Check if the given hash has been hashed using the given options. + * + * @param string $hashedValue + * @param array $options + * + * @return bool + */ + public function needsRehash(string $hashedValue, array $options = []): bool + { + return password_needs_rehash($hashedValue, $this->algorithm(), [ + 'memory_cost' => $this->memory($options), + 'time_cost' => $this->time($options), + 'threads' => $this->threads($options), + ]); + } + + /** + * Set the default password memory factor. + * + * @param int $memory + * + * @return $this + */ + public function setMemory(int $memory): self + { + $this->memory = $memory; + + return $this; + } + + /** + * Set the default password timing factor. + * + * @param int $time + * + * @return $this + */ + public function setTime(int $time): self + { + $this->time = $time; + + return $this; + } + + /** + * Set the default password threads factor. + * + * @param int $threads + * + * @return $this + */ + public function setThreads(int $threads): self + { + $this->threads = $threads; + + return $this; + } + + /** + * Extract the memory cost value from the options array. + * + * @param array $options + * + * @return int + */ + protected function memory(array $options): int + { + return $options['memory'] ?? $this->memory; + } + + /** + * Extract the time cost value from the options array. + * + * @param array $options + * + * @return int + */ + protected function time(array $options): int + { + return $options['time'] ?? $this->time; + } + + /** + * Extract the threads value from the options array. + * + * @param array $options + * + * @return int + */ + protected function threads(array $options): int + { + return $options['threads'] ?? $this->threads; + } +} diff --git a/src/Driver/Argon2IdDriver.php b/src/Driver/Argon2IdDriver.php new file mode 100644 index 0000000..fd615db --- /dev/null +++ b/src/Driver/Argon2IdDriver.php @@ -0,0 +1,44 @@ +verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'argon2id') { + throw new RuntimeException('This password does not use the Argon2id algorithm.'); + } + + if (strlen($hashedValue) === 0) { + return false; + } + + return password_verify($value, $hashedValue); + } + + /** + * Get the algorithm that should be used for hashing. + * + * @return int + */ + protected function algorithm() + { + return PASSWORD_ARGON2ID; + } +} diff --git a/src/Driver/BcryptDriver.php b/src/Driver/BcryptDriver.php new file mode 100755 index 0000000..313c65b --- /dev/null +++ b/src/Driver/BcryptDriver.php @@ -0,0 +1,122 @@ +rounds = $options['rounds'] ?? $this->rounds; + $this->verifyAlgorithm = $options['verify'] ?? $this->verifyAlgorithm; + } + + /** + * Hash the given value. + * + * @param string $value + * @param array $options + * + * @return string + * + * @throws \RuntimeException + */ + public function make(string $value, array $options = []): string + { + $hash = password_hash($value, PASSWORD_BCRYPT, [ + 'cost' => $this->cost($options), + ]); + + if ($hash === false) { + throw new RuntimeException('Bcrypt hashing not supported.'); + } + + return $hash; + } + + /** + * Check the given plain value against a hash. + * + * @param string $value + * @param string $hashedValue + * @param array $options + * + * @return bool + * + * @throws \RuntimeException + */ + public function check(string $value, string $hashedValue, array $options = []): bool + { + if ($this->verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'bcrypt') { + throw new RuntimeException('This password does not use the Bcrypt algorithm.'); + } + + return parent::check($value, $hashedValue, $options); + } + + /** + * Check if the given hash has been hashed using the given options. + * + * @param string $hashedValue + * @param array $options + * + * @return bool + */ + public function needsRehash(string $hashedValue, array $options = []): bool + { + return password_needs_rehash($hashedValue, PASSWORD_BCRYPT, [ + 'cost' => $this->cost($options), + ]); + } + + /** + * Set the default password work factor. + * + * @param int $rounds + * + * @return $this + */ + public function setRounds(int $rounds): self + { + $this->rounds = $rounds; + + return $this; + } + + /** + * Extract the cost value from the options array. + * + * @param array $options + * + * @return int + */ + protected function cost(array $options = []): int + { + return $options['rounds'] ?? $this->rounds; + } +} diff --git a/src/Hash.php b/src/Hash.php new file mode 100644 index 0000000..2485e2a --- /dev/null +++ b/src/Hash.php @@ -0,0 +1,37 @@ +get(HashInterface::class)->getDriver($name); + } + + public static function info(string $hashedValue, ?string $driverName = null): array + { + return static::getDriver($driverName)->info($hashedValue); + } + + public static function make(string $value, array $options = [], ?string $driverName = null): string + { + return static::getDriver($driverName)->make($value, $options); + } + + public static function check(string $value, string $hashedValue, array $options = [], ?string $driverName = null): bool + { + return static::getDriver($driverName)->check($value, $hashedValue, $options); + } + + public static function needsRehash(string $hashedValue, array $options = [], ?string $driverName = null): bool + { + return static::getDriver($driverName)->needsRehash($hashedValue. $options); + } +} diff --git a/src/HashManager.php b/src/HashManager.php new file mode 100644 index 0000000..5e76933 --- /dev/null +++ b/src/HashManager.php @@ -0,0 +1,114 @@ +config = $config; + } + + /** + * Get information about the given hashed value. + * + * @param string $hashedValue + * + * @return array + */ + public function info(string $hashedValue): array + { + return $this->getDriver()->info($hashedValue); + } + + /** + * Hash the given value. + * + * @param string $value + * @param array $options + * + * @return string + */ + public function make(string $value, array $options = []): string + { + return $this->getDriver()->make($value, $options); + } + + /** + * Check the given plain value against a hash. + * + * @param string $value + * @param string $hashedValue + * @param array $options + * + * @return bool + */ + public function check(string $value, string $hashedValue, array $options = []): bool + { + return $this->getDriver()->check($value, $hashedValue, $options); + } + + /** + * Check if the given hash has been hashed using the given options. + * + * @param string $hashedValue + * @param array $options + * + * @return bool + */ + public function needsRehash(string $hashedValue, array $options = []): bool + { + return $this->getDriver()->needsRehash($hashedValue, $options); + } + + /** + * Get a driver instance. + * + * @param string|null $name + * + * @return \HyperfExt\Hashing\Contract\DriverInterface + * + * @throws \InvalidArgumentException + */ + public function getDriver(?string $name = null): DriverInterface + { + if (isset($this->drivers[$name]) && $this->drivers[$name] instanceof DriverInterface) { + return $this->drivers[$name]; + } + + $name = $name ?: $this->config->get('ext-hashing.default', 'bcrypt'); + + $config = $this->config->get("ext-hashing.driver.{$name}"); + if (empty($config) or empty($config['class'])) { + throw new InvalidArgumentException(sprintf('The hashing driver config %s is invalid.', $name)); + } + + $driverClass = $config['class'] ?? BcryptDriver::class; + + $driver = make($driverClass, ['options' => $config['options'] ?? []]); + + return $this->drivers[$name] = $driver; + } +} diff --git a/tests/HashTest.php b/tests/HashTest.php new file mode 100644 index 0000000..519eb94 --- /dev/null +++ b/tests/HashTest.php @@ -0,0 +1,99 @@ +make('password'); + $this->assertNotSame('password', $value); + $this->assertTrue($hasher->check('password', $value)); + $this->assertFalse($hasher->needsRehash($value)); + $this->assertTrue($hasher->needsRehash($value, ['rounds' => 1])); + $this->assertSame('bcrypt', password_get_info($value)['algoName']); + } + + public function testBasicArgon2iHashing() + { + if (!defined('PASSWORD_ARGON2I')) { + $this->markTestSkipped('PHP not compiled with Argon2i hashing support.'); + } + + $hasher = new Argon2IDriver; + $value = $hasher->make('password'); + $this->assertNotSame('password', $value); + $this->assertTrue($hasher->check('password', $value)); + $this->assertFalse($hasher->needsRehash($value)); + $this->assertTrue($hasher->needsRehash($value, ['threads' => 1])); + $this->assertSame('argon2i', password_get_info($value)['algoName']); + } + + public function testBasicArgon2idHashing() + { + if (!defined('PASSWORD_ARGON2ID')) { + $this->markTestSkipped('PHP not compiled with Argon2id hashing support.'); + } + + $hasher = new Argon2IdDriver; + $value = $hasher->make('password'); + $this->assertNotSame('password', $value); + $this->assertTrue($hasher->check('password', $value)); + $this->assertFalse($hasher->needsRehash($value)); + $this->assertTrue($hasher->needsRehash($value, ['threads' => 1])); + $this->assertSame('argon2id', password_get_info($value)['algoName']); + } + + /** + * @depends testBasicBcryptHashing + */ + public function testBasicBcryptVerification() + { + $this->expectException(RuntimeException::class); + + if (!defined('PASSWORD_ARGON2I')) { + $this->markTestSkipped('PHP not compiled with Argon2i hashing support.'); + } + + $argonHasher = new Argon2IDriver(['verify' => true]); + $argonHashed = $argonHasher->make('password'); + (new BcryptDriver(['verify' => true]))->check('password', $argonHashed); + } + + /** + * @depends testBasicArgon2iHashing + */ + public function testBasicArgon2iVerification() + { + $this->expectException(RuntimeException::class); + + $bcryptHasher = new BcryptDriver(['verify' => true]); + $bcryptHashed = $bcryptHasher->make('password'); + (new Argon2IDriver(['verify' => true]))->check('password', $bcryptHashed); + } + + /** + * @depends testBasicArgon2idHashing + */ + public function testBasicArgon2idVerification() + { + $this->expectException(RuntimeException::class); + + $bcryptHasher = new BcryptDriver(['verify' => true]); + $bcryptHashed = $bcryptHasher->make('password'); + (new Argon2IdDriver(['verify' => true]))->check('password', $bcryptHashed); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..b4f1239 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,14 @@ +