1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus framework.
7: *
8: * (c) John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Password\Hash;
15:
16: use Nexus\Password\Algorithm;
17: use Nexus\Password\HashException;
18:
19: abstract readonly class AbstractArgon2Hash extends AbstractHash
20: {
21: private const int MINIMUM_MEMORY_COST = 7 * 1024;
22: private const int MINIMUM_TIME_COST = 1;
23: private const int MINIMUM_THREADS = 1;
24:
25: /**
26: * @var int<self::MINIMUM_MEMORY_COST, max>
27: */
28: private int $memoryCost;
29:
30: /**
31: * @var int<self::MINIMUM_TIME_COST, max>
32: */
33: private int $timeCost;
34:
35: /**
36: * @var int<self::MINIMUM_THREADS, max>
37: */
38: private int $threads;
39:
40: /**
41: * @param array{
42: * memory_cost?: int,
43: * time_cost?: int,
44: * threads?: int,
45: * } $options
46: *
47: * @throws HashException
48: */
49: public function __construct(
50: public Algorithm $algorithm,
51: array $options = [],
52: ) {
53: ['memory_cost' => $this->memoryCost, 'time_cost' => $this->timeCost, 'threads' => $this->threads] = $this->validatedOptions(
54: $options,
55: PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
56: PASSWORD_ARGON2_DEFAULT_TIME_COST,
57: PASSWORD_ARGON2_DEFAULT_THREADS,
58: );
59: }
60:
61: /**
62: * @param array{
63: * memory_cost?: int,
64: * threads?: int,
65: * time_cost?: int,
66: * } $options
67: */
68: public function hash(#[\SensitiveParameter] string $password, array $options = []): string
69: {
70: if (! $this->isValidPassword($password)) {
71: throw new HashException('Invalid password provided.');
72: }
73:
74: return password_hash(
75: $password,
76: $this->algorithm->value,
77: $this->validatedOptions(
78: $options,
79: $this->memoryCost,
80: $this->timeCost,
81: $this->threads,
82: ),
83: );
84: }
85:
86: /**
87: * @param array{
88: * memory_cost?: int,
89: * threads?: int,
90: * time_cost?: int,
91: * } $options
92: */
93: public function needsRehash(string $hash, array $options = []): bool
94: {
95: return password_needs_rehash(
96: $hash,
97: $this->algorithm->value,
98: $this->validatedOptions(
99: $options,
100: $this->memoryCost,
101: $this->timeCost,
102: $this->threads,
103: ),
104: );
105: }
106:
107: public function verify(string $password, string $hash): bool
108: {
109: if (! $this->isValidPassword($password)) {
110: return false;
111: }
112:
113: return password_verify($password, $hash);
114: }
115:
116: /**
117: * @param array{
118: * memory_cost?: int,
119: * time_cost?: int,
120: * threads?: int,
121: * } $options
122: *
123: * @return array{
124: * memory_cost: int<self::MINIMUM_MEMORY_COST, max>,
125: * time_cost: int<self::MINIMUM_TIME_COST, max>,
126: * threads: int<self::MINIMUM_THREADS, max>,
127: * }
128: *
129: * @throws HashException
130: */
131: private function validatedOptions(array $options, int $memoryCost, int $timeCost, int $threads): array
132: {
133: $memoryCost = $options['memory_cost'] ?? $memoryCost;
134: $timeCost = $options['time_cost'] ?? $timeCost;
135: $threads = $options['threads'] ?? $threads;
136:
137: if ($memoryCost < self::MINIMUM_MEMORY_COST) {
138: throw new HashException(\sprintf(
139: 'Memory cost should be %sKiB or greater, %sKiB given.',
140: number_format(self::MINIMUM_MEMORY_COST / 1024),
141: number_format($memoryCost / 1024),
142: ));
143: }
144:
145: if ($timeCost < self::MINIMUM_TIME_COST) {
146: throw new HashException(\sprintf(
147: 'Time cost should be %d or greater, %d given.',
148: self::MINIMUM_TIME_COST,
149: $timeCost,
150: ));
151: }
152:
153: if ($threads < self::MINIMUM_THREADS) {
154: throw new HashException(\sprintf(
155: 'Number of threads should be %d or greater, %d given.',
156: self::MINIMUM_THREADS,
157: $threads,
158: ));
159: }
160:
161: return [
162: 'memory_cost' => $memoryCost,
163: 'time_cost' => $timeCost,
164: 'threads' => $threads,
165: ];
166: }
167: }
168: