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: final readonly class SodiumHash extends AbstractHash
20: {
21: /**
22: * Represents a maximum amount of computations to perform.
23: *
24: * Raising this number will make the function require more CPU cycles to compute a key.
25: * There are constants available to set the operations limit to appropriate values
26: * depending on intended use, in order of strength:
27: * - `SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE`
28: * - `SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE`
29: * - `SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE`
30: *
31: * @var int<SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, max>
32: */
33: private int $opslimit;
34:
35: /**
36: * The maximum amount of RAM that the function will use, in bytes.
37: *
38: * There are constants to help you choose an appropriate value, in order of size:
39: * - `SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE`
40: * - `SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE`
41: * - `SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE`
42: *
43: * @var int<SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, max>
44: */
45: private int $memlimit;
46:
47: /**
48: * @param array{opslimit?: int, memlimit?: int} $options
49: */
50: public function __construct(
51: public Algorithm $algorithm,
52: array $options = [],
53: ) {
54: if (Algorithm::Sodium !== $algorithm) {
55: throw new HashException(\sprintf(
56: 'Algorithm expected to be Algorithm::Sodium, Algorithm::%s given.',
57: $algorithm->name,
58: ));
59: }
60:
61: ['opslimit' => $this->opslimit, 'memlimit' => $this->memlimit] = $this->validatedOptions(
62: $options,
63: SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE,
64: SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE,
65: );
66: }
67:
68: /**
69: * @param array{opslimit?: int, memlimit?: int} $options
70: */
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: ['opslimit' => $opslimit, 'memlimit' => $memlimit] = $this->validatedOptions(
78: $options,
79: $this->opslimit,
80: $this->memlimit,
81: );
82:
83: return sodium_crypto_pwhash_str($password, $opslimit, $memlimit);
84: }
85:
86: /**
87: * @param array{opslimit?: int, memlimit?: int} $options
88: */
89: public function needsRehash(string $hash, array $options = []): bool
90: {
91: ['opslimit' => $opslimit, 'memlimit' => $memlimit] = $this->validatedOptions(
92: $options,
93: $this->opslimit,
94: $this->memlimit,
95: );
96:
97: return sodium_crypto_pwhash_str_needs_rehash($hash, $opslimit, $memlimit);
98: }
99:
100: public function verify(#[\SensitiveParameter] string $password, string $hash): bool
101: {
102: if (! $this->isValidPassword($password)) {
103: return false;
104: }
105:
106: if (! str_starts_with($hash, SODIUM_CRYPTO_PWHASH_STRPREFIX)) {
107: return false;
108: }
109:
110: return sodium_crypto_pwhash_str_verify($hash, $password);
111: }
112:
113: public function valid(): bool
114: {
115: return \extension_loaded('sodium')
116: && version_compare(SODIUM_LIBRARY_VERSION, '1.0.14', '>=');
117: }
118:
119: /**
120: * @param array{opslimit?: int, memlimit?: int} $options
121: *
122: * @return array{
123: * opslimit: int<SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, max>,
124: * memlimit: int<SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, max>,
125: * }
126: *
127: * @throws HashException
128: */
129: private function validatedOptions(array $options, int $opslimit, int $memlimit): array
130: {
131: $opslimit = $options['opslimit'] ?? $opslimit;
132: $memlimit = $options['memlimit'] ?? $memlimit;
133:
134: if ($opslimit < SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE) {
135: throw new HashException(\sprintf(
136: 'Operations limit should be %d or greater, %d given.',
137: SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
138: $opslimit,
139: ));
140: }
141:
142: if ($memlimit < SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE) {
143: throw new HashException(\sprintf(
144: 'Memory limit should be %sMiB or greater (expressed in bytes), %sMiB given.',
145: number_format(SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE / 1024 ** 2),
146: number_format($memlimit / 1024 ** 2),
147: ));
148: }
149:
150: return compact('opslimit', 'memlimit');
151: }
152: }
153: