1<?php
2
3/**
4 * This file contains an example helper classes for the php-scrypt extension.
5 *
6 * As with all cryptographic code; it is recommended that you use a tried and
7 * tested library which uses this library; rather than rolling your own.
8 *
9 * PHP version 5
10 *
11 * @category Security
12 * @package  Scrypt
13 * @author   Dominic Black <thephenix@gmail.com>
14 * @license  http://www.opensource.org/licenses/BSD-2-Clause BSD 2-Clause License
15 * @link     http://github.com/DomBlack/php-scrypt
16 */
17
18/**
19 * This class abstracts away from scrypt module, allowing for easy use.
20 *
21 * You can create a new hash for a password by calling Password::hash($password)
22 *
23 * You can check a password by calling Password::check($password, $hash)
24 *
25 * @category Security
26 * @package  Scrypt
27 * @author   Dominic Black <thephenix@gmail.com>
28 * @license  http://www.opensource.org/licenses/BSD-2-Clause BSD 2-Clause License
29 * @link     http://github.com/DomBlack/php-scrypt
30 */
31abstract class Password
32{
33
34    /**
35     *
36     * @var int The key length
37     */
38    private static $_keyLength = 32;
39
40    /**
41     * Get the byte-length of the given string
42     *
43     * @param string $str Input string
44     *
45     * @return int
46     */
47    protected static function strlen( $str ) {
48        static $isShadowed = null;
49
50        if ($isShadowed === null) {
51            $isShadowed = extension_loaded('mbstring') &&
52                ini_get('mbstring.func_overload') & 2;
53        }
54
55        if ($isShadowed) {
56            return mb_strlen($str, '8bit');
57        } else {
58            return strlen($str);
59        }
60    }
61
62    /**
63     * Generates a random salt
64     *
65     * @param int $length The length of the salt
66     *
67     * @return string The salt
68     */
69    public static function generateSalt($length = 8)
70    {
71        $buffer = '';
72        $buffer_valid = false;
73        if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
74            $buffer = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
75            if ($buffer) {
76                $buffer_valid = true;
77            }
78        }
79        if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
80            $cryptoStrong = false;
81            $buffer = openssl_random_pseudo_bytes($length, $cryptoStrong);
82            if ($buffer && $cryptoStrong) {
83                $buffer_valid = true;
84            }
85        }
86        if (!$buffer_valid && is_readable('/dev/urandom')) {
87            $f = fopen('/dev/urandom', 'r');
88            $read = static::strlen($buffer);
89            while ($read < $length) {
90                $buffer .= fread($f, $length - $read);
91                $read = static::strlen($buffer);
92            }
93            fclose($f);
94            if ($read >= $length) {
95                $buffer_valid = true;
96            }
97        }
98        if (!$buffer_valid || static::strlen($buffer) < $length) {
99            $bl = static::strlen($buffer);
100            for ($i = 0; $i < $length; $i++) {
101                if ($i < $bl) {
102                    $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
103                } else {
104                    $buffer .= chr(mt_rand(0, 255));
105                }
106            }
107        }
108        $salt = str_replace(array('+', '$'), array('.', ''), base64_encode($buffer));
109
110        return $salt;
111    }
112
113    /**
114     * Create a password hash
115     *
116     * @param string $password The clear text password
117     * @param string $salt     The salt to use, or null to generate a random one
118     * @param int    $N        The CPU difficultly (must be a power of 2, > 1)
119     * @param int    $r        The memory difficultly
120     * @param int    $p        The parallel difficultly
121     *
122     * @return string The hashed password
123     */
124    public static function hash($password, $salt = false, $N = 16384, $r = 8, $p = 1)
125    {
126        if ($salt === false)
127            $salt = self::generateSalt();
128
129        if (function_exists( 'scrypt' )) {
130            if ($N == 0 || ($N & ($N - 1)) != 0) {
131                throw new \InvalidArgumentException("N must be > 0 and a power of 2");
132            }
133
134            if ($N > PHP_INT_MAX / 128 / $r) {
135                throw new \InvalidArgumentException("Parameter N is too large");
136            }
137
138            if ($r > PHP_INT_MAX / 128 / $p) {
139                throw new \InvalidArgumentException("Parameter r is too large");
140            }
141
142            if ($salt !== false) {
143                // Remove dollar signs from the salt, as we use that as a separator.
144                $salt = str_replace(array('+', '$'), array('.', ''), base64_encode($salt));
145            }
146
147            $hash = scrypt($password, $salt, $N, $r, $p, self::$_keyLength);
148
149            return $N . '$' . $r . '$' . $p . '$' . $salt . '$' . $hash;
150        }
151
152        if (function_exists( 'password_hash' )) {
153            return password_hash($password, PASSWORD_DEFAULT);
154        }
155
156        return crypt($password,$salt);
157
158    }
159
160    /**
161     * Check a clear text password against a hash
162     *
163     * @param string $password The clear text password
164     * @param string $hash     The hashed password
165     *
166     * @return boolean If the clear text matches
167     */
168    public static function check($password, $hash)
169    {
170        // Is there actually a hash?
171        if (!$hash) {
172            return false;
173        }
174
175        $hcheck = false;
176
177        if (function_exists( 'scrypt' )) {
178            list ($N, $r, $p, $salt, $hash) = explode('$', $hash);
179
180            // No empty fields?
181            if (empty($N) or empty($r) or empty($p) or empty($salt) or empty($hash)) {
182                return false;
183            }
184
185            // Are numeric values numeric?
186            if (!is_numeric($N) or !is_numeric($r) or !is_numeric($p)) {
187                return false;
188            }
189
190            $calculated = scrypt($password, $salt, $N, $r, $p, self::$_keyLength);
191
192            // Use compareStrings to avoid timeing attacks
193            $hcheck = self::compareStrings($hash, $calculated);
194        }
195
196        if (!$hcheck && function_exists('password_verify')) {
197            $hcheck = password_verify($password, $hash);
198        }
199
200        if (!$hcheck && function_exists( 'crypt' )) {
201            $hcheck = (hash_equals($hash, crypt($password, $hash)));
202        }
203
204        return $hcheck;
205    }
206
207    /**
208     * Zend Framework (http://framework.zend.com/)
209     *
210     * @link      http://github.com/zendframework/zf2 for the canonical source repository
211     * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
212     * @license   http://framework.zend.com/license/new-bsd New BSD License
213     *
214     * Compare two strings to avoid timing attacks
215     *
216     * C function memcmp() internally used by PHP, exits as soon as a difference
217     * is found in the two buffers. That makes possible of leaking
218     * timing information useful to an attacker attempting to iteratively guess
219     * the unknown string (e.g. password).
220     *
221     * @param string $expected
222     * @param string $actual
223     *
224     * @return boolean If the two strings match.
225     */
226    public static function compareStrings($expected, $actual)
227    {
228        $expected    = (string) $expected;
229        $actual      = (string) $actual;
230        $lenExpected = static::strlen($expected);
231        $lenActual   = static::strlen($actual);
232        $len         = min($lenExpected, $lenActual);
233
234        $result = 0;
235        for ($i = 0; $i < $len; $i ++) {
236            $result |= ord($expected[$i]) ^ ord($actual[$i]);
237        }
238        $result |= $lenExpected ^ $lenActual;
239
240        return ($result === 0);
241    }
242}
243