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: $validatedOptions = self::validatedOptions(
54: $options,
55: PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
56: PASSWORD_ARGON2_DEFAULT_TIME_COST,
57: PASSWORD_ARGON2_DEFAULT_THREADS,
58: );
59:
60: $this->memoryCost = $validatedOptions['memory_cost'];
61: $this->timeCost = $validatedOptions['time_cost'];
62: $this->threads = $validatedOptions['threads'];
63: }
64:
65: /**
66: * @param array{
67: * memory_cost?: int,
68: * threads?: int,
69: * time_cost?: int,
70: * } $options
71: */
72: #[\Override]
73: public function hash(#[\SensitiveParameter] string $password, array $options = []): string
74: {
75: if (! $this->isValidPassword($password)) {
76: throw new HashException('Invalid password provided.');
77: }
78:
79: return password_hash(
80: $password,
81: $this->algorithm->value,
82: self::validatedOptions(
83: $options,
84: $this->memoryCost,
85: $this->timeCost,
86: $this->threads,
87: ),
88: );
89: }
90:
91: /**
92: * @param array{
93: * memory_cost?: int,
94: * threads?: int,
95: * time_cost?: int,
96: * } $options
97: */
98: #[\Override]
99: public function needsRehash(string $hash, array $options = []): bool
100: {
101: return password_needs_rehash(
102: $hash,
103: $this->algorithm->value,
104: self::validatedOptions(
105: $options,
106: $this->memoryCost,
107: $this->timeCost,
108: $this->threads,
109: ),
110: );
111: }
112:
113: #[\Override]
114: public function verify(string $password, string $hash): bool
115: {
116: if (! $this->isValidPassword($password)) {
117: return false;
118: }
119:
120: if (! str_starts_with($hash, '$argon2')) {
121: return false;
122: }
123:
124: return password_verify($password, $hash);
125: }
126:
127: /**
128: * @param array{
129: * memory_cost?: int,
130: * time_cost?: int,
131: * threads?: int,
132: * } $options
133: *
134: * @return array{
135: * memory_cost: int<self::MINIMUM_MEMORY_COST, max>,
136: * time_cost: int<self::MINIMUM_TIME_COST, max>,
137: * threads: int<self::MINIMUM_THREADS, max>,
138: * }
139: *
140: * @throws HashException
141: */
142: private static function validatedOptions(array $options, int $memoryCost, int $timeCost, int $threads): array
143: {
144: $memoryCost = $options['memory_cost'] ?? $memoryCost;
145: $timeCost = $options['time_cost'] ?? $timeCost;
146: $threads = $options['threads'] ?? $threads;
147:
148: if ($memoryCost < self::MINIMUM_MEMORY_COST) {
149: throw new HashException(\sprintf(
150: 'Memory cost should be %sKiB or greater, %sKiB given.',
151: number_format(self::MINIMUM_MEMORY_COST / 1024),
152: number_format($memoryCost / 1024),
153: ));
154: }
155:
156: if ($timeCost < self::MINIMUM_TIME_COST) {
157: throw new HashException(\sprintf(
158: 'Time cost should be %d or greater, %d given.',
159: self::MINIMUM_TIME_COST,
160: $timeCost,
161: ));
162: }
163:
164: if ($threads < self::MINIMUM_THREADS) {
165: throw new HashException(\sprintf(
166: 'Number of threads should be %d or greater, %d given.',
167: self::MINIMUM_THREADS,
168: $threads,
169: ));
170: }
171:
172: return [
173: 'memory_cost' => $memoryCost,
174: 'time_cost' => $timeCost,
175: 'threads' => $threads,
176: ];
177: }
178: }
179: