aboutsummaryrefslogtreecommitdiffhomepage
path: root/main/app/sprinkles/account/src/Authenticate/Hasher.php
blob: e277eef838f71f8346542c4f73c87b6946aaef8f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?php
/**
 * UserFrosting (http://www.userfrosting.com)
 *
 * @link      https://github.com/userfrosting/UserFrosting
 * @license   https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
 */
namespace UserFrosting\Sprinkle\Account\Authenticate;

/**
 * Password hashing and validation class
 *
 * @author Alex Weissman (https://alexanderweissman.com)
 */
class Hasher
{
    /**
     * Default crypt cost factor.
     *
     * @var int
     */
    protected $defaultRounds = 10;

    /**
     * Returns the hashing type for a specified password hash.
     *
     * Automatically detects the hash type: "sha1" (for UserCake legacy accounts), "legacy" (for 0.1.x accounts), and "modern" (used for new accounts).
     * @param string $password the hashed password.
     * @return string "sha1"|"legacy"|"modern".
     */
    public function getHashType($password)
    {
        // If the password in the db is 65 characters long, we have an sha1-hashed password.
        if (strlen($password) == 65) {
            return 'sha1';
        } elseif (strlen($password) == 82) {
            return 'legacy';
        }

        return 'modern';
    }

    /**
     * Hashes a plaintext password using bcrypt.
     *
     * @param string $password the plaintext password.
     * @param array  $options
     * @return string the hashed password.
     * @throws HashFailedException
     */
    public function hash($password, array $options = [])
    {
        $hash = password_hash($password, PASSWORD_BCRYPT, [
            'cost' => $this->cost($options),
        ]);

        if (!$hash) {
            throw new HashFailedException();
        }

        return $hash;
    }

    /**
     * Verify a plaintext password against the user's hashed password.
     *
     * @param string $password The plaintext password to verify.
     * @param string $hash The hash to compare against.
     * @param array  $options
     * @return boolean True if the password matches, false otherwise.
     */
    public function verify($password, $hash, array $options = [])
    {
        $hashType = $this->getHashType($hash);

        if ($hashType == 'sha1') {
            // Legacy UserCake passwords
            $salt = substr($hash, 0, 25);		// Extract the salt from the hash
            $inputHash = $salt . sha1($salt . $password);

            return (hash_equals($inputHash, $hash) === true);

        } elseif ($hashType == 'legacy') {
            // Homegrown implementation (assuming that current install has been using a cost parameter of 12)
            // Used for manual implementation of bcrypt.
            // Note that this legacy hashing put the salt at the _end_ for some reason.
            $salt = substr($hash, 60);
            $inputHash = crypt($password, '$2y$12$' . $salt);
            $correctHash = substr($hash, 0, 60);

            return (hash_equals($inputHash, $correctHash) === true);
        }

        // Modern implementation
        return password_verify($password, $hash);
    }

    /**
     * Extract the cost value from the options array.
     *
     * @param  array  $options
     * @return int
     */
    protected function cost(array $options = [])
    {
        return isset($options['rounds']) ? $options['rounds'] : $this->defaultRounds;
    }
}