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: use PHPassLib\Exception\RuntimeException;
18:
19: /**
20: * PBKDF2-SHA1/256/512 Module
21: *
22: * This module provides three hash schemes compatible with Python PassLib's
23: * pbkdf2_<digest> schemes. PBKDF2-SHA512 is recommended for new applications.
24: *
25: * See http://packages.python.org/passlib/lib/passlib.hash.pbkdf2_digest.html
26: * for more details about this hash scheme.
27: *
28: * The PBKDF2 specification uses the HMAC variant of the SHA-1 hash function,
29: * which is not vulnerable to any of the known SHA-1 weaknesses. Any of the
30: * three digests are perfectly safe to use.
31: *
32: * Supported parameters:
33: *
34: * <ul>
35: * <li><b>digest:</b> Must be one of sha1, sha256, or sha512. Defaults to
36: * sha512.</li>
37: *
38: * <li><b>rounds:</b> Optional number of rounds to use. Must be an integer
39: * between 1 and 4294967296 inclusive. Defaults to 12000.</li>
40: *
41: * <li><b>saltSize:</b> Optional number of bytes to use when generating new
42: * salts. Must be an integer between 0 and 1024 inclusive. Defaults to 16.</li>
43: *
44: * <li><b>salt:</b> Optional salt string. If provided, it must be a string
45: * between 0 and 1024 characters in length. It is highly recommended that
46: * this parameter be left blank, in which case the library will generate a
47: * suitable salt for you.</li>
48: * </ul>
49: *
50: * This module requires the <i>HASH Message Digest Framework</i> extension to
51: * be loaded in order to work.
52: *
53: * @package PHPassLib\Hashes
54: * @author Ryan Chouinard <rchouinard@gmail.com>
55: * @copyright Copyright (c) 2012, Ryan Chouinard
56: * @license MIT License - http://www.opensource.org/licenses/mit-license.php
57: */
58: class PBKDF2 implements Hash
59: {
60:
61: const DIGEST_SHA1 = 'sha1';
62: const DIGEST_SHA256 = 'sha256';
63: const DIGEST_SHA512 = 'sha512';
64:
65: /**
66: * Generate a config string from an array.
67: *
68: * @param array $config Array of configuration options.
69: * @return string Configuration string.
70: * @throws InvalidArgumentException Throws an InvalidArgumentException if
71: * any passed-in configuration options are invalid.
72: */
73: public static function genConfig(array $config = array ())
74: {
75: $defaults = array (
76: 'digest' => self::DIGEST_SHA512,
77: 'rounds' => 12000,
78: 'salt' => null,
79: 'saltsize' => 16,
80: );
81: $config = array_merge($defaults, array_change_key_case($config, CASE_LOWER));
82:
83: $string = '*1';
84: if (self::validateOptions($config)) {
85: // Generate a salt value if we need one
86: if ($config['salt'] === null && (int) $config['saltsize'] > 0) {
87: $config['salt'] = self::genSalt(Utilities::genRandomBytes((int) $config['saltsize']));
88: }
89:
90: // pbkdf2-sha1 doesn't include the digest in the hash identifier
91: // We also have to treat the rounds parameter as a float, otherwise
92: // values above 2147483647 will wrap on 32-bit systems.
93: $string = str_replace('-sha1', '', sprintf('$pbkdf2-%s$%0.0f$%s', $config['digest'], $config['rounds'], $config['salt']));
94: }
95:
96: return $string;
97: }
98:
99: /**
100: * Parse a config string into an array.
101: *
102: * @param string $config Configuration string.
103: * @return array Array of configuration options or false on failure.
104: */
105: public static function parseConfig($config)
106: {
107: $options = false;
108: $matches = array ();
109: if (preg_match('/^\$pbkdf2(?:-(sha256|sha512))?\$(\d+)\$([\.\/0-9A-Za-z]{0,1366})\$?/', $config, $matches)) {
110: $options = array (
111: 'digest' => $matches[1] ?: 'sha1',
112: 'rounds' => $matches[2],
113: 'salt' => $matches[3],
114: 'saltSize' => $matches[3] ? strlen(Utilities::altBase64Decode($matches[3])) : 0,
115: );
116:
117: try {
118: self::validateOptions($options);
119: } catch (InvalidArgumentException $e) {
120: $options = false;
121: }
122: }
123:
124: return $options;
125: }
126:
127: /**
128: * Generate a password hash using a config string.
129: *
130: * @param string $password Password string.
131: * @param string $config Configuration string.
132: * @return string Returns the hash string on success. On failure, one of
133: * *0 or *1 is returned.
134: * @throws RuntimeException Throws a RuntimeException if the required
135: * HASH Message Digest Framework is not not loaded.
136: */
137: public static function genHash($password, $config)
138: {
139: $hash = ($config == '*0') ? '*1' : '*0';
140:
141: $config = self::parseConfig($config);
142: if (is_array($config)) {
143: $keysize = 64;
144: if ($config['digest'] == self::DIGEST_SHA256) {
145: $keysize = 32;
146: } elseif ($config['digest'] == self::DIGEST_SHA1) {
147: $keysize = 20;
148: }
149:
150: // hashPbkdf2() will throw a runtime exception if ext-hash
151: // is not loaded.
152: $checksum = self::hashPbkdf2($password, Utilities::altBase64Decode($config['salt']), $config['rounds'], $keysize, $config['digest']);
153: $hash = self::genConfig($config) . '$' . Utilities::altBase64Encode($checksum);
154: }
155:
156: return $hash;
157: }
158:
159: /**
160: * Generate a password hash using a config string or array.
161: *
162: * @param string $password Password string.
163: * @param string|array $config Optional config string or array of options.
164: * @return string Returns the hash string on success. On failure, one of
165: * *0 or *1 is returned.
166: * @throws InvalidArgumentException Throws an InvalidArgumentException if
167: * any passed-in configuration options are invalid.
168: * @throws RuntimeException Throws a RuntimeException if the required
169: * HASH Message Digest Framework is not not loaded.
170: */
171: public static function hash($password, $config = array ())
172: {
173: if (is_array($config)) {
174: $config = self::genConfig($config);
175: }
176:
177: return self::genHash($password, $config);
178: }
179:
180: /**
181: * Verify a password against a hash string.
182: *
183: * @param string $password Password string.
184: * @param string $hash Hash string.
185: * @return boolean Returns true if the password matches, false otherwise.
186: */
187: public static function verify($password, $hash)
188: {
189: return ($hash === self::hash($password, $hash));
190: }
191:
192: /**
193: * @param string $input
194: * @return string
195: */
196: protected static function genSalt($input = null)
197: {
198: if (!$input) {
199: $input = Utilities::genRandomBytes(16);
200: }
201:
202: return Utilities::altBase64Encode($input);
203: }
204:
205: /**
206: * Implementation of the PBKDF2 algorithm.
207: *
208: * @param string $password Password string.
209: * @param string $salt Salt string.
210: * @param integer $rounds Number of rounds to use.
211: * @param integer $keyLength Desired length of key.
212: * @param string $digest Digest to use.
213: * @return string Returns the raw byte string of the derived key.
214: * @throws RuntimeException Throws a RuntimeException if the required
215: * HASH Message Digest Framework is not not loaded.
216: */
217: protected static function hashPbkdf2($password, $salt, $rounds = 12000, $keyLength = 64, $digest = 'sha512')
218: {
219: if (!extension_loaded('hash')) {
220: throw new RuntimeException('Required extension "hash" not loaded: PBKDF2 requires the HASH Message Digest Framework.');
221: }
222:
223: $hashLength = strlen(hash($digest, null, true));
224: $keyBlocks = ceil($keyLength / $hashLength);
225: $derivedKey = '';
226:
227: for ($block = 1; $block <= $keyBlocks; ++$block) {
228: $iteratedBlock = $currentBlock = hash_hmac($digest, $salt . pack('N', $block), $password, true);
229: for ($iteration = 1; $iteration < $rounds; ++$iteration) {
230: $iteratedBlock ^= $currentBlock = hash_hmac($digest, $currentBlock, $password, true);
231: }
232:
233: $derivedKey .= $iteratedBlock;
234: }
235:
236: return substr($derivedKey, 0, $keyLength);
237: }
238:
239:
240: /**
241: * @param array $options
242: * @return boolean
243: * @throws InvalidArgumentException
244: */
245: protected static function validateOptions(array $options)
246: {
247: $options = array_change_key_case($options, CASE_LOWER);
248: foreach ($options as $option => $value) switch ($option) {
249:
250: case 'digest':
251: if (!in_array($value, array (self::DIGEST_SHA1, self::DIGEST_SHA256, self::DIGEST_SHA512))) {
252: throw new InvalidArgumentException('Invalid digest parameter');
253: }
254: break;
255:
256: case 'rounds':
257: if (substr($value, 0, 1) == 0 || $value < 1 || $value > 4294967296) {
258: throw new InvalidArgumentException('Invalid rounds parameter');
259: }
260: break;
261:
262: case 'saltsize':
263: if ($value > 1024) {
264: throw new InvalidArgumentException('Invalid salt size parameter');
265: }
266: break;
267:
268: case 'salt':
269: if (!preg_match('/^[\.\/0-9A-Za-z]{0,1366}$/', $value)) {
270: throw new InvalidArgumentException('Invalid salt parameter');
271: }
272: break;
273:
274: default:
275: break;
276:
277: }
278:
279: return true;
280: }
281:
282: }
283: