Skip to content

Commit

Permalink
feat: password hash (#370)
Browse files Browse the repository at this point in the history
* feat: password hash

* formatting (fix phpstan)

* feat: hash driver register
  • Loading branch information
SonyPradana authored Sep 3, 2024
1 parent ea2d45d commit e6782f2
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/System/Security/Hashing/Argon2IdHasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace System\Security\Hashing;

class Argon2IdHasher extends ArgonHasher implements HashInterface
{
public function make(string $value, array $options = []): string
{
$hash = @password_hash($value, PASSWORD_ARGON2ID, [
'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_ARGON2ID . ' hashing not supported.');
}

return $hash;
}

public function isValidAlgorithm(string $hash): bool
{
return 'argon2id' === $this->info($hash)['algoName'];
}
}
55 changes: 55 additions & 0 deletions src/System/Security/Hashing/ArgonHasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace System\Security\Hashing;

class ArgonHasher extends DefaultHasher implements HashInterface
{
protected int $memory = 1024;

protected int $time = 2;

protected int $threads = 2;

public function setMemory(int $memory): self
{
$this->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'];
}
}
31 changes: 31 additions & 0 deletions src/System/Security/Hashing/BcryptHasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace System\Security\Hashing;

class BcryptHasher extends DefaultHasher implements HashInterface
{
protected int $rounds = 12;

public function setRounds(int $rounds): self
{
$this->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'];
}
}
28 changes: 28 additions & 0 deletions src/System/Security/Hashing/DefaultHasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace System\Security\Hashing;

class DefaultHasher implements HashInterface
{
public function info(string $hash): array
{
return password_get_info($hash);
}

public function verify(string $value, string $hashed_value, array $options = []): bool
{
return password_verify($value, $hashed_value);
}

public function make(string $value, array $options = []): string
{
return password_hash($value, PASSWORD_DEFAULT);
}

public function isValidAlgorithm(string $hash): bool
{
return 'bcrypt' === $this->info($hash)['algoName'];
}
}
33 changes: 33 additions & 0 deletions src/System/Security/Hashing/HashInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace System\Security\Hashing;

interface HashInterface
{
/**
* Get information about hash.
*
* @return array<string, int|string|bool>
*/
public function info(string $hash): array;

/**
* Verify hash and hashed.
*
* @param array<string, int|string|bool> $options
*/
public function verify(string $value, string $hashed_value, array $options = []): bool;

/**
* Hash given string.
*
* @param array<string, int|string|bool> $options
*
* @throws \RuntimeException
*/
public function make(string $value, array $options = []): string;

public function isValidAlgorithm(string $hash): bool;
}
61 changes: 61 additions & 0 deletions src/System/Security/Hashing/HashManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace System\Security\Hashing;

class HashManager implements HashInterface
{
/** @var array<string, HashInterface> */
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);
}
}
33 changes: 33 additions & 0 deletions tests/Security/Hashing/HasherMangerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace System\Test\Security\Hashing;

use PHPUnit\Framework\TestCase;
use System\Security\Hashing\BcryptHasher;
use System\Security\Hashing\HashManager;

class HasherMangerTest extends TestCase
{
/** @test */
public function itCanHashDefaultHasher()
{
$hasher = new HashManager();
$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));
}
}
54 changes: 54 additions & 0 deletions tests/Security/Hashing/HasherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace System\Test\Security\Hashing;

use PHPUnit\Framework\TestCase;
use System\Security\Hashing\Argon2IdHasher;
use System\Security\Hashing\ArgonHasher;
use System\Security\Hashing\BcryptHasher;
use System\Security\Hashing\DefaultHasher;

class HasherTest extends TestCase
{
/** @test */
public function itCanHashDefaultHasher()
{
$hasher = new DefaultHasher();
$hash = $hasher->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));
}
}

0 comments on commit e6782f2

Please sign in to comment.