1: <?php
2: /**
3: * Wei Framework
4: *
5: * @copyright Copyright (c) 2008-2015 Twin Huang
6: * @license http://opensource.org/licenses/mit-license.php MIT License
7: */
8:
9: namespace Wei;
10:
11: /**
12: * A wrapper class for password hashing functions
13: *
14: * If you needs these original functions, please checkout
15: * https://github.com/ircmaxell/password_compat instead.
16: *
17: * - password_hash
18: * - password_get_info
19: * - password_needs_rehash
20: * - password_verify
21: *
22: * @author Anthony Ferrara <ircmaxell@php.net>
23: * @author https://github.com/ircmaxell/password_compat/graphs/contributors
24: * @author Twin Huang <twinhuang@qq.com>
25: * @link https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
26: */
27: class Password extends Base
28: {
29: /**
30: * The cost parameter for bcrypt
31: *
32: * @var int
33: */
34: protected $cost = 10;
35:
36: /**
37: * Constructor
38: *
39: * @param array $options
40: * @throws \Exception
41: */
42: public function __construct(array $options = array())
43: {
44: parent::__construct($options);
45:
46: if (!defined('PASSWORD_BCRYPT')) {
47: define('PASSWORD_BCRYPT', 1);
48: define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
49: }
50: }
51:
52: /**
53: * Hash the password using the specified algorithm
54: *
55: * @param string $password The password to hash
56: * @param string $salt The salt string for the algorithm to use
57: * @throws \InvalidArgumentException
58: * @return string|false The hashed password, or false on error.
59: */
60: public function hash($password, $salt = null)
61: {
62: $hash_format = sprintf("$2y$%02d$", $this->cost);
63:
64: !$salt && $salt = $this->generateSalt();
65: if (strlen($salt) < 22) {
66: throw new \InvalidArgumentException(sprintf("Provided salt is too short: %d expecting %d", strlen($salt), 22));
67: }
68:
69: $hash = $hash_format . $salt;
70: $ret = crypt($password, $hash);
71:
72: if (!is_string($ret) || strlen($ret) <= 13) {
73: return false;
74: }
75:
76: return $ret;
77: }
78:
79: /**
80: * Get information about the password hash. Returns an array of the information
81: * that was used to generate the password hash.
82: *
83: * array(
84: * 'algo' => 1,
85: * 'algoName' => 'bcrypt',
86: * 'options' => array(
87: * 'cost' => 10,
88: * ),
89: * )
90: *
91: * @param string $hash The password hash to extract info from
92: *
93: * @return array The array of information about the hash.
94: */
95: public function getInfo($hash)
96: {
97: $return = array(
98: 'algo' => 0,
99: 'algoName' => 'unknown',
100: 'options' => array(),
101: );
102: if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
103: $return['algo'] = PASSWORD_BCRYPT;
104: $return['algoName'] = 'bcrypt';
105: list($cost) = sscanf($hash, "$2y$%d$");
106: $return['options']['cost'] = $cost;
107: }
108: return $return;
109: }
110:
111: /**
112: * Determine if the password hash needs to be rehashed according to the options provided
113: *
114: * If the answer is true, after validating the password using password_verify, rehash it.
115: *
116: * @param string $hash The hash to test
117: * @param int $algo The algorithm used for new password hashes
118: * @param array $options The options array passed to password_hash
119: *
120: * @return boolean True if the password needs to be rehashed.
121: */
122: public function needsRehash($hash, $algo, array $options = array())
123: {
124: $info = $this->getInfo($hash);
125: if ($info['algo'] != $algo) {
126: return true;
127: }
128: switch ($algo) {
129: case PASSWORD_BCRYPT:
130: $cost = isset($options['cost']) ? $options['cost'] : 10;
131: if ($cost != $info['options']['cost']) {
132: return true;
133: }
134: break;
135: }
136: return false;
137: }
138:
139: /**
140: * Verify a password against a hash using a timing attack resistant approach
141: *
142: * @param string $password The password to verify
143: * @param string $hash The hash to verify against
144: *
145: * @return boolean If the password matches the hash
146: */
147: public function verify($password, $hash)
148: {
149: $ret = crypt($password, $hash);
150: if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
151: return false;
152: }
153:
154: $status = 0;
155: for ($i = 0; $i < strlen($ret); $i++) {
156: $status |= (ord($ret[$i]) ^ ord($hash[$i]));
157: }
158:
159: return $status === 0;
160: }
161:
162: /**
163: * Set the cost parameter for bcrypt
164: *
165: * @param int $cost
166: * @return $this
167: * @throws \InvalidArgumentException
168: */
169: public function setCost($cost)
170: {
171: if ($cost < 4 || $cost > 31) {
172: throw new \InvalidArgumentException(sprintf("Invalid bcrypt cost parameter specified: %s", $cost));
173: }
174: $this->cost = $cost;
175: return $this;
176: }
177:
178: /**
179: * Generate a 22 bytes salt string
180: *
181: * @return string
182: */
183: public function generateSalt()
184: {
185: // The length of salt to generate
186: $raw_salt_len = 16;
187: // The length required in the final serialization
188: $required_salt_len = 22;
189:
190: $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
191: $salt = str_replace('+', '.', base64_encode($buffer));
192: $salt = substr($salt, 0, $required_salt_len);
193:
194: return $salt;
195: }
196: }
197: