1: <?php
2: /**
3: * PHP Password Library
4: *
5: * @package PHPassLib\Hashes
6: * @author Ryan Chouinard <rchouinard@gmail.com>
7: * @copyright Copyright (c) 2012, Ryan Chouinard
8: * @license MIT License - http://www.opensource.org/licenses/mit-license.php
9: * @version 3.0.0-dev
10: */
11:
12: namespace PHPassLib\Hash;
13:
14: use PHPassLib\Hash;
15: use PHPassLib\Utilities;
16: use PHPassLib\Exception\InvalidArgumentException;
17:
18: /**
19: * BCrypt Module
20: *
21: * BCrypt is based on a version of the Blowfish stream cipher, and features
22: * a variable number of rounds and a large salt. BCrypt is recommended for
23: * new applications.
24: *
25: * Supported parameters:
26: *
27: * <ul>
28: * <li><b>rounds:</b> Optional number of rounds to use. Must be an integer
29: * between 4 and 31 inclusive. This value is logarithmic, meaning the actual
30: * number of rounds will be 2^<rounds>. Defaults to 12.</li>
31: *
32: * <li><b>ident:</b> Identifier which specifies the version of the algorithm
33: * to use. The default of 2a is correct for most uses, but the following
34: * values are supported: 2a, 2y, 2x. For more information on what these mean,
35: * see http://php.net/security/crypt_blowfish.php.</li>
36: *
37: * <li><b>salt:</b> Optional salt string. If provided, it must be a 22
38: * character string containing only characters in the regex range
39: * [./0-9A-Za-z]. It is highly recommended that this parameter be left blank,
40: * in which case the library will generate a suitable salt for you.</li>
41: * </ul>
42: *
43: * This module uses PHP's native crypt() function, which has had native support
44: * for BCrypt since 5.3.0. PHP 5.3.7 introduced support for the 2x and 2y
45: * identifiers, and applications running on older versions will not be able
46: * to use the ident parameter with these values.
47: *
48: * @package PHPassLib\Hashes
49: * @author Ryan Chouinard <rchouinard@gmail.com>
50: * @copyright Copyright (c) 2012, Ryan Chouinard
51: * @license MIT License - http://www.opensource.org/licenses/mit-license.php
52: */
53: class BCrypt implements Hash
54: {
55:
56: /**
57: * Generate a config string from an array.
58: *
59: * @param array $config Array of configuration options.
60: * @return string Configuration string.
61: * @throws InvalidArgumentException Throws an InvalidArgumentException if
62: * any passed-in configuration options are invalid.
63: */
64: public static function genConfig(array $config = array ())
65: {
66: $defaults = array (
67: 'ident' => '2a',
68: 'rounds' => 12,
69: 'salt' => self::genSalt(),
70: );
71: $config = array_merge($defaults, array_change_key_case($config, CASE_LOWER));
72:
73: $string = '*1';
74: if (self::validateOptions($config)) {
75: $string = sprintf('$%s$%02d$%s', $config['ident'], (int) $config['rounds'], $config['salt']);
76: }
77:
78: return $string;
79: }
80:
81: /**
82: * Parse a config string into an array.
83: *
84: * @param string $config Configuration string.
85: * @return array Array of configuration options or false on failure.
86: */
87: public static function parseConfig($config)
88: {
89: $options = false;
90: $matches = array ();
91: if (preg_match('/^\$(2a|2y|2x)\$(\d{2})\$([\.\/0-9A-Za-z]{22})/', $config, $matches)) {
92: $options = array (
93: 'ident' => $matches[1],
94: 'rounds' => (int) $matches[2],
95: 'salt' => $matches[3],
96: );
97:
98: try {
99: self::validateOptions($options);
100: } catch (InvalidArgumentException $e) {
101: $options = false;
102: }
103: }
104:
105: return $options;
106: }
107:
108: /**
109: * Generate a password hash using a config string.
110: *
111: * @param string $password Password string.
112: * @param string $config Configuration string.
113: * @return string Returns the hash string on success. On failure, one of
114: * *0 or *1 is returned.
115: */
116: public static function genHash($password, $config)
117: {
118: $hash = crypt($password, $config);
119: if (!preg_match('/^\$(?:2a|2y|2x)\$\d{2}\$[\.\/0-9A-Za-z]{53}$/', $hash)) {
120: $hash = ($config == '*0') ? '*1' : '*0';
121: }
122:
123: return $hash;
124: }
125:
126: /**
127: * Generate a password hash using a config string or array.
128: *
129: * @param string $password Password string.
130: * @param string|array $config Optional config string or array of options.
131: * @return string Returns the hash string on success. On failure, one of
132: * *0 or *1 is returned.
133: * @throws InvalidArgumentException Throws an InvalidArgumentException if
134: * any passed-in configuration options are invalid.
135: */
136: public static function hash($password, $config = array ())
137: {
138: if (is_array($config)) {
139: $config = self::genConfig($config);
140: }
141:
142: return self::genHash($password, $config);
143: }
144:
145: /**
146: * Verify a password against a hash string.
147: *
148: * @param string $password Password string.
149: * @param string $hash Hash string.
150: * @return boolean Returns true if the password matches, false otherwise.
151: */
152: public static function verify($password, $hash)
153: {
154: return ($hash === self::hash($password, $hash));
155: }
156:
157: /**
158: * @param string $input
159: * @return string
160: */
161: protected static function genSalt($input = null)
162: {
163: if (!$input) {
164: $input = Utilities::genRandomBytes(16);
165: }
166: $count = strlen($input);
167:
168: $atoi64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
169: $output = '';
170: $i = 0;
171: do {
172: $c1 = ord($input[$i++]);
173: $output .= $atoi64[$c1 >> 2];
174: $c1 = ($c1 & 0x03) << 4;
175: if ($i >= $count) {
176: $output .= $atoi64[$c1];
177: break;
178: }
179:
180: $c2 = ord($input[$i++]);
181: $c1 |= $c2 >> 4;
182: $output .= $atoi64[$c1];
183: $c1 = ($c2 & 0x0f) << 2;
184:
185: $c2 = ord($input[$i++]);
186: $c1 |= $c2 >> 6;
187: $output .= $atoi64[$c1];
188: $output .= $atoi64[$c2 & 0x3f];
189: } while (1);
190:
191: return $output;
192: }
193:
194: /**
195: * @param array $options
196: * @return boolean
197: * @throws InvalidArgumentException
198: */
199: protected static function validateOptions(array $options)
200: {
201: $options = array_change_key_case($options, CASE_LOWER);
202: foreach ($options as $option => $value) switch ($option) {
203:
204: case 'ident':
205: $idents = (version_compare('5.3.7', PHP_VERSION) === 1)
206: ? array ('2a') // <= 5.3.6
207: : array ('2a', '2y', '2x'); // >= 5.3.7
208: if (!in_array($value, $idents)) {
209: throw new InvalidArgumentException('Invalid ident parameter');
210: }
211: break;
212:
213: case 'rounds':
214: if ($value < 4 || $value > 31) {
215: throw new InvalidArgumentException('Invalid rounds parameter');
216: }
217: break;
218:
219: case 'salt':
220: if (!preg_match('/^[\.\/0-9A-Za-z]{22}$/', $value)) {
221: throw new InvalidArgumentException('Invalid salt parameter');
222: }
223: break;
224:
225: default:
226: break;
227:
228: }
229:
230: return true;
231: }
232:
233: }
234: