diff --git a/src/System/Security/Hashing/Argon2IdHasher.php b/src/System/Security/Hashing/Argon2IdHasher.php new file mode 100644 index 00000000..ebe700f5 --- /dev/null +++ b/src/System/Security/Hashing/Argon2IdHasher.php @@ -0,0 +1,28 @@ + $options['memory'] ?? $this->memory, + 'time_cost' => $options['time'] ?? $this->time, + 'threads' => $options['threads'] ?? $this->threads, + ]); + + if (!is_string($hash)) { + throw new \RuntimeException(PASSWORD_ARGON2ID . ' hashing not supported.'); + } + + return $hash; + } + + public function isValidAlgorithm(string $hash): bool + { + return 'argon2id' === $this->info($hash)['algoName']; + } +} diff --git a/src/System/Security/Hashing/ArgonHasher.php b/src/System/Security/Hashing/ArgonHasher.php new file mode 100644 index 00000000..8018dd57 --- /dev/null +++ b/src/System/Security/Hashing/ArgonHasher.php @@ -0,0 +1,55 @@ +memory = $memory; + + return $this; + } + + public function setTime(int $time): self + { + $this->time = $time; + + return $this; + } + + public function setThreads(int $threads): self + { + $this->threads = $threads; + + return $this; + } + + public function make(string $value, array $options = []): string + { + $hash = @password_hash($value, PASSWORD_ARGON2I, [ + 'memory_cost' => $options['memory'] ?? $this->memory, + 'time_cost' => $options['time'] ?? $this->time, + 'threads' => $options['threads'] ?? $this->threads, + ]); + + if (!is_string($hash)) { + throw new \RuntimeException(PASSWORD_ARGON2I . ' hashing not supported.'); + } + + return $hash; + } + + public function isValidAlgorithm(string $hash): bool + { + return 'argon2i' === $this->info($hash)['algoName']; + } +} diff --git a/src/System/Security/Hashing/BcryptHasher.php b/src/System/Security/Hashing/BcryptHasher.php new file mode 100644 index 00000000..50721d7e --- /dev/null +++ b/src/System/Security/Hashing/BcryptHasher.php @@ -0,0 +1,31 @@ +rounds = $rounds; + + return $this; + } + + public function make(string $value, array $options = []): string + { + $hash = password_hash($value, PASSWORD_BCRYPT, [ + 'cost' => $options['rounds'] ?? $this->rounds, + ]); + + return $hash; + } + + public function isValidAlgorithm(string $hash): bool + { + return 'bcrypt' === $this->info($hash)['algoName']; + } +} diff --git a/src/System/Security/Hashing/DefaultHasher.php b/src/System/Security/Hashing/DefaultHasher.php new file mode 100644 index 00000000..3db8372c --- /dev/null +++ b/src/System/Security/Hashing/DefaultHasher.php @@ -0,0 +1,28 @@ +info($hash)['algoName']; + } +} diff --git a/src/System/Security/Hashing/HashInterface.php b/src/System/Security/Hashing/HashInterface.php new file mode 100644 index 00000000..bbea0e28 --- /dev/null +++ b/src/System/Security/Hashing/HashInterface.php @@ -0,0 +1,33 @@ + + */ + public function info(string $hash): array; + + /** + * Verify hash and hashed. + * + * @param array $options + */ + public function verify(string $value, string $hashed_value, array $options = []): bool; + + /** + * Hash given string. + * + * @param array $options + * + * @throws \RuntimeException + */ + public function make(string $value, array $options = []): string; + + public function isValidAlgorithm(string $hash): bool; +} diff --git a/src/System/Security/Hashing/HashManager.php b/src/System/Security/Hashing/HashManager.php new file mode 100644 index 00000000..e3451889 --- /dev/null +++ b/src/System/Security/Hashing/HashManager.php @@ -0,0 +1,61 @@ + */ + private $driver = []; + + private HashInterface $default_driver; + + public function __construct() + { + $this->setDefaultDriver(new DefaultHasher()); + } + + public function setDefaultDriver(HashInterface $driver): self + { + $this->default_driver = $driver; + + return $this; + } + + public function setDriver(string $driver_name, HashInterface $driver): self + { + $this->driver[$driver_name] = $driver; + + return $this; + } + + public function driver(?string $driver = null): HashInterface + { + if (array_key_exists($driver, $this->driver)) { + return $this->driver[$driver]; + } + + return $this->default_driver; + } + + public function info(string $hashed_value): array + { + return $this->driver()->info($hashed_value); + } + + public function make(string $value, array $options = []): string + { + return $this->driver()->make($value, $options); + } + + public function verify(string $value, string $hashed_value, array $options = []): bool + { + return $this->driver()->verify($value, $hashed_value, $options); + } + + public function isValidAlgorithm(string $hash): bool + { + return $this->driver()->isValidAlgorithm($hash); + } +} diff --git a/tests/Security/Hashing/HasherMangerTest.php b/tests/Security/Hashing/HasherMangerTest.php new file mode 100644 index 00000000..5c03e0d1 --- /dev/null +++ b/tests/Security/Hashing/HasherMangerTest.php @@ -0,0 +1,33 @@ +make('password'); + $this->assertNotSame('password', $hash); + $this->assertTrue($hasher->verify('password', $hash)); + $this->assertTrue($hasher->isValidAlgorithm($hash)); + } + + /** @test */ + public function itCanUsingDriver() + { + $hasher = new HashManager(); + $hasher->setDriver('bcrypt', new BcryptHasher()); + $hash = $hasher->driver('bcrypt')->make('password'); + $this->assertNotSame('password', $hash); + $this->assertTrue($hasher->driver('bcrypt')->verify('password', $hash)); + $this->assertTrue($hasher->driver('bcrypt')->isValidAlgorithm($hash)); + } +} diff --git a/tests/Security/Hashing/HasherTest.php b/tests/Security/Hashing/HasherTest.php new file mode 100644 index 00000000..05198953 --- /dev/null +++ b/tests/Security/Hashing/HasherTest.php @@ -0,0 +1,54 @@ +make('password'); + $this->assertNotSame('password', $hash); + $this->assertTrue($hasher->verify('password', $hash)); + $this->assertTrue($hasher->isValidAlgorithm($hash)); + } + + /** @test */ + public function itCanHashBryptHasher() + { + $hasher = new BcryptHasher(); + $hash = $hasher->make('password'); + $this->assertNotSame('password', $hash); + $this->assertTrue($hasher->verify('password', $hash)); + $this->assertTrue($hasher->isValidAlgorithm($hash)); + } + + /** @test */ + public function itCanHashArgonHasher() + { + $hasher = new ArgonHasher(); + $hash = $hasher->make('password'); + $this->assertNotSame('password', $hash); + $this->assertTrue($hasher->verify('password', $hash)); + $this->assertTrue($hasher->isValidAlgorithm($hash)); + } + + /** @test */ + public function itCanHashArgon2IdHasher() + { + $hasher = new Argon2IdHasher(); + $hash = $hasher->make('password'); + $this->assertNotSame('password', $hash); + $this->assertTrue($hasher->verify('password', $hash)); + $this->assertTrue($hasher->isValidAlgorithm($hash)); + } +}