PhpPasswordHasher::verify()   B
last analyzed

Complexity

Conditions 7
Paths 6

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 17
rs 8.8333
cc 7
nc 6
nop 2
1
<?php
2
3
/**
4
 * This file is part of web-stack
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Slick\WebStack\Domain\Security\PasswordHasher\Hasher;
13
14
use Slick\WebStack\Domain\Security\Exception\InvalidPasswordException;
15
use Slick\WebStack\Domain\Security\PasswordHasher\PasswordHasherInterface;
16
use InvalidArgumentException;
17
use SensitiveParameter;
0 ignored issues
show
Bug introduced by
The type SensitiveParameter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use function defined;
19
use function strlen;
20
use const PASSWORD_BCRYPT;
21
22
/**
23
 * PhpPasswordHasher
24
 *
25
 * @package Slick\WebStack\Domain\Security\PasswordHasher\Hasher
26
 */
27
final class PhpPasswordHasher implements PasswordHasherInterface
28
{
29
    use CheckPasswordLengthTrait;
30
31
    private string $algorithm = PASSWORD_BCRYPT;
32
33
    /**
34
     * @var array<string, mixed>|int[]
35
     */
36
    private array $options = [];
37
38
    /**
39
     * @var array<string, string>
40
     */
41
    private array $algorithms = [
42
        PASSWORD_BCRYPT => 'BCrypt',
43
        PASSWORD_ARGON2I => 'Argon 2I',
44
        PASSWORD_ARGON2ID => 'Argon 2ID',
45
    ];
46
47
    /**
48
     * Creates a PhpPasswordHasher
49
     *
50
     * @param int|null $opsLimit
51
     * @param int|null $memLimit
52
     * @param int|null $cost
53
     * @param string|null $algorithm
54
     */
55
    public function __construct(
56
        ?int $opsLimit = null,
57
        ?int $memLimit = null,
58
        ?int $cost = null,
59
        ?string $algorithm = null
60
    ) {
61
        $cost ??= 13;
62
        $opsLimit ??= 4;
63
        $memLimit ??= 64 * 1024 * 1024;
64
65
66
        if (3 > $opsLimit) {
67
            throw new InvalidArgumentException('$opsLimit must be 3 or greater.');
68
        }
69
70
        if (10 * 1024 > $memLimit) {
71
            throw new InvalidArgumentException('$memLimit must be 10k or greater.');
72
        }
73
74
        if ($cost < 4 || 31 < $cost) {
75
            throw new InvalidArgumentException('$cost must be in the range of 4-31.');
76
        }
77
78
        $algorithms = $this->getSupportedAlgorithms();
79
        if (null !== $algorithm) {
80
            $this->algorithm = $algorithms[$algorithm] ?? $algorithm;
81
        }
82
83
        $this->options = [
84
            'cost' => $cost,
85
            'time_cost' => $opsLimit,
86
            'memory_cost' => $memLimit >> 10,
87
            'threads' => 1,
88
        ];
89
    }
90
91
    /**
92
     * @inheritDoc
93
     */
94
    public function hash(#[SensitiveParameter] string $plainPassword): string
95
    {
96
        if ($this->isPasswordTooLong($plainPassword)) {
97
            throw new InvalidPasswordException("Password too long.");
98
        }
99
100
        $isBigOrHasNullByte = 72 < strlen($plainPassword) || str_contains($plainPassword, "\0");
101
        if (PASSWORD_BCRYPT === $this->algorithm && $isBigOrHasNullByte) {
102
            $plainPassword = base64_encode(hash('sha512', $plainPassword, true));
103
        }
104
105
        return password_hash($plainPassword, $this->algorithm, $this->options);
0 ignored issues
show
Bug Best Practice introduced by
The expression return password_hash($pl...orithm, $this->options) could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
106
    }
107
108
    /**
109
     * @inheritDoc
110
     */
111
    public function verify(string $hashedPassword, #[SensitiveParameter] string $plainPassword): bool
112
    {
113
        if ('' === $plainPassword || $this->isPasswordTooLong($plainPassword)) {
114
            return false;
115
        }
116
117
        if (!str_starts_with($hashedPassword, '$argon')) {
118
            // Bcrypt cuts on NUL chars and after 72 bytes
119
            $isBigOrHasNullByte = 72 < strlen($plainPassword) || str_contains($plainPassword, "\0");
120
            if (str_starts_with($hashedPassword, '$2') && ($isBigOrHasNullByte)) {
121
                $plainPassword = base64_encode(hash('sha512', $plainPassword, true));
122
            }
123
124
            return password_verify($plainPassword, $hashedPassword);
125
        }
126
127
        return password_verify($plainPassword, $hashedPassword);
128
    }
129
130
    /**
131
     * @inheritDoc
132
     */
133
    public function needsRehash(string $hashedPassword): bool
134
    {
135
        return password_needs_rehash($hashedPassword, $this->algorithm, $this->options);
136
    }
137
138
    public function info(): array
139
    {
140
        return [
141
            'algorithm' => $this->algorithms[$this->algorithm],
142
            ...$this->options
143
        ];
144
    }
145
146
    /**
147
     * @return array<string|int, mixed>
148
     */
149
    public function getSupportedAlgorithms(): array
150
    {
151
        $algorithms = [1 => PASSWORD_BCRYPT, '2y' => PASSWORD_BCRYPT];
152
        $this->algorithm = PASSWORD_BCRYPT;
153
154
        if (defined('PASSWORD_ARGON2I')) {
155
            $algorithms[2] = $algorithms['argon2i'] = PASSWORD_ARGON2I;
156
            $this->algorithm = PASSWORD_ARGON2I;
157
        }
158
159
        if (defined('PASSWORD_ARGON2ID')) {
160
            $algorithms[3] = $algorithms['argon2id'] = PASSWORD_ARGON2ID;
161
            $this->algorithm = PASSWORD_ARGON2ID;
162
        }
163
        return $algorithms;
164
    }
165
}
166