From f803bddb718f44e9a9567e874b921191797581e9 Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Sat, 31 Aug 2024 13:59:38 +0700 Subject: [PATCH 1/3] feat: password hash --- .../Security/Hashing/Argon2IdHasher.php | 28 ++++++++++ src/System/Security/Hashing/ArgonHasher.php | 55 +++++++++++++++++++ src/System/Security/Hashing/BcryptHasher.php | 35 ++++++++++++ src/System/Security/Hashing/DefaultHasher.php | 28 ++++++++++ src/System/Security/Hashing/HashInterface.php | 16 ++++++ src/System/Security/Hashing/HashManager.php | 40 ++++++++++++++ tests/Security/Hashing/HasherMangerTest.php | 23 ++++++++ tests/Security/Hashing/HasherTest.php | 54 ++++++++++++++++++ 8 files changed, 279 insertions(+) create mode 100644 src/System/Security/Hashing/Argon2IdHasher.php create mode 100644 src/System/Security/Hashing/ArgonHasher.php create mode 100644 src/System/Security/Hashing/BcryptHasher.php create mode 100644 src/System/Security/Hashing/DefaultHasher.php create mode 100644 src/System/Security/Hashing/HashInterface.php create mode 100644 src/System/Security/Hashing/HashManager.php create mode 100644 tests/Security/Hashing/HasherMangerTest.php create mode 100644 tests/Security/Hashing/HasherTest.php 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..89e64fb1 --- /dev/null +++ b/src/System/Security/Hashing/BcryptHasher.php @@ -0,0 +1,35 @@ +rounds = $rounds; + + return $this; + } + + public function make(string $value, array $options = []): string + { + $hash = password_hash($value, PASSWORD_BCRYPT, [ + 'cost' => $options['rounds'] ?? $this->rounds, + ]); + + if (false === $hash) { + throw new \RuntimeException('Bcrypt hashing not supported.'); + } + + 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..68a5bd70 --- /dev/null +++ b/src/System/Security/Hashing/HashInterface.php @@ -0,0 +1,16 @@ +driver = $driver; + } + + private function driver(?string $driver = null): HashInterface + { + return $driver ?? $this->driver ?? new DefaultHasher(); + } + + 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..dafd64cd --- /dev/null +++ b/tests/Security/Hashing/HasherMangerTest.php @@ -0,0 +1,23 @@ +setDriver(new BcryptHasher()); + $hash = $hasher->make('password'); + $this->assertNotSame('password', $hash); + $this->assertTrue($hasher->verify('password', $hash)); + $this->assertTrue($hasher->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)); + } +} From ab93f9890394aa0281ece41245f99509117aa72c Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Sat, 31 Aug 2024 14:15:33 +0700 Subject: [PATCH 2/3] formatting (fix phpstan) --- src/System/Security/Hashing/BcryptHasher.php | 4 ---- src/System/Security/Hashing/HashInterface.php | 17 +++++++++++++++++ src/System/Security/Hashing/HashManager.php | 4 +++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/System/Security/Hashing/BcryptHasher.php b/src/System/Security/Hashing/BcryptHasher.php index 89e64fb1..50721d7e 100644 --- a/src/System/Security/Hashing/BcryptHasher.php +++ b/src/System/Security/Hashing/BcryptHasher.php @@ -21,10 +21,6 @@ public function make(string $value, array $options = []): string 'cost' => $options['rounds'] ?? $this->rounds, ]); - if (false === $hash) { - throw new \RuntimeException('Bcrypt hashing not supported.'); - } - return $hash; } diff --git a/src/System/Security/Hashing/HashInterface.php b/src/System/Security/Hashing/HashInterface.php index 68a5bd70..bbea0e28 100644 --- a/src/System/Security/Hashing/HashInterface.php +++ b/src/System/Security/Hashing/HashInterface.php @@ -6,10 +6,27 @@ interface HashInterface { + /** + * Get information about hash. + * + * @return array + */ 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 index 6fa58669..8026e332 100644 --- a/src/System/Security/Hashing/HashManager.php +++ b/src/System/Security/Hashing/HashManager.php @@ -8,9 +8,11 @@ class HashManager implements HashInterface { private ?HashInterface $driver = null; - public function setDriver(HashInterface $driver) + public function setDriver(HashInterface $driver): self { $this->driver = $driver; + + return $this; } private function driver(?string $driver = null): HashInterface From f91fd57b96e443fb5a6f67ed0aeb3ef513e96e98 Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Tue, 3 Sep 2024 15:26:25 +0700 Subject: [PATCH 3/3] feat: hash driver register --- src/System/Security/Hashing/HashManager.php | 29 +++++++++++++++++---- tests/Security/Hashing/HasherMangerTest.php | 12 ++++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/System/Security/Hashing/HashManager.php b/src/System/Security/Hashing/HashManager.php index 8026e332..e3451889 100644 --- a/src/System/Security/Hashing/HashManager.php +++ b/src/System/Security/Hashing/HashManager.php @@ -6,18 +6,37 @@ class HashManager implements HashInterface { - private ?HashInterface $driver = null; + /** @var array */ + private $driver = []; - public function setDriver(HashInterface $driver): self + 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; + $this->driver[$driver_name] = $driver; return $this; } - private function driver(?string $driver = null): HashInterface + public function driver(?string $driver = null): HashInterface { - return $driver ?? $this->driver ?? new DefaultHasher(); + if (array_key_exists($driver, $this->driver)) { + return $this->driver[$driver]; + } + + return $this->default_driver; } public function info(string $hashed_value): array diff --git a/tests/Security/Hashing/HasherMangerTest.php b/tests/Security/Hashing/HasherMangerTest.php index dafd64cd..5c03e0d1 100644 --- a/tests/Security/Hashing/HasherMangerTest.php +++ b/tests/Security/Hashing/HasherMangerTest.php @@ -14,10 +14,20 @@ class HasherMangerTest extends TestCase public function itCanHashDefaultHasher() { $hasher = new HashManager(); - $hasher->setDriver(new BcryptHasher()); $hash = $hasher->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)); + } }