Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 6
66.67% covered (warning)
66.67%
4 / 6
CRAP
n/a
0 / 0
password_hash
n/a
0 / 0
n/a
0 / 0
39
password_get_info
n/a
0 / 0
n/a
0 / 0
3
password_needs_rehash
n/a
0 / 0
n/a
0 / 0
5
password_verify
n/a
0 / 0
n/a
0 / 0
6
PasswordCompat\binary\_strlen
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
PasswordCompat\binary\_substr
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * A Compatibility library with PHP 5.5's simplified password hashing API.
4 *
5 * @author Anthony Ferrara <ircmaxell@php.net>
6 * @license http://www.opensource.org/licenses/mit-license.html MIT License
7 * @copyright 2012 The Authors
8 */
9
10namespace {
11
12if (!defined('PASSWORD_DEFAULT')) {
13
14    define('PASSWORD_BCRYPT', 1);
15    define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
16
17    /**
18     * Hash the password using the specified algorithm
19     *
20     * @param string $password The password to hash
21     * @param int    $algo     The algorithm to use (Defined by PASSWORD_* constants)
22     * @param array  $options  The options for the algorithm to use
23     *
24     * @return string|false The hashed password, or false on error.
25     */
26    function password_hash($password, $algo, array $options = array()) {
27        if (!function_exists('crypt')) {
28            trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
29            return null;
30        }
31        if (!is_string($password)) {
32            trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
33            return null;
34        }
35        if (!is_int($algo)) {
36            trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
37            return null;
38        }
39        $resultLength = 0;
40        switch ($algo) {
41            case PASSWORD_BCRYPT:
42                // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
43                $cost = 10;
44                if (isset($options['cost'])) {
45                    $cost = $options['cost'];
46                    if ($cost < 4 || $cost > 31) {
47                        trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
48                        return null;
49                    }
50                }
51                // The length of salt to generate
52                $raw_salt_len = 16;
53                // The length required in the final serialization
54                $required_salt_len = 22;
55                $hash_format = sprintf("$2y$%02d$", $cost);
56                // The expected length of the final crypt() output
57                $resultLength = 60;
58                break;
59            default:
60                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
61                return null;
62        }
63        $salt_requires_encoding = false;
64        if (isset($options['salt'])) {
65            switch (gettype($options['salt'])) {
66                case 'NULL':
67                case 'boolean':
68                case 'integer':
69                case 'double':
70                case 'string':
71                    $salt = (string) $options['salt'];
72                    break;
73                case 'object':
74                    if (method_exists($options['salt'], '__tostring')) {
75                        $salt = (string) $options['salt'];
76                        break;
77                    }
78                case 'array':
79                case 'resource':
80                default:
81                    trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
82                    return null;
83            }
84            if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
85                trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
86                return null;
87            } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
88                $salt_requires_encoding = true;
89            }
90        } else {
91            $buffer = '';
92            $buffer_valid = false;
93            if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
94                $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
95                if ($buffer) {
96                    $buffer_valid = true;
97                }
98            }
99            if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
100                $buffer = openssl_random_pseudo_bytes($raw_salt_len);
101                if ($buffer) {
102                    $buffer_valid = true;
103                }
104            }
105            if (!$buffer_valid && @is_readable('/dev/urandom')) {
106                $f = fopen('/dev/urandom', 'r');
107                $read = PasswordCompat\binary\_strlen($buffer);
108                while ($read < $raw_salt_len) {
109                    $buffer .= fread($f, $raw_salt_len - $read);
110                    $read = PasswordCompat\binary\_strlen($buffer);
111                }
112                fclose($f);
113                if ($read >= $raw_salt_len) {
114                    $buffer_valid = true;
115                }
116            }
117            if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
118                $bl = PasswordCompat\binary\_strlen($buffer);
119                for ($i = 0; $i < $raw_salt_len; $i++) {
120                    if ($i < $bl) {
121                        $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
122                    } else {
123                        $buffer .= chr(mt_rand(0, 255));
124                    }
125                }
126            }
127            $salt = $buffer;
128            $salt_requires_encoding = true;
129        }
130        if ($salt_requires_encoding) {
131            // encode string with the Base64 variant used by crypt
132            $base64_digits =
133                'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
134            $bcrypt64_digits =
135                './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
136
137            $base64_string = base64_encode($salt);
138            $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
139        }
140        $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
141
142        $hash = $hash_format . $salt;
143
144        $ret = crypt($password, $hash);
145
146        if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
147            return false;
148        }
149
150        return $ret;
151    }
152
153    /**
154     * Get information about the password hash. Returns an array of the information
155     * that was used to generate the password hash.
156     *
157     * array(
158     *    'algo' => 1,
159     *    'algoName' => 'bcrypt',
160     *    'options' => array(
161     *        'cost' => 10,
162     *    ),
163     * )
164     *
165     * @param string $hash The password hash to extract info from
166     *
167     * @return array The array of information about the hash.
168     */
169    function password_get_info($hash) {
170        $return = array(
171            'algo' => 0,
172            'algoName' => 'unknown',
173            'options' => array(),
174        );
175        if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
176            $return['algo'] = PASSWORD_BCRYPT;
177            $return['algoName'] = 'bcrypt';
178            list($cost) = sscanf($hash, "$2y$%d$");
179            $return['options']['cost'] = $cost;
180        }
181        return $return;
182    }
183
184    /**
185     * Determine if the password hash needs to be rehashed according to the options provided
186     *
187     * If the answer is true, after validating the password using password_verify, rehash it.
188     *
189     * @param string $hash    The hash to test
190     * @param int    $algo    The algorithm used for new password hashes
191     * @param array  $options The options array passed to password_hash
192     *
193     * @return boolean True if the password needs to be rehashed.
194     */
195    function password_needs_rehash($hash, $algo, array $options = array()) {
196        $info = password_get_info($hash);
197        if ($info['algo'] != $algo) {
198            return true;
199        }
200        switch ($algo) {
201            case PASSWORD_BCRYPT:
202                $cost = isset($options['cost']) ? $options['cost'] : 10;
203                if ($cost != $info['options']['cost']) {
204                    return true;
205                }
206                break;
207        }
208        return false;
209    }
210
211    /**
212     * Verify a password against a hash using a timing attack resistant approach
213     *
214     * @param string $password The password to verify
215     * @param string $hash     The hash to verify against
216     *
217     * @return boolean If the password matches the hash
218     */
219    function password_verify($password, $hash) {
220        if (!function_exists('crypt')) {
221            trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
222            return false;
223        }
224        $ret = crypt($password, $hash);
225        if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
226            return false;
227        }
228
229        $status = 0;
230        for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
231            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
232        }
233
234        return $status === 0;
235    }
236}
237
238}
239
240namespace PasswordCompat\binary {
241    /**
242     * Count the number of bytes in a string
243     *
244     * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
245     * In this case, strlen() will count the number of *characters* based on the internal encoding. A
246     * sequence of bytes might be regarded as a single multibyte character.
247     *
248     * @param string $binary_string The input string
249     *
250     * @internal
251     * @return int The number of bytes
252     */
253    function _strlen($binary_string) {
254           if (function_exists('mb_strlen')) {
255               return mb_strlen($binary_string, '8bit');
256           }
257           return strlen($binary_string);
258    }
259
260    /**
261     * Get a substring based on byte limits
262     *
263     * @see _strlen()
264     *
265     * @param string $binary_string The input string
266     * @param int    $start
267     * @param int    $length
268     *
269     * @internal
270     * @return string The substring
271     */
272    function _substr($binary_string, $start, $length) {
273       if (function_exists('mb_substr')) {
274           return mb_substr($binary_string, $start, $length, '8bit');
275       }
276       return substr($binary_string, $start, $length);
277   }
278
279}