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(public Algorithm $algorithm, array $options = [])
51: {
52: if (Algorithm::Sodium !== $algorithm) {
53: throw new HashException(\sprintf(
54: 'Algorithm expected to be Algorithm::Sodium, Algorithm::%s given.',
55: $algorithm->name,
56: ));
57: }
58:
59: $validatedOptions = self::validatedOptions(
60: $options,
61: \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE,
62: \SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE,
63: );
64:
65: $this->opslimit = $validatedOptions['opslimit'];
66: $this->memlimit = $validatedOptions['memlimit'];
67: }
68:
69: /**
70: * @param array{opslimit?: int, memlimit?: int} $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: $validatedOptions = self::validatedOptions($options, $this->opslimit, $this->memlimit);
80:
81: return sodium_crypto_pwhash_str($password, $validatedOptions['opslimit'], $validatedOptions['memlimit']);
82: }
83:
84: /**
85: * @param array{opslimit?: int, memlimit?: int} $options
86: */
87: #[\Override]
88: public function needsRehash(string $hash, array $options = []): bool
89: {
90: $validatedOptions = self::validatedOptions($options, $this->opslimit, $this->memlimit);
91:
92: return sodium_crypto_pwhash_str_needs_rehash($hash, $validatedOptions['opslimit'], $validatedOptions['memlimit']);
93: }
94:
95: #[\Override]
96: public function verify(#[\SensitiveParameter] string $password, string $hash): bool
97: {
98: if (! $this->isValidPassword($password)) {
99: return false;
100: }
101:
102: return sodium_crypto_pwhash_str_verify($hash, $password);
103: }
104:
105: #[\Override]
106: public function valid(): bool
107: {
108: return \extension_loaded('sodium')
109: && version_compare(\SODIUM_LIBRARY_VERSION, '1.0.14', '>=');
110: }
111:
112: /**
113: * @param array{opslimit?: int, memlimit?: int} $options
114: *
115: * @return array{
116: * opslimit: int<SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, max>,
117: * memlimit: int<SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, max>,
118: * }
119: *
120: * @throws HashException
121: */
122: private static function validatedOptions(array $options, int $opslimit, int $memlimit): array
123: {
124: $opslimit = $options['opslimit'] ?? $opslimit;
125: $memlimit = $options['memlimit'] ?? $memlimit;
126:
127: if ($opslimit < \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE) {
128: throw new HashException(\sprintf(
129: 'Operations limit should be %d or greater, %d given.',
130: \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
131: $opslimit,
132: ));
133: }
134:
135: if ($memlimit < \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE) {
136: throw new HashException(\sprintf(
137: 'Memory limit should be %sMiB or greater (expressed in bytes), %sMiB given.',
138: number_format(\SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE / 1024 ** 2),
139: number_format($memlimit / 1024 ** 2),
140: ));
141: }
142:
143: return compact('opslimit', 'memlimit');
144: }
145: }
146: