From b063a2317c48cc6f3dba1eab0298641b19accdcd Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 9 Mar 2025 23:50:59 +0100 Subject: [PATCH 01/11] Add getName() method to Hash classes Implement a getName() method for all hash algorithm classes to provide a consistent way of identifying the specific hashing algorithm being used - Add abstract getName() method to base Hash class - Implement getName() for each hash algorithm (Argon2, Bcrypt, MD5, PHPass, Plaintext, Scrypt, ScryptModified, Sha) - Add corresponding test cases to verify the method returns the correct algorithm name --- src/Auth/Hash.php | 7 +++++++ src/Auth/Hashes/Argon2.php | 8 ++++++++ src/Auth/Hashes/Bcrypt.php | 8 ++++++++ src/Auth/Hashes/MD5.php | 8 ++++++++ src/Auth/Hashes/PHPass.php | 8 ++++++++ src/Auth/Hashes/Plaintext.php | 8 ++++++++ src/Auth/Hashes/Scrypt.php | 8 ++++++++ src/Auth/Hashes/ScryptModified.php | 8 ++++++++ src/Auth/Hashes/Sha.php | 8 ++++++++ tests/Auth/Algorithms/Argon2Test.php | 5 +++++ tests/Auth/Algorithms/BcryptTest.php | 5 +++++ tests/Auth/Algorithms/MD5Test.php | 5 +++++ tests/Auth/Algorithms/PHPassTest.php | 5 +++++ tests/Auth/Algorithms/PlaintextTest.php | 5 +++++ tests/Auth/Algorithms/ScryptModifiedTest.php | 5 +++++ tests/Auth/Algorithms/ScryptTest.php | 5 +++++ tests/Auth/Algorithms/ShaTest.php | 5 +++++ 17 files changed, 111 insertions(+) diff --git a/src/Auth/Hash.php b/src/Auth/Hash.php index 82b8d9b..f7270b4 100644 --- a/src/Auth/Hash.php +++ b/src/Auth/Hash.php @@ -61,4 +61,11 @@ abstract public function hash(string $value): string; * @return bool */ abstract public function verify(string $value, string $hash): bool; + + /** + * Get the name of the hash algorithm + * + * @return string + */ + abstract public function getName(): string; } diff --git a/src/Auth/Hashes/Argon2.php b/src/Auth/Hashes/Argon2.php index 09b4ed4..5fea928 100644 --- a/src/Auth/Hashes/Argon2.php +++ b/src/Auth/Hashes/Argon2.php @@ -88,4 +88,12 @@ public function setThreads(int $threads): self return $this; } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'argon2'; + } } diff --git a/src/Auth/Hashes/Bcrypt.php b/src/Auth/Hashes/Bcrypt.php index 3b55fab..804a576 100644 --- a/src/Auth/Hashes/Bcrypt.php +++ b/src/Auth/Hashes/Bcrypt.php @@ -48,4 +48,12 @@ public function setCost(int $cost): self return $this; } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'bcrypt'; + } } diff --git a/src/Auth/Hashes/MD5.php b/src/Auth/Hashes/MD5.php index 301f337..d2fff06 100644 --- a/src/Auth/Hashes/MD5.php +++ b/src/Auth/Hashes/MD5.php @@ -21,4 +21,12 @@ public function verify(string $value, string $hash): bool { return $this->hash($value) === $hash; } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'md5'; + } } diff --git a/src/Auth/Hashes/PHPass.php b/src/Auth/Hashes/PHPass.php index b70d7b1..acf8ec6 100644 --- a/src/Auth/Hashes/PHPass.php +++ b/src/Auth/Hashes/PHPass.php @@ -253,4 +253,12 @@ public function setPortableHashes(bool $portable): PHPass return $this; } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'phpass'; + } } diff --git a/src/Auth/Hashes/Plaintext.php b/src/Auth/Hashes/Plaintext.php index 531bebe..35c54db 100644 --- a/src/Auth/Hashes/Plaintext.php +++ b/src/Auth/Hashes/Plaintext.php @@ -21,4 +21,12 @@ public function verify(string $value, string $hash): bool { return $this->hash($value) === $hash; } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'plaintext'; + } } diff --git a/src/Auth/Hashes/Scrypt.php b/src/Auth/Hashes/Scrypt.php index 901fc0b..92607b7 100644 --- a/src/Auth/Hashes/Scrypt.php +++ b/src/Auth/Hashes/Scrypt.php @@ -139,4 +139,12 @@ public function setSalt(string $salt): self return $this; } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'scrypt'; + } } diff --git a/src/Auth/Hashes/ScryptModified.php b/src/Auth/Hashes/ScryptModified.php index db4a9dd..e7ab78e 100644 --- a/src/Auth/Hashes/ScryptModified.php +++ b/src/Auth/Hashes/ScryptModified.php @@ -164,4 +164,12 @@ public function setSignerKey(string $key): self return $this; } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'scryptMod'; + } } diff --git a/src/Auth/Hashes/Sha.php b/src/Auth/Hashes/Sha.php index 52d01c8..371ec4f 100644 --- a/src/Auth/Hashes/Sha.php +++ b/src/Auth/Hashes/Sha.php @@ -86,4 +86,12 @@ public function verify(string $value, string $hash): bool { return $this->hash($value) === $hash; } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'sha'; + } } diff --git a/tests/Auth/Algorithms/Argon2Test.php b/tests/Auth/Algorithms/Argon2Test.php index 8690601..e952aa4 100644 --- a/tests/Auth/Algorithms/Argon2Test.php +++ b/tests/Auth/Algorithms/Argon2Test.php @@ -76,4 +76,9 @@ public function testValidThreads(): void $hash = $this->argon2->hash($password); $this->assertTrue($this->argon2->verify($password, $hash)); } + + public function testGetName(): void + { + $this->assertEquals('argon2', $this->argon2->getName()); + } } diff --git a/tests/Auth/Algorithms/BcryptTest.php b/tests/Auth/Algorithms/BcryptTest.php index 5168b9c..880ac63 100644 --- a/tests/Auth/Algorithms/BcryptTest.php +++ b/tests/Auth/Algorithms/BcryptTest.php @@ -25,4 +25,9 @@ public function testHash(): void $this->assertTrue($this->bcrypt->verify($password, $hash)); $this->assertFalse($this->bcrypt->verify('wrongpassword', $hash)); } + + public function testGetName(): void + { + $this->assertEquals('bcrypt', $this->bcrypt->getName()); + } } diff --git a/tests/Auth/Algorithms/MD5Test.php b/tests/Auth/Algorithms/MD5Test.php index ccba042..48d0094 100644 --- a/tests/Auth/Algorithms/MD5Test.php +++ b/tests/Auth/Algorithms/MD5Test.php @@ -64,4 +64,9 @@ public function testUnicodeCharacters(): void $this->assertEquals(md5($password), $hash); $this->assertTrue($this->md5->verify($password, $hash)); } + + public function testGetName(): void + { + $this->assertEquals('md5', $this->md5->getName()); + } } diff --git a/tests/Auth/Algorithms/PHPassTest.php b/tests/Auth/Algorithms/PHPassTest.php index 7b052de..5a83d7b 100644 --- a/tests/Auth/Algorithms/PHPassTest.php +++ b/tests/Auth/Algorithms/PHPassTest.php @@ -82,4 +82,9 @@ public function testLongPassword(): void $hash = $this->phpass->hash($password); $this->assertTrue($this->phpass->verify($password, $hash)); } + + public function testGetName(): void + { + $this->assertEquals('phpass', $this->phpass->getName()); + } } diff --git a/tests/Auth/Algorithms/PlaintextTest.php b/tests/Auth/Algorithms/PlaintextTest.php index acdcb57..87635ea 100644 --- a/tests/Auth/Algorithms/PlaintextTest.php +++ b/tests/Auth/Algorithms/PlaintextTest.php @@ -52,4 +52,9 @@ public function testEmptyString(): void $this->assertEquals($password, $hash); $this->assertTrue($this->plaintext->verify($password, $hash)); } + + public function testGetName(): void + { + $this->assertEquals('plaintext', $this->plaintext->getName()); + } } diff --git a/tests/Auth/Algorithms/ScryptModifiedTest.php b/tests/Auth/Algorithms/ScryptModifiedTest.php index 7354a18..fe52133 100644 --- a/tests/Auth/Algorithms/ScryptModifiedTest.php +++ b/tests/Auth/Algorithms/ScryptModifiedTest.php @@ -36,4 +36,9 @@ public function testCustomOptions(): void $this->assertTrue($this->scryptModified->verify($password, $hash)); } + + public function testGetName(): void + { + $this->assertEquals('scryptMod', $this->scryptModified->getName()); + } } diff --git a/tests/Auth/Algorithms/ScryptTest.php b/tests/Auth/Algorithms/ScryptTest.php index 6b9b606..7bae12f 100644 --- a/tests/Auth/Algorithms/ScryptTest.php +++ b/tests/Auth/Algorithms/ScryptTest.php @@ -38,4 +38,9 @@ public function testCustomOptions(): void $this->assertTrue($this->scrypt->verify($password, $hash)); } + + public function testGetName(): void + { + $this->assertEquals('scrypt', $this->scrypt->getName()); + } } diff --git a/tests/Auth/Algorithms/ShaTest.php b/tests/Auth/Algorithms/ShaTest.php index ebcef15..2955924 100644 --- a/tests/Auth/Algorithms/ShaTest.php +++ b/tests/Auth/Algorithms/ShaTest.php @@ -39,4 +39,9 @@ public function testInvalidVersion(): void $this->expectException(\InvalidArgumentException::class); $this->sha->setVersion('invalid-version'); } + + public function testGetName(): void + { + $this->assertEquals('sha', $this->sha->getName()); + } } From ada77726740eb7180a4cee762cdf0c1beb0dc3a3 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 16 Mar 2025 17:32:38 +0100 Subject: [PATCH 02/11] Updated the store class --- src/Auth/Store.php | 38 +++++++++++++++++++++++---- tests/StoreTest.php | 62 ++++++++++++++++++++++++++++++--------------- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/src/Auth/Store.php b/src/Auth/Store.php index 2ae6766..ccab76d 100644 --- a/src/Auth/Store.php +++ b/src/Auth/Store.php @@ -10,31 +10,59 @@ class Store protected array $data = []; /** - * Get a value from the store + * @var string|null + */ + protected ?string $key = null; + + /** + * Get a property from the store * * @param string $key * @param mixed $default * @return mixed */ - public function get(string $key, mixed $default = null): mixed + public function getProperty(string $key, mixed $default = null): mixed { return $this->data[$key] ?? $default; } /** - * Set a value in the store + * Set a property in the store * * @param string $key * @param mixed $value * @return self */ - public function set(string $key, mixed $value): self + public function setProperty(string $key, mixed $value): self { $this->data[$key] = $value; return $this; } + /** + * Get the store key + * + * @return string|null + */ + public function getKey(): ?string + { + return $this->key; + } + + /** + * Set the store key + * + * @param string|null $key + * @return self + */ + public function setKey(?string $key): self + { + $this->key = $key; + + return $this; + } + /** * Encode store data to base64 string * @@ -66,7 +94,7 @@ public function decode(string $data): self $json = json_decode($decoded, true, 512, JSON_THROW_ON_ERROR); if (is_array($json)) { foreach ($json as $key => $value) { - $this->set($key, $value); + $this->setProperty($key, $value); } } } catch (\JsonException $e) { diff --git a/tests/StoreTest.php b/tests/StoreTest.php index ade98f9..9b1c1b4 100644 --- a/tests/StoreTest.php +++ b/tests/StoreTest.php @@ -7,28 +7,49 @@ class StoreTest extends TestCase { - public function testGetAndSet(): void + public function testGetAndSetProperty(): void { $store = new Store(); // Test setting and getting a string - $store->set('name', 'John Doe'); - $this->assertEquals('John Doe', $store->get('name')); + $store->setProperty('name', 'John Doe'); + $this->assertEquals('John Doe', $store->getProperty('name')); // Test setting and getting different types - $store->set('age', 30) - ->set('active', true) - ->set('scores', [95, 87, 92]) - ->set('details', ['city' => 'New York', 'country' => 'USA']); + $store->setProperty('age', 30) + ->setProperty('active', true) + ->setProperty('scores', [95, 87, 92]) + ->setProperty('details', ['city' => 'New York', 'country' => 'USA']); - $this->assertEquals(30, $store->get('age')); - $this->assertTrue($store->get('active')); - $this->assertEquals([95, 87, 92], $store->get('scores')); - $this->assertEquals(['city' => 'New York', 'country' => 'USA'], $store->get('details')); + $this->assertEquals(30, $store->getProperty('age')); + $this->assertTrue($store->getProperty('active')); + $this->assertEquals([95, 87, 92], $store->getProperty('scores')); + $this->assertEquals(['city' => 'New York', 'country' => 'USA'], $store->getProperty('details')); // Test default value for non-existent key - $this->assertNull($store->get('nonexistent')); - $this->assertEquals('default', $store->get('nonexistent', 'default')); + $this->assertNull($store->getProperty('nonexistent')); + $this->assertEquals('default', $store->getProperty('nonexistent', 'default')); + } + + public function testGetAndSetKey(): void + { + $store = new Store(); + + // Test initial key is null + $this->assertNull($store->getKey()); + + // Test setting and getting a key + $store->setKey('test-key'); + $this->assertEquals('test-key', $store->getKey()); + + // Test setting key to null + $store->setKey(null); + $this->assertNull($store->getKey()); + + // Test method chaining + $store->setKey('new-key')->setProperty('test', 'value'); + $this->assertEquals('new-key', $store->getKey()); + $this->assertEquals('value', $store->getProperty('test')); } public function testEncodeAndDecode(): void @@ -42,10 +63,11 @@ public function testEncodeAndDecode(): void 'details' => ['city' => 'New York', 'country' => 'USA'], ]; - // Set multiple values + // Set multiple values and key foreach ($data as $key => $value) { - $store->set($key, $value); + $store->setProperty($key, $value); } + $store->setKey('test-key'); // Encode the store $encoded = $store->encode(); @@ -61,7 +83,7 @@ public function testEncodeAndDecode(): void // Verify all data was preserved foreach ($data as $key => $value) { - $this->assertEquals($value, $newStore->get($key)); + $this->assertEquals($value, $store->getProperty($key)); } } @@ -71,23 +93,23 @@ public function testDecodeInvalidData(): void // Test decoding invalid base64 $store->decode('invalid-base64'); - $this->assertNull($store->get('any')); + $this->assertNull($store->getProperty('any')); // Test decoding valid base64 but invalid JSON $store->decode(base64_encode('invalid-json')); - $this->assertNull($store->get('any')); + $this->assertNull($store->getProperty('any')); // Test decoding valid base64 and JSON, but not an array $json = json_encode('string', JSON_THROW_ON_ERROR); $store->decode(base64_encode($json)); - $this->assertNull($store->get('any')); + $this->assertNull($store->getProperty('any')); } public function testEncodeWithInvalidData(): void { $store = new Store(); // Create an invalid UTF-8 string that will cause json_encode to fail - $store->set('invalid', "\xFF"); + $store->setProperty('invalid', "\xFF"); $this->expectException(\JsonException::class); $store->encode(); From 966fbfefb27be94e3363f07279787d5cf8a66b95 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 16 Mar 2025 19:32:00 +0100 Subject: [PATCH 03/11] Removed validators --- src/Auth/Hashes/Argon2.php | 12 ------------ tests/Auth/Algorithms/Argon2Test.php | 18 ------------------ 2 files changed, 30 deletions(-) diff --git a/src/Auth/Hashes/Argon2.php b/src/Auth/Hashes/Argon2.php index 5fea928..ffb8e3e 100644 --- a/src/Auth/Hashes/Argon2.php +++ b/src/Auth/Hashes/Argon2.php @@ -42,10 +42,6 @@ public function verify(string $value, string $hash): bool */ public function setMemoryCost(int $cost): self { - if ($cost < PASSWORD_ARGON2_DEFAULT_MEMORY_COST) { - throw new \InvalidArgumentException('Memory cost must be >= '.PASSWORD_ARGON2_DEFAULT_MEMORY_COST.' KiB'); - } - $this->setOption('memory_cost', $cost); return $this; @@ -61,10 +57,6 @@ public function setMemoryCost(int $cost): self */ public function setTimeCost(int $cost): self { - if ($cost < PASSWORD_ARGON2_DEFAULT_TIME_COST) { - throw new \InvalidArgumentException('Time cost must be >= '.PASSWORD_ARGON2_DEFAULT_TIME_COST); - } - $this->setOption('time_cost', $cost); return $this; @@ -80,10 +72,6 @@ public function setTimeCost(int $cost): self */ public function setThreads(int $threads): self { - if ($threads < PASSWORD_ARGON2_DEFAULT_THREADS) { - throw new \InvalidArgumentException('Threads must be >= '.PASSWORD_ARGON2_DEFAULT_THREADS); - } - $this->setOption('threads', $threads); return $this; diff --git a/tests/Auth/Algorithms/Argon2Test.php b/tests/Auth/Algorithms/Argon2Test.php index e952aa4..b9d46d4 100644 --- a/tests/Auth/Algorithms/Argon2Test.php +++ b/tests/Auth/Algorithms/Argon2Test.php @@ -26,12 +26,6 @@ public function testHash(): void $this->assertFalse($this->argon2->verify('wrongpassword', $hash)); } - public function testMemoryCost(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->argon2->setMemoryCost(1); // Should throw exception for too low memory cost - } - public function testValidMemoryCost(): void { $cost = PASSWORD_ARGON2_DEFAULT_MEMORY_COST + 1024; @@ -43,12 +37,6 @@ public function testValidMemoryCost(): void $this->assertTrue($this->argon2->verify($password, $hash)); } - public function testTimeCost(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->argon2->setTimeCost(0); // Should throw exception for too low time cost - } - public function testValidTimeCost(): void { $cost = PASSWORD_ARGON2_DEFAULT_TIME_COST + 1; @@ -60,12 +48,6 @@ public function testValidTimeCost(): void $this->assertTrue($this->argon2->verify($password, $hash)); } - public function testThreads(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->argon2->setThreads(0); // Should throw exception for too low thread count - } - public function testValidThreads(): void { $threads = PASSWORD_ARGON2_DEFAULT_THREADS + 1; From 8708ce2ccdeb937327dca1129d81726e74c13b2d Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 17 Mar 2025 16:41:12 +0100 Subject: [PATCH 04/11] Small fixes --- src/Auth/Hash.php | 19 ++++++- src/Auth/Hashes/Argon2.php | 8 +-- src/Auth/Proofs/Password.php | 28 ++++++++++- tests/Auth/HashTest.php | 81 ++++++++++++++++++++++++++++++ tests/Auth/Proofs/PasswordTest.php | 20 +------- 5 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 tests/Auth/HashTest.php diff --git a/src/Auth/Hash.php b/src/Auth/Hash.php index f7270b4..b86481a 100644 --- a/src/Auth/Hash.php +++ b/src/Auth/Hash.php @@ -16,13 +16,28 @@ abstract class Hash * @param mixed $value The value to set for the option * @return self */ - protected function setOption(string $key, mixed $value): self + public function setOption(string $key, mixed $value): self { $this->options[$key] = $value; return $this; } + /** + * Set multiple hashing options at once + * + * @param array $options Array of options to set + * @return self + */ + public function setOptions(array $options): self + { + foreach ($options as $key => $value) { + $this->setOption($key, $value); + } + + return $this; + } + /** * Get a specific option value * @@ -30,7 +45,7 @@ protected function setOption(string $key, mixed $value): self * @param mixed $default Default value if option doesn't exist * @return mixed The option value or default if not found */ - protected function getOption(string $key, mixed $default = null): mixed + public function getOption(string $key, mixed $default = null): mixed { return $this->options[$key] ?? $default; } diff --git a/src/Auth/Hashes/Argon2.php b/src/Auth/Hashes/Argon2.php index ffb8e3e..3c15e7d 100644 --- a/src/Auth/Hashes/Argon2.php +++ b/src/Auth/Hashes/Argon2.php @@ -11,8 +11,8 @@ class Argon2 extends Hash */ public function __construct() { - $this->setOption('memory_cost', 65536); - $this->setOption('time_cost', 4); + $this->setOption('memoryCost', 65536); + $this->setOption('timeCost', 4); $this->setOption('threads', 3); } @@ -42,7 +42,7 @@ public function verify(string $value, string $hash): bool */ public function setMemoryCost(int $cost): self { - $this->setOption('memory_cost', $cost); + $this->setOption('memoryCost', $cost); return $this; } @@ -57,7 +57,7 @@ public function setMemoryCost(int $cost): self */ public function setTimeCost(int $cost): self { - $this->setOption('time_cost', $cost); + $this->setOption('timeCost', $cost); return $this; } diff --git a/src/Auth/Proofs/Password.php b/src/Auth/Proofs/Password.php index d873d81..0262bbc 100644 --- a/src/Auth/Proofs/Password.php +++ b/src/Auth/Proofs/Password.php @@ -60,7 +60,7 @@ public function __construct(array $hashes = []) } $this->hashes = $hashes; - $this->hash = reset($hashes); // Set the first hash as the default one + $this->hash = new Argon2(); // Set the first hash as the default one } /** @@ -171,4 +171,30 @@ public function generate(): string return $password; } + + /** + * Create a hash instance by type + * + * @param string $type One of the supported hash types (ARGON2, BCRYPT, SCRYPT, SCRYPT_MODIFIED, SHA, MD5, PHPASS) + * @param array $options Optional parameters for hash configuration + * @return Hash + * @throws \Exception + */ + public static function createHash(string $type, array $options = []): Hash + { + $hash = match ($type) { + self::ARGON2 => new Argon2(), + self::BCRYPT => new Bcrypt(), + self::SCRYPT => new Scrypt(), + self::SCRYPT_MODIFIED => new ScryptModified(), + self::SHA => new Sha(), + self::MD5 => new MD5(), + self::PHPASS => new PHPass(), + default => throw new \Exception("Unsupported hash type: {$type}") + }; + + $hash->setOptions($options); + + return $hash; + } } diff --git a/tests/Auth/HashTest.php b/tests/Auth/HashTest.php new file mode 100644 index 0000000..7c20b9f --- /dev/null +++ b/tests/Auth/HashTest.php @@ -0,0 +1,81 @@ +hash = new class extends Hash { + public function hash(string $value): string + { + return 'hashed_' . $value; + } + + public function verify(string $value, string $hash): bool + { + return $hash === 'hashed_' . $value; + } + + public function getName(): string + { + return 'test_hash'; + } + }; + } + + public function testSetAndGetOption(): void + { + $this->hash->setOption('key1', 'value1'); + $this->assertEquals('value1', $this->hash->getOption('key1')); + $this->assertNull($this->hash->getOption('nonexistent')); + $this->assertEquals('default', $this->hash->getOption('nonexistent', 'default')); + } + + public function testSetOptions(): void + { + $options = [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => ['nested' => 'value'] + ]; + + $this->hash->setOptions($options); + + // Verify all options were set + $this->assertEquals($options, $this->hash->getOptions()); + + // Verify individual options + foreach ($options as $key => $value) { + $this->assertEquals($value, $this->hash->getOption($key)); + } + } + + public function testGetOptions(): void + { + $options = [ + 'key1' => 'value1', + 'key2' => 'value2' + ]; + + $this->hash->setOptions($options); + $this->assertEquals($options, $this->hash->getOptions()); + } + + public function testMethodChaining(): void + { + $result = $this->hash + ->setOption('key1', 'value1') + ->setOptions(['key2' => 'value2']); + + $this->assertInstanceOf(Hash::class, $result); + $this->assertEquals('value1', $this->hash->getOption('key1')); + $this->assertEquals('value2', $this->hash->getOption('key2')); + } +} \ No newline at end of file diff --git a/tests/Auth/Proofs/PasswordTest.php b/tests/Auth/Proofs/PasswordTest.php index e39c641..f50cdb5 100644 --- a/tests/Auth/Proofs/PasswordTest.php +++ b/tests/Auth/Proofs/PasswordTest.php @@ -16,8 +16,6 @@ class PasswordTest extends TestCase { protected Password $password; - protected Password $legacyPassword; - protected Bcrypt $bcrypt; protected function setUp(): void @@ -27,7 +25,6 @@ protected function setUp(): void // Test legacy constructor with explicit hashes $this->bcrypt = new Bcrypt(); - $this->legacyPassword = new Password(['bcrypt' => $this->bcrypt]); } public function testGenerate(): void @@ -122,7 +119,7 @@ public function testRemoveHash(): void { // First try to remove the current hash (should fail) $this->expectException(\Exception::class); - $this->password->removeHash(Password::ARGON2); // Argon2 is the default current hash + $this->password->removeHash('random-hash'); // Argon2 is the default current hash } public function testRemoveNonCurrentHash(): void @@ -165,19 +162,4 @@ public function testAllHashesWork(): void $this->assertFalse($this->password->verify('wrongpassword', $hash), "Hash {$algo} failed wrong password test"); } } - - public function testLegacyConstructor(): void - { - $proof = $this->password->generate(); - $hash = $this->legacyPassword->hash($proof); - - $this->assertNotEmpty($hash); - $this->assertIsString($hash); - $this->assertStringStartsWith('$2y$', $hash); - $this->assertTrue($this->legacyPassword->verify($proof, $hash)); - - // Verify that only the specified hash is available - $this->expectException(\Exception::class); - $this->legacyPassword->getHashByName(Password::ARGON2); - } } From b9226abbab10439ab7524fecb842be9cb978d76d Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 17 Mar 2025 16:44:00 +0100 Subject: [PATCH 05/11] Added test --- tests/Auth/Proofs/PasswordTest.php | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/Auth/Proofs/PasswordTest.php b/tests/Auth/Proofs/PasswordTest.php index f50cdb5..4a2bd98 100644 --- a/tests/Auth/Proofs/PasswordTest.php +++ b/tests/Auth/Proofs/PasswordTest.php @@ -162,4 +162,42 @@ public function testAllHashesWork(): void $this->assertFalse($this->password->verify('wrongpassword', $hash), "Hash {$algo} failed wrong password test"); } } + + public function testCreateHash(): void + { + // Test default hash creation + $argon2Hash = Password::createHash(Password::ARGON2); + $this->assertInstanceOf(Argon2::class, $argon2Hash); + + $bcryptHash = Password::createHash(Password::BCRYPT); + $this->assertInstanceOf(Bcrypt::class, $bcryptHash); + + // Test hash creation with options + $customBcrypt = Password::createHash(Password::BCRYPT, [ + 'cost' => 8 + ]); + $this->assertInstanceOf(Bcrypt::class, $customBcrypt); + + $customScrypt = Password::createHash(Password::SCRYPT, [ + 'cpu_cost' => 8192, + 'memory_cost' => 4, + 'parallel_cost' => 1, + 'key_length' => 32 + ]); + $this->assertInstanceOf(Scrypt::class, $customScrypt); + + // Test invalid hash type + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported hash type: invalid-hash'); + Password::createHash('invalid-hash'); + } + + public function testCreateHashWithInvalidOptions(): void + { + // Test that invalid options are ignored + $hash = Password::createHash(Password::BCRYPT, [ + 'invalid_option' => 'value' + ]); + $this->assertInstanceOf(Bcrypt::class, $hash); + } } From e6c657762e0e7d43d85176bd7425e276a80f7fce Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 17 Mar 2025 20:39:19 +0100 Subject: [PATCH 06/11] backward compatibility fix --- src/Auth/Hashes/ScryptModified.php | 2 +- tests/Auth/Algorithms/ScryptModifiedTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Auth/Hashes/ScryptModified.php b/src/Auth/Hashes/ScryptModified.php index e7ab78e..b5dc02d 100644 --- a/src/Auth/Hashes/ScryptModified.php +++ b/src/Auth/Hashes/ScryptModified.php @@ -170,6 +170,6 @@ public function setSignerKey(string $key): self */ public function getName(): string { - return 'scryptMod'; + return 'scrypt-modified'; } } diff --git a/tests/Auth/Algorithms/ScryptModifiedTest.php b/tests/Auth/Algorithms/ScryptModifiedTest.php index fe52133..5e3d06a 100644 --- a/tests/Auth/Algorithms/ScryptModifiedTest.php +++ b/tests/Auth/Algorithms/ScryptModifiedTest.php @@ -39,6 +39,6 @@ public function testCustomOptions(): void public function testGetName(): void { - $this->assertEquals('scryptMod', $this->scryptModified->getName()); + $this->assertEquals('scrypt-modified', $this->scryptModified->getName()); } } From a88540a22676d390332167ab311882b1dd49cd70 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 17 Mar 2025 20:46:23 +0100 Subject: [PATCH 07/11] Fix --- src/Auth/Hashes/ScryptModified.php | 2 +- tests/Auth/Algorithms/ScryptModifiedTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Auth/Hashes/ScryptModified.php b/src/Auth/Hashes/ScryptModified.php index b5dc02d..e7ab78e 100644 --- a/src/Auth/Hashes/ScryptModified.php +++ b/src/Auth/Hashes/ScryptModified.php @@ -170,6 +170,6 @@ public function setSignerKey(string $key): self */ public function getName(): string { - return 'scrypt-modified'; + return 'scryptMod'; } } diff --git a/tests/Auth/Algorithms/ScryptModifiedTest.php b/tests/Auth/Algorithms/ScryptModifiedTest.php index 5e3d06a..fe52133 100644 --- a/tests/Auth/Algorithms/ScryptModifiedTest.php +++ b/tests/Auth/Algorithms/ScryptModifiedTest.php @@ -39,6 +39,6 @@ public function testCustomOptions(): void public function testGetName(): void { - $this->assertEquals('scrypt-modified', $this->scryptModified->getName()); + $this->assertEquals('scryptMod', $this->scryptModified->getName()); } } From ed49b9e481030ba5e589140b41a9f4be1486310f Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 17 Mar 2025 20:57:57 +0100 Subject: [PATCH 08/11] Fix --- src/Auth/Proofs/Password.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth/Proofs/Password.php b/src/Auth/Proofs/Password.php index 0262bbc..9543f40 100644 --- a/src/Auth/Proofs/Password.php +++ b/src/Auth/Proofs/Password.php @@ -20,7 +20,7 @@ class Password extends Proof public const SCRYPT = 'scrypt'; - public const SCRYPT_MODIFIED = 'scrypt-modified'; + public const SCRYPT_MODIFIED = 'scryptMod'; public const SHA = 'sha'; From dfdf614644237700e41935b51da7e39f6848a6e7 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 18 Mar 2025 20:34:43 +0100 Subject: [PATCH 09/11] Changed default sha --- src/Auth/Hashes/Sha.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth/Hashes/Sha.php b/src/Auth/Hashes/Sha.php index 371ec4f..90db2f6 100644 --- a/src/Auth/Hashes/Sha.php +++ b/src/Auth/Hashes/Sha.php @@ -29,7 +29,7 @@ class Sha extends Hash */ public function __construct() { - $this->setOption('version', 'sha3-512'); + $this->setOption('version', 'sha256'); } /** From 19fb580de44fac5928f9c0211fd0fdfd5022efdb Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 19 Mar 2025 07:47:02 +0100 Subject: [PATCH 10/11] Added type option for backward compatibility --- src/Auth/Hashes/Argon2.php | 1 + src/Auth/Hashes/Bcrypt.php | 1 + src/Auth/Hashes/MD5.php | 8 ++++++++ src/Auth/Hashes/PHPass.php | 1 + src/Auth/Hashes/Plaintext.php | 8 ++++++++ src/Auth/Hashes/Scrypt.php | 1 + src/Auth/Hashes/ScryptModified.php | 2 ++ 7 files changed, 22 insertions(+) diff --git a/src/Auth/Hashes/Argon2.php b/src/Auth/Hashes/Argon2.php index 3c15e7d..8e3f435 100644 --- a/src/Auth/Hashes/Argon2.php +++ b/src/Auth/Hashes/Argon2.php @@ -11,6 +11,7 @@ class Argon2 extends Hash */ public function __construct() { + $this->setOption('type', $this->getName()); $this->setOption('memoryCost', 65536); $this->setOption('timeCost', 4); $this->setOption('threads', 3); diff --git a/src/Auth/Hashes/Bcrypt.php b/src/Auth/Hashes/Bcrypt.php index 804a576..b7fc6be 100644 --- a/src/Auth/Hashes/Bcrypt.php +++ b/src/Auth/Hashes/Bcrypt.php @@ -11,6 +11,7 @@ class Bcrypt extends Hash */ public function __construct() { + $this->setOption('type', $this->getName()); $this->setOption('cost', 8); } diff --git a/src/Auth/Hashes/MD5.php b/src/Auth/Hashes/MD5.php index d2fff06..2960e41 100644 --- a/src/Auth/Hashes/MD5.php +++ b/src/Auth/Hashes/MD5.php @@ -6,6 +6,14 @@ class MD5 extends Hash { + /** + * Constructor + */ + public function __construct() + { + $this->setOption('type', $this->getName()); + } + /** * {@inheritdoc} */ diff --git a/src/Auth/Hashes/PHPass.php b/src/Auth/Hashes/PHPass.php index acf8ec6..fa95790 100644 --- a/src/Auth/Hashes/PHPass.php +++ b/src/Auth/Hashes/PHPass.php @@ -21,6 +21,7 @@ public function __construct() $randomState .= getmypid(); } + $this->setOption('type', $this->getName()); $this->setOption('iteration_count_log2', 8); $this->setOption('portable_hashes', false); $this->setOption('random_state', $randomState); diff --git a/src/Auth/Hashes/Plaintext.php b/src/Auth/Hashes/Plaintext.php index 35c54db..36cbec7 100644 --- a/src/Auth/Hashes/Plaintext.php +++ b/src/Auth/Hashes/Plaintext.php @@ -6,6 +6,14 @@ class Plaintext extends Hash { + /** + * Constructor + */ + public function __construct() + { + $this->setOption('type', $this->getName()); + } + /** * {@inheritdoc} */ diff --git a/src/Auth/Hashes/Scrypt.php b/src/Auth/Hashes/Scrypt.php index 92607b7..33252b9 100644 --- a/src/Auth/Hashes/Scrypt.php +++ b/src/Auth/Hashes/Scrypt.php @@ -11,6 +11,7 @@ class Scrypt extends Hash */ public function __construct() { + $this->setOption('type', $this->getName()); $this->setOption('costCpu', 8); $this->setOption('costMemory', 14); $this->setOption('costParallel', 1); diff --git a/src/Auth/Hashes/ScryptModified.php b/src/Auth/Hashes/ScryptModified.php index e7ab78e..45938d4 100644 --- a/src/Auth/Hashes/ScryptModified.php +++ b/src/Auth/Hashes/ScryptModified.php @@ -16,6 +16,8 @@ public function __construct() $saltSeparator = random_bytes(16); $signerKey = random_bytes(32); + $this->setOption('type', $this->getName()); + // Set default options with secure random values $this->setOption('salt', base64_encode($salt)); $this->setOption('saltSeparator', base64_encode($saltSeparator)); From a175dc225eb522c1189b5c923b2cab9218fb6327 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 29 Apr 2025 21:25:50 +0200 Subject: [PATCH 11/11] Formatting --- src/Auth/Proofs/Password.php | 5 +++-- tests/Auth/HashTest.php | 13 +++++++------ tests/Auth/Proofs/PasswordTest.php | 8 ++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Auth/Proofs/Password.php b/src/Auth/Proofs/Password.php index 9543f40..4a55a00 100644 --- a/src/Auth/Proofs/Password.php +++ b/src/Auth/Proofs/Password.php @@ -175,9 +175,10 @@ public function generate(): string /** * Create a hash instance by type * - * @param string $type One of the supported hash types (ARGON2, BCRYPT, SCRYPT, SCRYPT_MODIFIED, SHA, MD5, PHPASS) - * @param array $options Optional parameters for hash configuration + * @param string $type One of the supported hash types (ARGON2, BCRYPT, SCRYPT, SCRYPT_MODIFIED, SHA, MD5, PHPASS) + * @param array $options Optional parameters for hash configuration * @return Hash + * * @throws \Exception */ public static function createHash(string $type, array $options = []): Hash diff --git a/tests/Auth/HashTest.php b/tests/Auth/HashTest.php index 7c20b9f..84c6dfd 100644 --- a/tests/Auth/HashTest.php +++ b/tests/Auth/HashTest.php @@ -12,15 +12,16 @@ class HashTest extends TestCase protected function setUp(): void { // Create a concrete implementation of Hash for testing - $this->hash = new class extends Hash { + $this->hash = new class extends Hash + { public function hash(string $value): string { - return 'hashed_' . $value; + return 'hashed_'.$value; } public function verify(string $value, string $hash): bool { - return $hash === 'hashed_' . $value; + return $hash === 'hashed_'.$value; } public function getName(): string @@ -43,7 +44,7 @@ public function testSetOptions(): void $options = [ 'key1' => 'value1', 'key2' => 'value2', - 'key3' => ['nested' => 'value'] + 'key3' => ['nested' => 'value'], ]; $this->hash->setOptions($options); @@ -61,7 +62,7 @@ public function testGetOptions(): void { $options = [ 'key1' => 'value1', - 'key2' => 'value2' + 'key2' => 'value2', ]; $this->hash->setOptions($options); @@ -78,4 +79,4 @@ public function testMethodChaining(): void $this->assertEquals('value1', $this->hash->getOption('key1')); $this->assertEquals('value2', $this->hash->getOption('key2')); } -} \ No newline at end of file +} diff --git a/tests/Auth/Proofs/PasswordTest.php b/tests/Auth/Proofs/PasswordTest.php index 4a2bd98..41d8d91 100644 --- a/tests/Auth/Proofs/PasswordTest.php +++ b/tests/Auth/Proofs/PasswordTest.php @@ -174,15 +174,15 @@ public function testCreateHash(): void // Test hash creation with options $customBcrypt = Password::createHash(Password::BCRYPT, [ - 'cost' => 8 + 'cost' => 8, ]); $this->assertInstanceOf(Bcrypt::class, $customBcrypt); - + $customScrypt = Password::createHash(Password::SCRYPT, [ 'cpu_cost' => 8192, 'memory_cost' => 4, 'parallel_cost' => 1, - 'key_length' => 32 + 'key_length' => 32, ]); $this->assertInstanceOf(Scrypt::class, $customScrypt); @@ -196,7 +196,7 @@ public function testCreateHashWithInvalidOptions(): void { // Test that invalid options are ignored $hash = Password::createHash(Password::BCRYPT, [ - 'invalid_option' => 'value' + 'invalid_option' => 'value', ]); $this->assertInstanceOf(Bcrypt::class, $hash); }