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