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: * PHPass Portable Module
20: *
21: * This module supports Openwall's PHPass Portable Hash format. Information
22: * about the original implementation may be found at
23: * http://www.openwall.com/phpass/.
24: *
25: * Supported parameters:
26: *
27: * <ul>
28: * <li><b>ident:</b> The original implementation uses the identifier P, while
29: * phpBB3 uses H. Defaults to P.</li>
30: *
31: * <li><b>rounds:</b> Optional number of rounds to use. Must be an integer
32: * between 7 and 30 inclusive. This value is logarithmic, meaning the actual
33: * number of rounds will be 2^<rounds>. Defaults to 16.</li>
34: *
35: * <li><b>salt:</b> Optional salt string. If provided, it must be an 8
36: * character string containing only characters in the regex range
37: * [./0-9A-Za-z]. It is highly recommended that this parameter be left blank,
38: * in which case the library will generate a suitable salt for you.</li>
39: * </ul>
40: *
41: * @package PHPassLib\Hashes
42: * @author Ryan Chouinard <rchouinard@gmail.com>
43: * @copyright Copyright (c) 2012, Ryan Chouinard
44: * @license MIT License - http://www.opensource.org/licenses/mit-license.php
45: */
46: class Portable implements Hash
47: {
48:
49: const IDENT_PHPASS = 'P';
50: const IDENT_PHPBB = 'H';
51:
52: /**
53: * Generate a config string from an array.
54: *
55: * @param array $config Array of configuration options.
56: * @return string Configuration string.
57: * @throws InvalidArgumentException Throws an InvalidArgumentException if
58: * any passed-in configuration options are invalid.
59: */
60: public static function genConfig(array $config = array ())
61: {
62: $defaults = array (
63: 'ident' => self::IDENT_PHPASS,
64: 'rounds' => 16,
65: 'salt' => Utilities::encode64(Utilities::genRandomBytes(6)),
66: );
67: $config = array_merge($defaults, array_change_key_case($config, CASE_LOWER));
68:
69: $string = '*1';
70: if (self::validateOptions($config)) {
71: $charset = Utilities::CHARS_H64;
72: $string = sprintf('$%s$%s%s', $config['ident'], $charset[(int) $config['rounds']], $config['salt']);
73: }
74:
75: return $string;
76: }
77:
78: /**
79: * Parse a config string into an array.
80: *
81: * @param string $config Configuration string.
82: * @return array Array of configuration options or false on failure.
83: */
84: public static function parseConfig($config)
85: {
86: $options = false;
87: $matches = array ();
88: if (preg_match('/^\$(P|H)\$([5-9A-S]{1})([\.\/0-9A-Za-z]{8})/', $config, $matches)) {
89: $options = array (
90: 'ident' => $matches[1],
91: 'rounds' => strpos(Utilities::CHARS_H64, $config[3]),
92: 'salt' => $matches[3],
93: );
94:
95: try {
96: self::validateOptions($options);
97: } catch (InvalidArgumentException $e) {
98: $options = false;
99: }
100: }
101:
102: return $options;
103: }
104:
105: /**
106: * Generate a password hash using a config string.
107: *
108: * @param string $password Password string.
109: * @param string $config Configuration string.
110: * @return string Returns the hash string on success. On failure, one of
111: * *0 or *1 is returned.
112: */
113: public static function genHash($password, $config)
114: {
115: $hash = ($config == '*0') ? '*1' : '*0';
116:
117: $config = self::parseConfig($config);
118: if (is_array($config)) {
119: $rounds = (1 << $config['rounds']);
120: $checksum = md5($config['salt'] . $password, true);
121: do {
122: $checksum = md5($checksum . $password, true);
123: } while (--$rounds);
124: $hash = self::genConfig($config) . Utilities::encode64($checksum);
125: }
126:
127: return $hash;
128: }
129:
130: /**
131: * Generate a password hash using a config string or array.
132: *
133: * @param string $password Password string.
134: * @param string|array $config Optional config string or array of options.
135: * @return string Returns the hash string on success. On failure, one of
136: * *0 or *1 is returned.
137: * @throws InvalidArgumentException Throws an InvalidArgumentException if
138: * any passed-in configuration options are invalid.
139: */
140: public static function hash($password, $config = array ())
141: {
142: if (is_array($config)) {
143: $config = self::genConfig($config);
144: }
145:
146: return self::genHash($password, $config);
147: }
148:
149: /**
150: * Verify a password against a hash string.
151: *
152: * @param string $password Password string.
153: * @param string $hash Hash string.
154: * @return boolean Returns true if the password matches, false otherwise.
155: */
156: public static function verify($password, $hash)
157: {
158: return ($hash === self::hash($password, $hash));
159: }
160:
161: /**
162: * @param array $options
163: * @return boolean
164: * @throws InvalidArgumentException
165: */
166: protected static function validateOptions(array $options)
167: {
168: $options = array_change_key_case($options, CASE_LOWER);
169: foreach ($options as $option => $value) switch ($option) {
170:
171: case 'ident':
172: $idents = array (self::IDENT_PHPASS, self::IDENT_PHPBB);
173: if (!in_array($value, $idents)) {
174: throw new InvalidArgumentException('Invalid ident parameter');
175: }
176: break;
177:
178: case 'rounds':
179: if ($value < 7 || $value > 30) {
180: throw new InvalidArgumentException('Invalid rounds parameter');
181: }
182: break;
183:
184: case 'salt':
185: if (!preg_match('/^[\.\/0-9A-Za-z]{8}$/', $value)) {
186: throw new InvalidArgumentException('Invalid salt parameter');
187: }
188: break;
189:
190: default:
191: break;
192:
193: }
194:
195: return true;
196: }
197:
198: }
199: