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