Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
PasswordHasher
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
6 / 6
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hash
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 verify
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 needsRehash
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isLegacyHash
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 legacyHash
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Password hashing and verification for database authentication.
5 *
6 * Writes bcrypt hashes and verifies both bcrypt and legacy salted SHA-256
7 * hashes, enabling transparent migration of legacy passwords on login.
8 *
9 * This Source Code Form is subject to the terms of the Mozilla Public License,
10 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
11 * obtain one at https://mozilla.org/MPL/2.0/.
12 *
13 * @package   phpMyFAQ
14 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
15 * @copyright 2026 phpMyFAQ Team
16 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
17 * @link      https://www.phpmyfaq.de
18 */
19
20declare(strict_types=1);
21
22namespace phpMyFAQ\Auth;
23
24use phpMyFAQ\Configuration;
25use SensitiveParameter;
26
27final class PasswordHasher
28{
29    public function __construct(
30        private readonly Configuration $configuration,
31    ) {
32    }
33
34    /**
35     * Hashes a password for storage using bcrypt.
36     */
37    public function hash(#[SensitiveParameter] string $password): string
38    {
39        return password_hash($password, PASSWORD_BCRYPT);
40    }
41
42    /**
43     * Verifies a password against a stored hash, accepting both bcrypt and
44     * legacy salted SHA-256 hashes.
45     */
46    public function verify(string $login, #[SensitiveParameter] string $password, string $storedHash): bool
47    {
48        if ($this->isLegacyHash($storedHash)) {
49            return hash_equals($storedHash, $this->legacyHash($login, $password));
50        }
51
52        return password_verify($password, $storedHash);
53    }
54
55    /**
56     * Returns true when the stored hash should be upgraded to current bcrypt
57     * parameters (legacy SHA-256, or bcrypt with outdated cost).
58     */
59    public function needsRehash(string $storedHash): bool
60    {
61        if ($this->isLegacyHash($storedHash)) {
62            return true;
63        }
64
65        return password_needs_rehash($storedHash, PASSWORD_BCRYPT);
66    }
67
68    private function isLegacyHash(string $storedHash): bool
69    {
70        // password_get_info() reports algo === null for non-PHC strings,
71        // i.e. the 64-char salted SHA-256 hex hashes produced by the old scheme.
72        return password_get_info($storedHash)['algo'] === null;
73    }
74
75    private function legacyHash(string $login, #[SensitiveParameter] string $password): string
76    {
77        $salt = (string) $this->configuration->get('security.salt') . $login;
78        return hash('sha256', $password . $salt);
79    }
80}