aboutsummaryrefslogtreecommitdiff
path: root/ratatoeskr/sys/PasswordHash.php
blob: 92286420ebb87807081fe79328e6389372c77f8a (plain)
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
<?php


namespace r7r\cms\sys;

/**
 * Functions for creating and checking password hashes. Mostly wrappers around php's builtin password_\* functions but
 * can also verify our old legacy password hash.
 */
class PasswordHash
{
    private const PASSWORD_ALGO = \PASSWORD_DEFAULT;

    /** @var bool */
    private $isLegacy;

    /** @var string */
    private $hashData;

    private function __construct(bool $isLegacy, string $hashData)
    {
        $this->isLegacy = $isLegacy;
        $this->hashData = $hashData;
    }

    private static function verifyLegacy(string $password, string $pwhash): bool
    {
        list($iterations, $hexsalt) = explode('$', $pwhash);
        return self::hashLegacy($password, pack("H*", $hexsalt), $iterations) == $pwhash;
    }

    private static function hashLegacy(string $data, $salt, string $iterations): string
    {
        $hash = $data . $salt;
        for ($i = $iterations ;$i--;) {
            $hash = sha1($data . $hash . $salt, (bool) $i);
        }
        return $iterations . '$' . bin2hex($salt) . '$' . $hash;
    }

    private function format(): string
    {
        return $this->isLegacy
            ? $this->hashData
            : '!' . $this->hashData;
    }

    private static function parse(string $s): self
    {
        return substr($s, 0, 1) === '!'
            ? new self(false, substr($s, 1))
            : new self(true, $s);
    }

    /**
     * Verifies that a given password is valid for the given hash
     * @param string $password
     * @param string $hash
     * @return bool
     */
    public static function verify(string $password, string $hash): bool
    {
        $hash = self::parse($hash);
        return $hash->isLegacy
            ? self::verifyLegacy($password, $hash->hashData)
            : password_verify($password, $hash->hashData);
    }

    /**
     * Creates a hash for a password
     * @param string $password
     * @return string Treat this as opaque data. Don't rely on it being in a certain format, it might change in the future.
     */
    public static function hash(string $password): string
    {
        return (new self(false, password_hash($password, self::PASSWORD_ALGO)))->format();
    }

    /**
     * Checks, if a given hash should be recomputed (because it's not considered secure any more) if the password is known.
     * @param string $hash
     * @return bool
     */
    public static function needsRehash(string $hash): bool
    {
        $hash = self::parse($hash);
        return $hash->isLegacy
            ? true
            : password_needs_rehash($hash->hashData, self::PASSWORD_ALGO);
    }
}