XpressEngine Core  1.11.2
 All Classes Namespaces Files Functions Variables Pages
Password.class.php
Go to the documentation of this file.
1 <?php
2 /* Copyright (C) NAVER <http://www.navercorp.com> */
3 
13 class Password
14 {
19  public function getSupportedAlgorithms()
20  {
21  $retval = array();
22  if(function_exists('hash_hmac') && in_array('sha256', hash_algos()))
23  {
24  $retval['pbkdf2'] = 'pbkdf2';
25  }
26  if(version_compare(PHP_VERSION, '5.3.7', '>=') && defined('CRYPT_BLOWFISH'))
27  {
28  $retval['bcrypt'] = 'bcrypt';
29  }
30  $retval['md5'] = 'md5';
31  return $retval;
32  }
33 
38  public function getBestAlgorithm()
39  {
40  $algos = $this->getSupportedAlgorithms();
41  return key($algos);
42  }
43 
49  {
50  if(function_exists('getModel'))
51  {
52  $config = getModel('member')->getMemberConfig();
53  $algorithm = $config->password_hashing_algorithm;
54  if(strval($algorithm) === '')
55  {
56  $algorithm = 'md5'; // Historical default for XE
57  }
58  }
59  else
60  {
61  $algorithm = 'md5';
62  }
63  return $algorithm;
64  }
65 
70  public function getWorkFactor()
71  {
72  if(function_exists('getModel'))
73  {
74  $config = getModel('member')->getMemberConfig();
75  $work_factor = $config->password_hashing_work_factor;
76  if(!$work_factor || $work_factor < 4 || $work_factor > 31)
77  {
78  $work_factor = 8; // Reasonable default
79  }
80  }
81  else
82  {
83  $work_factor = 8;
84  }
85  return $work_factor;
86  }
87 
94  public function createHash($password, $algorithm = null)
95  {
96  if($algorithm === null)
97  {
98  $algorithm = $this->getCurrentlySelectedAlgorithm();
99  }
100  if(!array_key_exists($algorithm, $this->getSupportedAlgorithms()))
101  {
102  return false;
103  }
104 
105  $password = trim($password);
106 
107  switch($algorithm)
108  {
109  case 'md5':
110  return md5($password);
111 
112  case 'pbkdf2':
113  $iterations = pow(2, $this->getWorkFactor() + 5);
114  $salt = $this->createSecureSalt(12, 'alnum');
115  $hash = base64_encode($this->pbkdf2($password, $salt, 'sha256', $iterations, 24));
116  return 'sha256:'.sprintf('%07d', $iterations).':'.$salt.':'.$hash;
117 
118  case 'bcrypt':
119  return $this->bcrypt($password);
120 
121  default:
122  return false;
123  }
124  }
125 
133  public function checkPassword($password, $hash, $algorithm = null)
134  {
135  if($algorithm === null)
136  {
137  $algorithm = $this->checkAlgorithm($hash);
138  }
139 
140  $password = trim($password);
141 
142  switch($algorithm)
143  {
144  case 'md5':
145  return md5($password) === $hash || md5(sha1(md5($password))) === $hash;
146 
147  case 'mysql_old_password':
148  return (class_exists('Context') && substr(Context::getDBType(), 0, 5) === 'mysql') ?
149  DB::getInstance()->isValidOldPassword($password, $hash) : false;
150 
151  case 'mysql_password':
152  return $hash[0] === '*' && substr($hash, 1) === strtoupper(sha1(sha1($password, true)));
153 
154  case 'pbkdf2':
155  $hash = explode(':', $hash);
156  $hash[3] = base64_decode($hash[3]);
157  $hash_to_compare = $this->pbkdf2($password, $hash[2], $hash[0], intval($hash[1], 10), strlen($hash[3]));
158  return $this->strcmpConstantTime($hash_to_compare, $hash[3]);
159 
160  case 'bcrypt':
161  $hash_to_compare = $this->bcrypt($password, $hash);
162  return $this->strcmpConstantTime($hash_to_compare, $hash);
163 
164  default:
165  return false;
166  }
167  }
168 
174  function checkAlgorithm($hash)
175  {
176  if(preg_match('/^\$2[axy]\$([0-9]{2})\$/', $hash, $matches))
177  {
178  return 'bcrypt';
179  }
180  elseif(preg_match('/^sha[0-9]+:([0-9]+):/', $hash, $matches))
181  {
182  return 'pbkdf2';
183  }
184  elseif(strlen($hash) === 32 && ctype_xdigit($hash))
185  {
186  return 'md5';
187  }
188  elseif(strlen($hash) === 16 && ctype_xdigit($hash))
189  {
190  return 'mysql_old_password';
191  }
192  elseif(strlen($hash) === 41 && $hash[0] === '*')
193  {
194  return 'mysql_password';
195  }
196  else
197  {
198  return false;
199  }
200  }
201 
207  function checkWorkFactor($hash)
208  {
209  if(preg_match('/^\$2[axy]\$([0-9]{2})\$/', $hash, $matches))
210  {
211  return intval($matches[1], 10);
212  }
213  elseif(preg_match('/^sha[0-9]+:([0-9]+):/', $hash, $matches))
214  {
215  return max(0, round(log($matches[1], 2)) - 5);
216  }
217  else
218  {
219  return false;
220  }
221  }
222 
229  public function createSecureSalt($length, $format = 'hex')
230  {
231  // Find out how many bytes of entropy we really need
232  switch($format)
233  {
234  case 'hex':
235  $entropy_required_bytes = ceil($length / 2);
236  break;
237  case 'alnum':
238  case 'printable':
239  $entropy_required_bytes = ceil($length * 3 / 4);
240  break;
241  default:
242  $entropy_required_bytes = $length;
243  }
244 
245  // Cap entropy to 256 bits from any one source, because anything more is meaningless
246  $entropy_capped_bytes = min(32, $entropy_required_bytes);
247 
248  // Find and use the most secure way to generate a random string
249  $is_windows = (defined('PHP_OS') && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
250  if(function_exists('openssl_random_pseudo_bytes') && (!$is_windows || version_compare(PHP_VERSION, '5.4', '>=')))
251  {
252  $entropy = openssl_random_pseudo_bytes($entropy_capped_bytes);
253  }
254  elseif(function_exists('mcrypt_create_iv') && (!$is_windows || version_compare(PHP_VERSION, '5.3.7', '>=')))
255  {
256  $entropy = mcrypt_create_iv($entropy_capped_bytes, MCRYPT_DEV_URANDOM);
257  }
258  elseif(function_exists('mcrypt_create_iv') && $is_windows)
259  {
260  $entropy = mcrypt_create_iv($entropy_capped_bytes, MCRYPT_RAND);
261  }
262  elseif(!$is_windows && @is_readable('/dev/urandom'))
263  {
264  $fp = fopen('/dev/urandom', 'rb');
265  $entropy = fread($fp, $entropy_capped_bytes);
266  fclose($fp);
267  }
268  else
269  {
270  $entropy = '';
271  for($i = 0; $i < $entropy_capped_bytes; $i += 2)
272  {
273  $entropy .= pack('S', rand(0, 65536) ^ mt_rand(0, 65535));
274  }
275  }
276 
277  // Mixing (see RFC 4086 section 5)
278  $output = '';
279  for($i = 0; $i < $entropy_required_bytes; $i += 32)
280  {
281  $output .= hash('sha256', $entropy . $i . rand(), true);
282  }
283 
284  // Encode and return the random string
285  switch($format)
286  {
287  case 'hex':
288  return substr(bin2hex($output), 0, $length);
289  case 'binary':
290  return substr($output, 0, $length);
291  case 'printable':
292  $salt = '';
293  for($i = 0; $i < $length; $i++)
294  {
295  $salt .= chr(33 + (crc32(sha1($i . $output)) % 94));
296  }
297  return $salt;
298  case 'alnum':
299  default:
300  $salt = substr(base64_encode($output), 0, $length);
301  $replacements = chr(rand(65, 90)) . chr(rand(97, 122)) . rand(0, 9);
302  return strtr($salt, '+/=', $replacements);
303  }
304  }
305 
311  public function createTemporaryPassword($length = 16)
312  {
313  while(true)
314  {
315  $source = base64_encode($this->createSecureSalt(64, 'binary'));
316  $source = strtr($source, 'iIoOjl10/', '@#$%&*-!?');
317  $source_length = strlen($source);
318  for($i = 0; $i < $source_length - $length; $i++)
319  {
320  $candidate = substr($source, $i, $length);
321  if(preg_match('/[a-z]/', $candidate) && preg_match('/[A-Z]/', $candidate) &&
322  preg_match('/[0-9]/', $candidate) && preg_match('/[^a-zA-Z0-9]/', $candidate))
323  {
324  return $candidate;
325  }
326  }
327  }
328  }
329 
335  public static function createSignature($string)
336  {
337  $key = self::getSecretKey();
338  $salt = self::createSecureSalt(8, 'alnum');
339  $hash = substr(base64_encode(hash_hmac('sha256', hash_hmac('sha256', $string, $salt), $key, true)), 0, 32);
340  return $salt . strtr($hash, '+/', '-_');
341  }
342 
349  public static function checkSignature($string, $signature)
350  {
351  if(strlen($signature) !== 40)
352  {
353  return false;
354  }
355 
356  $key = self::getSecretKey();
357  $salt = substr($signature, 0, 8);
358  $hash = substr(base64_encode(hash_hmac('sha256', hash_hmac('sha256', $string, $salt), $key, true)), 0, 32);
359  return self::strcmpConstantTime(substr($signature, 8), strtr($hash, '+/', '-_'));
360  }
361 
366  public static function getSecretKey()
367  {
368  // If the secret key does not exist, the config file needs to be updated
369  $db_info = Context::getDbInfo();
370  if(!isset($db_info->secret_key))
371  {
372  $db_info->secret_key = self::createSecureSalt(48, 'alnum');
373  Context::setDBInfo($db_info);
374  getController('install')->makeConfigFile();
375  }
376  return $db_info->secret_key;
377  }
378 
388  public function pbkdf2($password, $salt, $algorithm = 'sha256', $iterations = 8192, $length = 24)
389  {
390  if(function_exists('hash_pbkdf2'))
391  {
392  return hash_pbkdf2($algorithm, $password, $salt, $iterations, $length, true);
393  }
394  else
395  {
396  $output = '';
397  $block_count = ceil($length / strlen(hash($algorithm, '', true))); // key length divided by the length of one hash
398  for($i = 1; $i <= $block_count; $i++)
399  {
400  $last = $salt . pack('N', $i); // $i encoded as 4 bytes, big endian
401  $last = $xorsum = hash_hmac($algorithm, $last, $password, true); // first iteration
402  for($j = 1; $j < $iterations; $j++) // The other $count - 1 iterations
403  {
404  $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
405  }
406  $output .= $xorsum;
407  }
408  return substr($output, 0, $length);
409  }
410  }
411 
418  public function bcrypt($password, $salt = null)
419  {
420  if($salt === null)
421  {
422  $salt = '$2y$'.sprintf('%02d', $this->getWorkFactor()).'$'.$this->createSecureSalt(22, 'alnum');
423  }
424  return crypt($password, $salt);
425  }
426 
433  function strcmpConstantTime($a, $b)
434  {
435  $diff = strlen($a) ^ strlen($b);
436  $maxlen = min(strlen($a), strlen($b));
437  for($i = 0; $i < $maxlen; $i++)
438  {
439  $diff |= ord($a[$i]) ^ ord($b[$i]);
440  }
441  return $diff === 0;
442  }
443 }
444 /* End of file : Password.class.php */
445 /* Location: ./classes/security/Password.class.php */
getController($module_name)
Definition: func.inc.php:90
static getSecretKey()
Get the secret key for this site.
bcrypt($password, $salt=null)
Generate the bcrypt hash of a string using a salt.
$output
Definition: ko.install.php:193
static checkSignature($string, $signature)
Check whether a signature is valid.
createTemporaryPassword($length=16)
Generate a temporary password using the secure salt generator.
getCurrentlySelectedAlgorithm()
Return the currently selected hashing algorithm.
checkAlgorithm($hash)
Check the algorithm used to create a hash.
getSupportedAlgorithms()
Return the list of hashing algorithms supported by this server.
createSecureSalt($length, $format= 'hex')
Generate a cryptographically secure random string to use as a salt.
getInstance($db_type=NULL)
Definition: DB.class.php:142
pbkdf2($password, $salt, $algorithm= 'sha256', $iterations=8192, $length=24)
Generate the PBKDF2 hash of a string using a salt.
strcmpConstantTime($a, $b)
Compare two strings in constant time.
getModel($module_name)
Definition: func.inc.php:145
checkWorkFactor($hash)
Check the work factor of a hash.
setDBInfo($db_info)
static createSignature($string)
Create a digital signature to verify the authenticity of a string.
getWorkFactor()
Return the currently configured work factor for bcrypt and other adjustable algorithms.
createHash($password, $algorithm=null)
Create a hash using the specified algorithm.
getBestAlgorithm()
Return the best hashing algorithm supported by this server.
checkPassword($password, $hash, $algorithm=null)
Check if a password matches a hash.