From f803bddb718f44e9a9567e874b921191797581e9 Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Sat, 31 Aug 2024 13:59:38 +0700 Subject: [PATCH] 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)); + } +}