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