Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

MDL-35332 lib: Improve security of hashed passwords

  • Loading branch information...
commit ec2d8ceb885754140b940ed100a78f9c1f35ff49 1 parent 6319737
Simon Coggins simoncoggins authored

Showing 27 changed files with 808 additions and 111 deletions. Show diff stats Hide diff stats

  1. +15 4 admin/tool/uploaduser/index.php
  2. +3 0  auth/db/auth.php
  3. +3 0  auth/email/auth.php
  4. +3 0  auth/ldap/auth.php
  5. +3 0  auth/manual/auth.php
  6. +3 0  auth/none/auth.php
  7. +3 0  auth/webservice/auth.php
  8. +1 1  backup/util/dbops/restore_dbops.class.php
  9. +31 29 config-dist.php
  10. +5 1 lib/cronlib.php
  11. +1 1  lib/db/install.xml
  12. +12 0 lib/db/upgrade.php
  13. +4 1 lib/installlib.php
  14. +142 31 lib/moodlelib.php
  15. +220 0 lib/password_compat/lib/password.php
  16. +37 0 lib/password_compat/readme_moodle.txt
  17. +36 0 lib/password_compat/tests/PasswordGetInfoTest.php
  18. +94 0 lib/password_compat/tests/PasswordHashTest.php
  19. +36 0 lib/password_compat/tests/PasswordNeedsRehashTest.php
  20. +39 0 lib/password_compat/tests/PasswordVerifyTest.php
  21. +0 2  lib/phpunit/bootstrap.php
  22. +1 1  lib/setuplib.php
  23. +114 0 lib/tests/moodlelib_test.php
  24. +1 0  phpunit.xml.dist
  25. +0 9 report/security/lang/en/report_security.php
  26. +0 30 report/security/locallib.php
  27. +1 1  version.php
19 admin/tool/uploaduser/index.php
@@ -618,7 +618,7 @@
618 618 // Do not mess with passwords of remote users.
619 619
620 620 } else if (!$isinternalauth) {
621   - $existinguser->password = 'not cached';
  621 + $existinguser->password = AUTH_PASSWORD_NOT_CACHED;
622 622 $upt->track('password', '-', 'normal', false);
623 623 // clean up prefs
624 624 unset_user_preference('create_password', $existinguser);
@@ -626,6 +626,8 @@
626 626
627 627 } else if (!empty($user->password)) {
628 628 if ($updatepasswords) {
  629 + // Check for passwords that we want to force users to reset next
  630 + // time they log in.
629 631 $errmsg = null;
630 632 $weak = !check_password_policy($user->password, $errmsg);
631 633 if ($resetpasswords == UU_PWRESET_ALL or ($resetpasswords == UU_PWRESET_WEAK and $weak)) {
@@ -638,7 +640,12 @@
638 640 unset_user_preference('auth_forcepasswordchange', $existinguser);
639 641 }
640 642 unset_user_preference('create_password', $existinguser); // no need to create password any more
641   - $existinguser->password = hash_internal_user_password($user->password);
  643 +
  644 + // Use a low cost factor when generating bcrypt hash otherwise
  645 + // hashing would be slow when uploading lots of users. Hashes
  646 + // will be automatically updated to a higher cost factor the first
  647 + // time the user logs in.
  648 + $existinguser->password = hash_internal_user_password($user->password, true);
642 649 $upt->track('password', $user->password, 'normal', false);
643 650 } else {
644 651 // do not print password when not changed
@@ -771,10 +778,14 @@
771 778 }
772 779 $forcechangepassword = true;
773 780 }
774   - $user->password = hash_internal_user_password($user->password);
  781 + // Use a low cost factor when generating bcrypt hash otherwise
  782 + // hashing would be slow when uploading lots of users. Hashes
  783 + // will be automatically updated to a higher cost factor the first
  784 + // time the user logs in.
  785 + $user->password = hash_internal_user_password($user->password, true);
775 786 }
776 787 } else {
777   - $user->password = 'not cached';
  788 + $user->password = AUTH_PASSWORD_NOT_CACHED;
778 789 $upt->track('password', '-', 'normal', false);
779 790 }
780 791
3  auth/db/auth.php
@@ -221,6 +221,9 @@ function user_update_password($user, $newpassword) {
221 221
222 222 if ($this->is_internal()) {
223 223 $puser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
  224 + // This will also update the stored hash to the latest algorithm
  225 + // if the existing hash is using an out-of-date algorithm (or the
  226 + // legacy md5 algorithm).
224 227 if (update_internal_user_password($puser, $newpassword)) {
225 228 $user->password = $puser->password;
226 229 return true;
3  auth/email/auth.php
@@ -59,6 +59,9 @@ function user_login ($username, $password) {
59 59 */
60 60 function user_update_password($user, $newpassword) {
61 61 $user = get_complete_user_data('id', $user->id);
  62 + // This will also update the stored hash to the latest algorithm
  63 + // if the existing hash is using an out-of-date algorithm (or the
  64 + // legacy md5 algorithm).
62 65 return update_internal_user_password($user, $newpassword);
63 66 }
64 67
3  auth/ldap/auth.php
@@ -529,6 +529,9 @@ function user_signup($user, $notify=true) {
529 529 profile_save_data($user);
530 530
531 531 $this->update_user_record($user->username);
  532 + // This will also update the stored hash to the latest algorithm
  533 + // if the existing hash is using an out-of-date algorithm (or the
  534 + // legacy md5 algorithm).
532 535 update_internal_user_password($user, $plainslashedpassword);
533 536
534 537 $user = $DB->get_record('user', array('id'=>$user->id));
3  auth/manual/auth.php
@@ -82,6 +82,9 @@ function user_login($username, $password) {
82 82 */
83 83 function user_update_password($user, $newpassword) {
84 84 $user = get_complete_user_data('id', $user->id);
  85 + // This will also update the stored hash to the latest algorithm
  86 + // if the existing hash is using an out-of-date algorithm (or the
  87 + // legacy md5 algorithm).
85 88 return update_internal_user_password($user, $newpassword);
86 89 }
87 90
3  auth/none/auth.php
@@ -59,6 +59,9 @@ function user_login ($username, $password) {
59 59 */
60 60 function user_update_password($user, $newpassword) {
61 61 $user = get_complete_user_data('id', $user->id);
  62 + // This will also update the stored hash to the latest algorithm
  63 + // if the existing hash is using an out-of-date algorithm (or the
  64 + // legacy md5 algorithm).
62 65 return update_internal_user_password($user, $newpassword);
63 66 }
64 67
3  auth/webservice/auth.php
@@ -85,6 +85,9 @@ function user_login_webservice($username, $password) {
85 85 */
86 86 function user_update_password($user, $newpassword) {
87 87 $user = get_complete_user_data('id', $user->id);
  88 + // This will also update the stored hash to the latest algorithm
  89 + // if the existing hash is using an out-of-date algorithm (or the
  90 + // legacy md5 algorithm).
88 91 return update_internal_user_password($user, $newpassword);
89 92 }
90 93
2  backup/util/dbops/restore_dbops.class.php
@@ -1052,7 +1052,7 @@ public static function create_included_users($basepath, $restoreid, $userid) {
1052 1052
1053 1053 // Most external plugins do not store passwords locally
1054 1054 if (!empty($userauth->preventpassindb)) {
1055   - $user->password = 'not cached';
  1055 + $user->password = AUTH_PASSWORD_NOT_CACHED;
1056 1056
1057 1057 // If Moodle is responsible for storing/validating pwd and reset functionality is available, mark
1058 1058 } else if ($userauth->isinternal and $userauth->canresetpwd) {
60 config-dist.php
@@ -63,28 +63,7 @@
63 63
64 64
65 65 //=========================================================================
66   -// 2. SECRET PASSWORD SALT
67   -//=========================================================================
68   -// User password salt is very important security feature, it is created
69   -// automatically in installer, you have to uncomment and modify value
70   -// on the next line if you are creating config.php manually.
71   -//
72   -// $CFG->passwordsaltmain = 'a_very_long_random_string_of_characters#@6&*1';
73   -//
74   -// After changing the main salt you have to copy old value into one
75   -// of the following settings - this allows migration to the new salt
76   -// during the next login of each user.
77   -//
78   -// $CFG->passwordsaltalt1 = '';
79   -// $CFG->passwordsaltalt2 = '';
80   -// $CFG->passwordsaltalt3 = '';
81   -// ....
82   -// $CFG->passwordsaltalt19 = '';
83   -// $CFG->passwordsaltalt20 = '';
84   -
85   -
86   -//=========================================================================
87   -// 3. WEB SITE LOCATION
  66 +// 2. WEB SITE LOCATION
88 67 //=========================================================================
89 68 // Now you need to tell Moodle where it is located. Specify the full
90 69 // web address to where moodle has been installed. If your web site
@@ -98,7 +77,7 @@
98 77
99 78
100 79 //=========================================================================
101   -// 4. DATA FILES LOCATION
  80 +// 3. DATA FILES LOCATION
102 81 //=========================================================================
103 82 // Now you need a place where Moodle can save uploaded files. This
104 83 // directory should be readable AND WRITEABLE by the web server user
@@ -114,7 +93,7 @@
114 93
115 94
116 95 //=========================================================================
117   -// 5. DATA FILES PERMISSIONS
  96 +// 4. DATA FILES PERMISSIONS
118 97 //=========================================================================
119 98 // The following parameter sets the permissions of new directories
120 99 // created by Moodle within the data directory. The format is in
@@ -128,7 +107,7 @@
128 107
129 108
130 109 //=========================================================================
131   -// 6. DIRECTORY LOCATION (most people can just ignore this setting)
  110 +// 5. DIRECTORY LOCATION (most people can just ignore this setting)
132 111 //=========================================================================
133 112 // A very few webhosts use /admin as a special URL for you to access a
134 113 // control panel or something. Unfortunately this conflicts with the
@@ -140,7 +119,7 @@
140 119
141 120
142 121 //=========================================================================
143   -// 7. OTHER MISCELLANEOUS SETTINGS (ignore these for new installations)
  122 +// 6. OTHER MISCELLANEOUS SETTINGS (ignore these for new installations)
144 123 //=========================================================================
145 124 //
146 125 // These are additional tweaks for which no GUI exists in Moodle yet.
@@ -471,7 +450,7 @@
471 450 // $CFG->svgicons = false;
472 451 //
473 452 //=========================================================================
474   -// 8. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
  453 +// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
475 454 //=========================================================================
476 455 //
477 456 // Force a debugging mode regardless the settings in the site administration
@@ -512,7 +491,7 @@
512 491 // $CFG->showcrondebugging = true;
513 492 //
514 493 //=========================================================================
515   -// 9. FORCED SETTINGS
  494 +// 8. FORCED SETTINGS
516 495 //=========================================================================
517 496 // It is possible to specify normal admin settings here, the point is that
518 497 // they can not be changed through the standard admin settings pages any more.
@@ -527,12 +506,35 @@
527 506 // 'otherplugin' => array('mysetting' => 'myvalue', 'thesetting' => 'thevalue'));
528 507 //
529 508 //=========================================================================
530   -// 10. PHPUNIT SUPPORT
  509 +// 9. PHPUNIT SUPPORT
531 510 //=========================================================================
532 511 // $CFG->phpunit_prefix = 'phpu_';
533 512 // $CFG->phpunit_dataroot = '/home/example/phpu_moodledata';
534 513 // $CFG->phpunit_directorypermissions = 02777; // optional
535 514 //
  515 +//
  516 +//=========================================================================
  517 +// 10. SECRET PASSWORD SALT
  518 +//=========================================================================
  519 +// A single site-wide password salt is no longer required *unless* you are
  520 +// upgrading an older version of Moodle (prior to 2.5), or if you are using
  521 +// a PHP version below 5.3.7. If upgrading, keep any values from your old
  522 +// config.php file. If you are using PHP < 5.3.7 set to a long random string
  523 +// below:
  524 +//
  525 +// $CFG->passwordsaltmain = 'a_very_long_random_string_of_characters#@6&*1';
  526 +//
  527 +// You may also have some alternative salts to allow migration from previously
  528 +// used salts.
  529 +//
  530 +// $CFG->passwordsaltalt1 = '';
  531 +// $CFG->passwordsaltalt2 = '';
  532 +// $CFG->passwordsaltalt3 = '';
  533 +// ....
  534 +// $CFG->passwordsaltalt19 = '';
  535 +// $CFG->passwordsaltalt20 = '';
  536 +//
  537 +//
536 538 //=========================================================================
537 539 // 11. BEHAT SUPPORT
538 540 //=========================================================================
6 lib/cronlib.php
@@ -216,7 +216,11 @@ function cron_run() {
216 216
217 217 // note: we can not send emails to suspended accounts
218 218 foreach ($newusers as $newuser) {
219   - if (setnew_password_and_mail($newuser)) {
  219 + // Use a low cost factor when generating bcrypt hash otherwise
  220 + // hashing would be slow when emailing lots of users. Hashes
  221 + // will be automatically updated to a higher cost factor the first
  222 + // time the user logs in.
  223 + if (setnew_password_and_mail($newuser, true)) {
220 224 unset_user_preference('create_password', $newuser);
221 225 set_user_preference('auth_forcepasswordchange', 1, $newuser);
222 226 } else {
2  lib/db/install.xml
@@ -753,7 +753,7 @@
753 753 <FIELD NAME="suspended" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="suspended flag prevents users to log in"/>
754 754 <FIELD NAME="mnethostid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
755 755 <FIELD NAME="username" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
756   - <FIELD NAME="password" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false"/>
  756 + <FIELD NAME="password" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
757 757 <FIELD NAME="idnumber" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
758 758 <FIELD NAME="firstname" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
759 759 <FIELD NAME="lastname" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
12 lib/db/upgrade.php
@@ -1564,6 +1564,18 @@ function xmldb_main_upgrade($oldversion) {
1564 1564 upgrade_main_savepoint(true, 2012120300.07);
1565 1565 }
1566 1566
  1567 + if ($oldversion < 2013020900.00) {
  1568 +
  1569 + // Changing precision of field password on table user to (255).
  1570 + $table = new xmldb_table('user');
  1571 + $field = new xmldb_field('password', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'username');
  1572 +
  1573 + // Launch change of precision for field password.
  1574 + $dbman->change_field_precision($table, $field);
  1575 +
  1576 + // Main savepoint reached.
  1577 + upgrade_main_savepoint(true, 2013020900.00);
  1578 + }
1567 1579
1568 1580 return true;
1569 1581 }
5 lib/installlib.php
@@ -233,7 +233,10 @@ function install_generate_configphp($database, $cfg) {
233 233 }
234 234 $configphp .= '$CFG->directorypermissions = ' . $chmod . ';' . PHP_EOL . PHP_EOL;
235 235
236   - $configphp .= '$CFG->passwordsaltmain = '.var_export(complex_random_string(), true) . ';' . PHP_EOL . PHP_EOL;
  236 + // A site-wide salt is only needed if bcrypt is not properly supported by the current version of PHP.
  237 + if (password_compat_not_supported()) {
  238 + $configphp .= '$CFG->passwordsaltmain = '.var_export(complex_random_string(), true) . ';' . PHP_EOL . PHP_EOL;
  239 + }
237 240
238 241 $configphp .= 'require_once(dirname(__FILE__) . \'/lib/setup.php\');' . PHP_EOL . PHP_EOL;
239 242 $configphp .= '// There is no php closing tag in this file,' . PHP_EOL;
173 lib/moodlelib.php
@@ -493,6 +493,11 @@
493 493 define('COURSE_DISPLAY_SINGLEPAGE', 0); // display all sections on one page
494 494 define('COURSE_DISPLAY_MULTIPAGE', 1); // split pages into a page per section
495 495
  496 +/**
  497 + * Authentication constants.
  498 + */
  499 +define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); // String used in password field when password is not stored.
  500 +
496 501 /// PARAMETER HANDLING ////////////////////////////////////////////////////
497 502
498 503 /**
@@ -3845,6 +3850,7 @@ function create_user_record($username, $password, $auth = 'manual') {
3845 3850 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})){
3846 3851 set_user_preference('auth_forcepasswordchange', 1, $user);
3847 3852 }
  3853 + // Set the password.
3848 3854 update_internal_user_password($user, $password);
3849 3855
3850 3856 // fetch full user record for the event, the complete user data contains too much info
@@ -4197,7 +4203,10 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
4197 4203 $user->auth = $auth;
4198 4204 }
4199 4205
4200   - update_internal_user_password($user, $password); // just in case salt or encoding were changed (magic quotes too one day)
  4206 + // If the existing hash is using an out-of-date algorithm (or the
  4207 + // legacy md5 algorithm), then we should update to the current
  4208 + // hash algorithm while we have access to the user's password.
  4209 + update_internal_user_password($user, $password);
4201 4210
4202 4211 if ($authplugin->is_synchronised_with_external()) { // update user record from external DB
4203 4212 $user = update_user_record($username);
@@ -4307,28 +4316,81 @@ function complete_user_login($user) {
4307 4316 }
4308 4317
4309 4318 /**
4310   - * Compare password against hash stored in internal user table.
4311   - * If necessary it also updates the stored hash to new format.
  4319 + * Check a password hash to see if it was hashed using the
  4320 + * legacy hash algorithm (md5).
  4321 + *
  4322 + * @param string $password String to check.
  4323 + * @return boolean True if the $password matches the format of an md5 sum.
  4324 + */
  4325 +function password_is_legacy_hash($password) {
  4326 + return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
  4327 +}
  4328 +
  4329 +/**
  4330 + * Checks whether the password compatibility library will work with the current
  4331 + * version of PHP. This cannot be done using PHP version numbers since the fix
  4332 + * has been backported to earlier versions in some distributions.
  4333 + *
  4334 + * See https://github.com/ircmaxell/password_compat/issues/10 for
  4335 + * more details.
  4336 + *
  4337 + * @return bool True if the library is NOT supported.
  4338 + */
  4339 +function password_compat_not_supported() {
  4340 +
  4341 + $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
  4342 +
  4343 + // Create a one off application cache to store bcrypt support status as
  4344 + // the support status doesn't change and crypt() is slow.
  4345 + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'password_compat');
  4346 +
  4347 + if (!$bcryptsupport = $cache->get('bcryptsupport')) {
  4348 + $test = crypt('password', $hash);
  4349 + // Cache string instead of boolean to avoid MDL-37472.
  4350 + if ($test == $hash) {
  4351 + $bcryptsupport = 'supported';
  4352 + } else {
  4353 + $bcryptsupport = 'not supported';
  4354 + }
  4355 + $cache->set('bcryptsupport', $bcryptsupport);
  4356 + }
  4357 +
  4358 + // Return true if bcrypt *not* supported.
  4359 + return ($bcryptsupport !== 'supported');
  4360 +}
  4361 +
  4362 +/**
  4363 + * Compare password against hash stored in user object to determine if it is valid.
  4364 + *
  4365 + * If necessary it also updates the stored hash to the current format.
4312 4366 *
4313   - * @param stdClass $user (password property may be updated)
4314   - * @param string $password plain text password
4315   - * @return bool is password valid?
  4367 + * @param stdClass $user (Password property may be updated).
  4368 + * @param string $password Plain text password.
  4369 + * @return bool True if password is valid.
4316 4370 */
4317 4371 function validate_internal_user_password($user, $password) {
4318 4372 global $CFG;
  4373 + require_once($CFG->libdir.'/password_compat/lib/password.php');
4319 4374
4320   - if (!isset($CFG->passwordsaltmain)) {
4321   - $CFG->passwordsaltmain = '';
  4375 + if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
  4376 + // Internal password is not used at all, it can not validate.
  4377 + return false;
4322 4378 }
4323 4379
4324   - $validated = false;
  4380 + // If hash isn't a legacy (md5) hash, validate using the library function.
  4381 + if (!password_is_legacy_hash($user->password)) {
  4382 + return password_verify($password, $user->password);
  4383 + }
4325 4384
4326   - if ($user->password === 'not cached') {
4327   - // internal password is not used at all, it can not validate
  4385 + // Otherwise we need to check for a legacy (md5) hash instead. If the hash
  4386 + // is valid we can then update it to the new algorithm.
4328 4387
4329   - } else if ($user->password === md5($password.$CFG->passwordsaltmain)
  4388 + $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
  4389 + $validated = false;
  4390 +
  4391 + if ($user->password === md5($password.$sitesalt)
4330 4392 or $user->password === md5($password)
4331   - or $user->password === md5(addslashes($password).$CFG->passwordsaltmain)
  4393 + or $user->password === md5(addslashes($password).$sitesalt)
4332 4394 or $user->password === md5(addslashes($password))) {
4333 4395 // note: we are intentionally using the addslashes() here because we
4334 4396 // need to accept old password hashes of passwords with magic quotes
@@ -4347,7 +4409,8 @@ function validate_internal_user_password($user, $password) {
4347 4409 }
4348 4410
4349 4411 if ($validated) {
4350   - // force update of password hash using latest main password salt and encoding if needed
  4412 + // If the password matches the existing md5 hash, update to the
  4413 + // current hash algorithm while we have access to the user's password.
4351 4414 update_internal_user_password($user, $password);
4352 4415 }
4353 4416
@@ -4355,39 +4418,85 @@ function validate_internal_user_password($user, $password) {
4355 4418 }
4356 4419
4357 4420 /**
4358   - * Calculate hashed value from password using current hash mechanism.
  4421 + * Calculate hash for a plain text password.
  4422 + *
  4423 + * @param string $password Plain text password to be hashed.
  4424 + * @param bool $fasthash If true, use a low cost factor when generating the hash
  4425 + * This is much faster to generate but makes the hash
  4426 + * less secure. It is used when lots of hashes need to
  4427 + * be generated quickly.
  4428 + * @return string The hashed password.
4359 4429 *
4360   - * @param string $password
4361   - * @return string password hash
  4430 + * @throws moodle_exception If a problem occurs while generating the hash.
4362 4431 */
4363   -function hash_internal_user_password($password) {
  4432 +function hash_internal_user_password($password, $fasthash = false) {
4364 4433 global $CFG;
  4434 + require_once($CFG->libdir.'/password_compat/lib/password.php');
4365 4435
4366   - if (isset($CFG->passwordsaltmain)) {
4367   - return md5($password.$CFG->passwordsaltmain);
4368   - } else {
4369   - return md5($password);
  4436 + // Use the legacy hashing algorithm (md5) if PHP is not new enough
  4437 + // to support bcrypt properly
  4438 + if (password_compat_not_supported()) {
  4439 + if (isset($CFG->passwordsaltmain)) {
  4440 + return md5($password.$CFG->passwordsaltmain);
  4441 + } else {
  4442 + return md5($password);
  4443 + }
  4444 + }
  4445 +
  4446 + // Set the cost factor to 4 for fast hashing, otherwise use default cost.
  4447 + $options = ($fasthash) ? array('cost' => 4) : array();
  4448 +
  4449 + $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
  4450 +
  4451 + if ($generatedhash === false) {
  4452 + throw new moodle_exception('Failed to generate password hash.');
4370 4453 }
  4454 +
  4455 + return $generatedhash;
4371 4456 }
4372 4457
4373 4458 /**
4374   - * Update password hash in user object.
  4459 + * Update password hash in user object (if necessary).
4375 4460 *
4376   - * @param stdClass $user (password property may be updated)
4377   - * @param string $password plain text password
4378   - * @return bool always returns true
  4461 + * The password is updated if:
  4462 + * 1. The password has changed (the hash of $user->password is different
  4463 + * to the hash of $password).
  4464 + * 2. The existing hash is using an out-of-date algorithm (or the legacy
  4465 + * md5 algorithm).
  4466 + *
  4467 + * Updating the password will modify the $user object and the database
  4468 + * record to use the current hashing algorithm.
  4469 + *
  4470 + * @param stdClass $user User object (password property may be updated).
  4471 + * @param string $password Plain text password.
  4472 + * @return bool Always returns true.
4379 4473 */
4380 4474 function update_internal_user_password($user, $password) {
4381   - global $DB;
  4475 + global $CFG, $DB;
  4476 + require_once($CFG->libdir.'/password_compat/lib/password.php');
  4477 +
  4478 + // Use the legacy hashing algorithm (md5) if PHP doesn't support
  4479 + // bcrypt properly.
  4480 + $legacyhash = password_compat_not_supported();
4382 4481
  4482 + // Figure out what the hashed password should be.
4383 4483 $authplugin = get_auth_plugin($user->auth);
4384 4484 if ($authplugin->prevent_local_passwords()) {
4385   - $hashedpassword = 'not cached';
  4485 + $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4386 4486 } else {
4387 4487 $hashedpassword = hash_internal_user_password($password);
4388 4488 }
4389 4489
4390   - if ($user->password !== $hashedpassword) {
  4490 + if ($legacyhash) {
  4491 + $passwordchanged = ($user->password !== $hashedpassword);
  4492 + $algorithmchanged = false;
  4493 + } else {
  4494 + // If verification fails then it means the password has changed.
  4495 + $passwordchanged = !password_verify($password, $user->password);
  4496 + $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
  4497 + }
  4498 +
  4499 + if ($passwordchanged || $algorithmchanged) {
4391 4500 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
4392 4501 $user->password = $hashedpassword;
4393 4502 }
@@ -5588,9 +5697,10 @@ function generate_email_supportuser() {
5588 5697 * @global object
5589 5698 * @global object
5590 5699 * @param user $user A {@link $USER} object
  5700 + * @param boolean $fasthash If true, use a low cost factor when generating the hash for speed.
5591 5701 * @return boolean|string Returns "true" if mail was sent OK and "false" if there was an error
5592 5702 */
5593   -function setnew_password_and_mail($user) {
  5703 +function setnew_password_and_mail($user, $fasthash = false) {
5594 5704 global $CFG, $DB;
5595 5705
5596 5706 // we try to send the mail in language the user understands,
@@ -5604,7 +5714,8 @@ function setnew_password_and_mail($user) {
5604 5714
5605 5715 $newpassword = generate_password();
5606 5716
5607   - $DB->set_field('user', 'password', hash_internal_user_password($newpassword), array('id'=>$user->id));
  5717 + $hashedpassword = hash_internal_user_password($newpassword, $fasthash);
  5718 + $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
5608 5719
5609 5720 $a = new stdClass();
5610 5721 $a->firstname = fullname($user, true);
220 lib/password_compat/lib/password.php
... ... @@ -0,0 +1,220 @@
  1 +<?php
  2 +/**
  3 + * A Compatibility library with PHP 5.5's simplified password hashing API.
  4 + *
  5 + * @author Anthony Ferrara <ircmaxell@php.net>
  6 + * @license http://www.opensource.org/licenses/mit-license.html MIT License
  7 + * @copyright 2012 The Authors
  8 + */
  9 +
  10 +if (!defined('PASSWORD_BCRYPT')) {
  11 +
  12 + define('PASSWORD_BCRYPT', 1);
  13 + define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
  14 +
  15 + /**
  16 + * Hash the password using the specified algorithm
  17 + *
  18 + * @param string $password The password to hash
  19 + * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
  20 + * @param array $options The options for the algorithm to use
  21 + *
  22 + * @return string|false The hashed password, or false on error.
  23 + */
  24 + function password_hash($password, $algo, array $options = array()) {
  25 + if (!function_exists('crypt')) {
  26 + trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
  27 + return null;
  28 + }
  29 + if (!is_string($password)) {
  30 + trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
  31 + return null;
  32 + }
  33 + if (!is_int($algo)) {
  34 + trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
  35 + return null;
  36 + }
  37 + switch ($algo) {
  38 + case PASSWORD_BCRYPT:
  39 + // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
  40 + $cost = 10;
  41 + if (isset($options['cost'])) {
  42 + $cost = $options['cost'];
  43 + if ($cost < 4 || $cost > 31) {
  44 + trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
  45 + return null;
  46 + }
  47 + }
  48 + $required_salt_len = 22;
  49 + $hash_format = sprintf("$2y$%02d$", $cost);
  50 + break;
  51 + default:
  52 + trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
  53 + return null;
  54 + }
  55 + if (isset($options['salt'])) {
  56 + switch (gettype($options['salt'])) {
  57 + case 'NULL':
  58 + case 'boolean':
  59 + case 'integer':
  60 + case 'double':
  61 + case 'string':
  62 + $salt = (string) $options['salt'];
  63 + break;
  64 + case 'object':
  65 + if (method_exists($options['salt'], '__tostring')) {
  66 + $salt = (string) $options['salt'];
  67 + break;
  68 + }
  69 + case 'array':
  70 + case 'resource':
  71 + default:
  72 + trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
  73 + return null;
  74 + }
  75 + if (strlen($salt) < $required_salt_len) {
  76 + trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
  77 + return null;
  78 + } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
  79 + $salt = str_replace('+', '.', base64_encode($salt));
  80 + }
  81 + } else {
  82 + $buffer = '';
  83 + $raw_length = (int) ($required_salt_len * 3 / 4 + 1);
  84 + $buffer_valid = false;
  85 + if (function_exists('mcrypt_create_iv')) {
  86 + $buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
  87 + if ($buffer) {
  88 + $buffer_valid = true;
  89 + }
  90 + }
  91 + if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
  92 + $buffer = openssl_random_pseudo_bytes($raw_length);
  93 + if ($buffer) {
  94 + $buffer_valid = true;
  95 + }
  96 + }
  97 + if (!$buffer_valid && file_exists('/dev/urandom')) {
  98 + $f = @fopen('/dev/urandom', 'r');
  99 + if ($f) {
  100 + $read = strlen($buffer);
  101 + while ($read < $raw_length) {
  102 + $buffer .= fread($f, $raw_length - $read);
  103 + $read = strlen($buffer);
  104 + }
  105 + fclose($f);
  106 + if ($read >= $raw_length) {
  107 + $buffer_valid = true;
  108 + }
  109 + }
  110 + }
  111 + if (!$buffer_valid || strlen($buffer) < $raw_length) {
  112 + $bl = strlen($buffer);
  113 + for ($i = 0; $i < $raw_length; $i++) {
  114 + if ($i < $bl) {
  115 + $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
  116 + } else {
  117 + $buffer .= chr(mt_rand(0, 255));
  118 + }
  119 + }
  120 + }
  121 + $salt = str_replace('+', '.', base64_encode($buffer));
  122 +
  123 + }
  124 + $salt = substr($salt, 0, $required_salt_len);
  125 +
  126 + $hash = $hash_format . $salt;
  127 +
  128 + $ret = crypt($password, $hash);
  129 +
  130 + if (!is_string($ret) || strlen($ret) <= 13) {
  131 + return false;
  132 + }
  133 +
  134 + return $ret;
  135 + }
  136 +
  137 + /**
  138 + * Get information about the password hash. Returns an array of the information
  139 + * that was used to generate the password hash.
  140 + *
  141 + * array(
  142 + * 'algo' => 1,
  143 + * 'algoName' => 'bcrypt',
  144 + * 'options' => array(
  145 + * 'cost' => 10,
  146 + * ),
  147 + * )
  148 + *
  149 + * @param string $hash The password hash to extract info from
  150 + *
  151 + * @return array The array of information about the hash.
  152 + */
  153 + function password_get_info($hash) {
  154 + $return = array(
  155 + 'algo' => 0,
  156 + 'algoName' => 'unknown',
  157 + 'options' => array(),
  158 + );
  159 + if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
  160 + $return['algo'] = PASSWORD_BCRYPT;
  161 + $return['algoName'] = 'bcrypt';
  162 + list($cost) = sscanf($hash, "$2y$%d$");
  163 + $return['options']['cost'] = $cost;
  164 + }
  165 + return $return;
  166 + }
  167 +
  168 + /**
  169 + * Determine if the password hash needs to be rehashed according to the options provided
  170 + *
  171 + * If the answer is true, after validating the password using password_verify, rehash it.
  172 + *
  173 + * @param string $hash The hash to test
  174 + * @param int $algo The algorithm used for new password hashes
  175 + * @param array $options The options array passed to password_hash
  176 + *
  177 + * @return boolean True if the password needs to be rehashed.
  178 + */
  179 + function password_needs_rehash($hash, $algo, array $options = array()) {
  180 + $info = password_get_info($hash);
  181 + if ($info['algo'] != $algo) {
  182 + return true;
  183 + }
  184 + switch ($algo) {
  185 + case PASSWORD_BCRYPT:
  186 + $cost = isset($options['cost']) ? $options['cost'] : 10;
  187 + if ($cost != $info['options']['cost']) {
  188 + return true;
  189 + }
  190 + break;
  191 + }
  192 + return false;
  193 + }
  194 +
  195 + /**
  196 + * Verify a password against a hash using a timing attack resistant approach
  197 + *
  198 + * @param string $password The password to verify
  199 + * @param string $hash The hash to verify against
  200 + *
  201 + * @return boolean If the password matches the hash
  202 + */
  203 + function password_verify($password, $hash) {
  204 + if (!function_exists('crypt')) {
  205 + trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
  206 + return false;
  207 + }
  208 + $ret = crypt($password, $hash);
  209 + if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
  210 + return false;
  211 + }
  212 +
  213 + $status = 0;
  214 + for ($i = 0; $i < strlen($ret); $i++) {
  215 + $status |= (ord($ret[$i]) ^ ord($hash[$i]));
  216 + }
  217 +
  218 + return $status === 0;
  219 + }
  220 +}
37 lib/password_compat/readme_moodle.txt
... ... @@ -0,0 +1,37 @@
  1 +Description of password_compat import into Moodle:
  2 +==================================================
  3 +
  4 +Imported from: https://github.com/ircmaxell/password_compat/commit/2a7b6355d27c65f7e0de1fbbc0016b5b6cd8226b
  5 +Copyright: (c) 2012 Anthony Ferrara
  6 +License: MIT License
  7 +
  8 +Removed:
  9 +* README.md, LICENSE.md and composer.json files.
  10 +* bootstrap.php and phpunit.xml.dist files from test directory.
  11 +
  12 +Added:
  13 +* None.
  14 +
  15 +Our changes:
  16 +* Moved tests from test/Unit/ to tests/ directory.
  17 +* Removed tabs and trailing whitespace from test files.
  18 +* Added markTestSkipped() check to tests so they only run if password_compat is supported
  19 +
  20 +Moodle commit history:
  21 +======================
  22 +
  23 +MDL-35332 Initial commit
  24 +
  25 +
  26 +Library description:
  27 +====================
  28 +
  29 +Compatibility with the password_* functions being worked on for PHP 5.5.
  30 +
  31 +This library requires PHP >= 5.3.7 due to a PHP security issue prior to that
  32 +version.
  33 +
  34 +See the RFC (https://wiki.php.net/rfc/password_hash) for more information.
  35 +
  36 +Latest code available from https://github.com/ircmaxell/password_compat/
  37 +under MIT license.
36 lib/password_compat/tests/PasswordGetInfoTest.php
... ... @@ -0,0 +1,36 @@
  1 +<?php
  2 +
  3 +global $CFG;
  4 +require_once($CFG->dirroot . '/lib/password_compat/lib/password.php');
  5 +
  6 +class PasswordGetInfoTest extends PHPUnit_Framework_TestCase {
  7 +
  8 + protected function setUp() {
  9 + if (password_compat_not_supported()) {
  10 + // Skip test if password_compat is not supported.
  11 + $this->markTestSkipped('password_compat not supported');
  12 + }
  13 + }
  14 +
  15 + public static function provideInfo() {
  16 + return array(
  17 + array('foo', array('algo' => 0, 'algoName' => 'unknown', 'options' => array())),
  18 + array('$2y$', array('algo' => 0, 'algoName' => 'unknown', 'options' => array())),
  19 + array('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', array('algo' => PASSWORD_BCRYPT, 'algoName' => 'bcrypt', 'options' => array('cost' => 7))),
  20 + array('$2y$10$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', array('algo' => PASSWORD_BCRYPT, 'algoName' => 'bcrypt', 'options' => array('cost' => 10))),
  21 +
  22 + );
  23 + }
  24 +
  25 + public function testFuncExists() {
  26 + $this->assertTrue(function_exists('password_get_info'));
  27 + }
  28 +
  29 + /**
  30 + * @dataProvider provideInfo
  31 + */
  32 + public function testInfo($hash, $info) {
  33 + $this->assertEquals($info, password_get_info($hash));
  34 + }
  35 +
  36 +}
94 lib/password_compat/tests/PasswordHashTest.php
... ... @@ -0,0 +1,94 @@
  1 +<?php
  2 +
  3 +global $CFG;
  4 +require_once($CFG->dirroot . '/lib/password_compat/lib/password.php');
  5 +
  6 +class PasswordHashTest extends PHPUnit_Framework_TestCase {
  7 +
  8 + protected function setUp() {
  9 + if (password_compat_not_supported()) {
  10 + // Skip test if password_compat is not supported.
  11 + $this->markTestSkipped('password_compat not supported');
  12 + }
  13 + }
  14 +
  15 + public function testFuncExists() {
  16 + $this->assertTrue(function_exists('password_hash'));
  17 + }
  18 +
  19 + public function testStringLength() {
  20 + $this->assertEquals(60, strlen(password_hash('foo', PASSWORD_BCRYPT)));
  21 + }
  22 +
  23 + public function testHash() {
  24 + $hash = password_hash('foo', PASSWORD_BCRYPT);
  25 + $this->assertEquals($hash, crypt('foo', $hash));
  26 + }
  27 +
  28 + public function testKnownSalt() {
  29 + $hash = password_hash("rasmuslerdorf", PASSWORD_BCRYPT, array("cost" => 7, "salt" => "usesomesillystringforsalt"));
  30 + $this->assertEquals('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', $hash);
  31 + }
  32 +
  33 + public function testRawSalt() {
  34 + $hash = password_hash("test", PASSWORD_BCRYPT, array("salt" => "123456789012345678901" . chr(0)));
  35 + $this->assertEquals('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', $hash);
  36 + }
  37 +
  38 + /**
  39 + * @expectedException PHPUnit_Framework_Error
  40 + */
  41 + public function testInvalidAlgo() {
  42 + password_hash('foo', array());
  43 + }
  44 +
  45 + /**
  46 + * @expectedException PHPUnit_Framework_Error
  47 + */
  48 + public function testInvalidAlgo2() {
  49 + password_hash('foo', 2);
  50 + }
  51 +
  52 + /**
  53 + * @expectedException PHPUnit_Framework_Error
  54 + */
  55 + public function testInvalidPassword() {
  56 + password_hash(array(), 1);
  57 + }
  58 +
  59 + /**
  60 + * @expectedException PHPUnit_Framework_Error
  61 + */
  62 + public function testInvalidSalt() {
  63 + password_hash('foo', PASSWORD_BCRYPT, array('salt' => array()));
  64 + }
  65 +
  66 + /**
  67 + * @expectedException PHPUnit_Framework_Error
  68 + */
  69 + public function testInvalidBcryptCostLow() {
  70 + password_hash('foo', PASSWORD_BCRYPT, array('cost' => 3));
  71 + }
  72 +
  73 + /**
  74 + * @expectedException PHPUnit_Framework_Error
  75 + */
  76 + public function testInvalidBcryptCostHigh() {
  77 + password_hash('foo', PASSWORD_BCRYPT, array('cost' => 32));
  78 + }
  79 +
  80 + /**
  81 + * @expectedException PHPUnit_Framework_Error
  82 + */
  83 + public function testInvalidBcryptCostInvalid() {
  84 + password_hash('foo', PASSWORD_BCRYPT, array('cost' => 'foo'));
  85 + }
  86 +
  87 + /**
  88 + * @expectedException PHPUnit_Framework_Error
  89 + */
  90 + public function testInvalidBcryptSaltShort() {
  91 + password_hash('foo', PASSWORD_BCRYPT, array('salt' => 'abc'));
  92 + }
  93 +
  94 +}
36 lib/password_compat/tests/PasswordNeedsRehashTest.php
... ... @@ -0,0 +1,36 @@
  1 +<?php
  2 +
  3 +global $CFG;
  4 +require_once($CFG->dirroot . '/lib/password_compat/lib/password.php');
  5 +
  6 +class PasswordNeedsRehashTest extends PHPUnit_Framework_TestCase {
  7 +
  8 + protected function setUp() {
  9 + if (password_compat_not_supported()) {
  10 + // Skip test if password_compat is not supported.
  11 + $this->markTestSkipped('password_compat not supported');
  12 + }
  13 + }
  14 +
  15 + public static function provideCases() {
  16 + return array(
  17 + array('foo', 0, array(), false),
  18 + array('foo', 1, array(), true),
  19 + array('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', PASSWORD_BCRYPT, array(), true),
  20 + array('$2y$07$usesomesillystringfore2udlvp1ii2e./u9c8sbjqp8i90dh6hi', PASSWORD_BCRYPT, array('cost' => 7), false),
  21 + array('$2y$07$usesomesillystringfore2udlvp1ii2e./u9c8sbjqp8i90dh6hi', PASSWORD_BCRYPT, array('cost' => 5), true),
  22 + );
  23 + }
  24 +
  25 + public function testFuncExists() {
  26 + $this->assertTrue(function_exists('password_needs_rehash'));
  27 + }
  28 +
  29 + /**
  30 + * @dataProvider provideCases
  31 + */
  32 + public function testCases($hash, $algo, $options, $valid) {
  33 + $this->assertEquals($valid, password_needs_rehash($hash, $algo, $options));
  34 + }
  35 +
  36 +}
39 lib/password_compat/tests/PasswordVerifyTest.php
... ... @@ -0,0 +1,39 @@
  1 +<?php
  2 +
  3 +global $CFG;
  4 +require_once($CFG->dirroot . '/lib/password_compat/lib/password.php');
  5 +
  6 +class PasswordVerifyTest extends PHPUnit_Framework_TestCase {
  7 +
  8 + protected function setUp() {
  9 + if (password_compat_not_supported()) {
  10 + // Skip test if password_compat is not supported.
  11 + $this->markTestSkipped('password_compat not supported');
  12 + }
  13 + }
  14 +
  15 + public function testFuncExists() {
  16 + $this->assertTrue(function_exists('password_verify'));
  17 + }
  18 +
  19 + public function testFailedType() {
  20 + $this->assertFalse(password_verify(123, 123));
  21 + }
  22 +
  23 + public function testSaltOnly() {
  24 + $this->assertFalse(password_verify('foo', '$2a$07$usesomesillystringforsalt$'));
  25 + }
  26 +
  27 + public function testInvalidPassword() {
  28 + $this->assertFalse(password_verify('rasmusler', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi'));
  29 + }
  30 +
  31 + public function testValidPassword() {
  32 + $this->assertTrue(password_verify('rasmuslerdorf', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi'));
  33 + }
  34 +
  35 + public function testInValidHash() {
  36 + $this->assertFalse(password_verify('rasmuslerdorf', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hj'));
  37 + }
  38 +
  39 +}
2  lib/phpunit/bootstrap.php
@@ -189,8 +189,6 @@
189 189 ini_set('display_errors', '1');
190 190 ini_set('log_errors', '1');
191 191
192   -$CFG->passwordsaltmain = 'phpunit'; // makes login via normal UI impossible
193   -
194 192 $CFG->noemailever = true; // better not mail anybody from tests, override temporarily if necessary
195 193 $CFG->cachetext = 0; // disable this very nasty setting
196 194
2  lib/setuplib.php
@@ -1140,7 +1140,7 @@ function disable_output_buffering() {
1140 1140 */
1141 1141 function redirect_if_major_upgrade_required() {
1142 1142 global $CFG;
1143   - $lastmajordbchanges = 2012110201;
  1143 + $lastmajordbchanges = 2013020900;
1144 1144 if (empty($CFG->version) or (int)$CFG->version < $lastmajordbchanges or
1145 1145 during_initial_install() or !empty($CFG->adminsetuppending)) {
1146 1146 try {
114 lib/tests/moodlelib_test.php
@@ -2245,4 +2245,118 @@ public function test_get_config() {
2245 2245 set_config('phpunit_test_get_config_4', 'test c', 'mod_forum');
2246 2246 $this->assertFalse($cache->get('mod_forum'));
2247 2247 }
  2248 +
  2249 + /**
  2250 + * Test function password_is_legacy_hash().
  2251 + */
  2252 + public function test_password_is_legacy_hash() {
  2253 + // Well formed md5s should be matched.
  2254 + foreach (array('some', 'strings', 'to_check!') as $string) {
  2255 + $md5 = md5($string);
  2256 + $this->assertTrue(password_is_legacy_hash($md5));
  2257 + }
  2258 + // Strings that are not md5s should not be matched.
  2259 + foreach (array('', AUTH_PASSWORD_NOT_CACHED, 'IPW8WTcsWNgAWcUS1FBVHegzJnw5M2jOmYkmfc8z.xdBOyC4Caeum') as $notmd5) {
  2260 + $this->assertFalse(password_is_legacy_hash($notmd5));
  2261 + }
  2262 + }
  2263 +
  2264 + /**
  2265 + * Test function validate_internal_user_password().
  2266 + */
  2267 + public function test_validate_internal_user_password() {
  2268 + if (password_compat_not_supported()) {
  2269 + // If bcrypt is not properly supported test legacy md5 hashes instead.
  2270 + // Can't hardcode these as we don't know the site's password salt.
  2271 + $validhashes = array(
  2272 + 'pw' => hash_internal_user_password('pw'),
  2273 + 'abc' => hash_internal_user_password('abc'),
  2274 + 'C0mP1eX_&}<?@*&%` |\"' => hash_internal_user_password('C0mP1eX_&}<?@*&%` |\"'),
  2275 + 'ĩńťėŕňăţĩōŋāĹ' => hash_internal_user_password('ĩńťėŕňăţĩōŋāĹ')
  2276 + );
  2277 + } else {
  2278 + // Otherwise test bcrypt hashes.
  2279 + $validhashes = array(
  2280 + 'pw' => '$2y$10$LOSDi5eaQJhutSRun.OVJ.ZSxQZabCMay7TO1KmzMkDMPvU40zGXK',
  2281 + 'abc' => '$2y$10$VWTOhVdsBbWwtdWNDRHSpewjd3aXBQlBQf5rBY/hVhw8hciarFhXa',
  2282 + 'C0mP1eX_&}<?@*&%` |\"' => '$2y$10$3PJf.q.9ywNJlsInPbqc8.IFeSsvXrGvQLKRFBIhVu1h1I3vpIry6',
  2283 + 'ĩńťėŕňăţĩōŋāĹ' => '$2y$10$3A2Y8WpfRAnP3czJiSv6N.6Xp0T8hW3QZz2hUCYhzyWr1kGP1yUve'
  2284 + );
  2285 + }
  2286 +
  2287 + foreach ($validhashes as $password => $hash) {
  2288 + $user = new stdClass();
  2289 + $user->auth = 'manual';
  2290 + $user->password = $hash;
  2291 + // The correct password should be validated.
  2292 + $this->assertTrue(validate_internal_user_password($user, $password));
  2293 + // An incorrect password should not be validated.
  2294 + $this->assertFalse(validate_internal_user_password($user, 'badpw'));
  2295 + }
  2296 + }
  2297 +
  2298 + /**
  2299 + * Test function hash_internal_user_password().
  2300 + */
  2301 + public function test_hash_internal_user_password() {
  2302 + $passwords = array('pw', 'abc123', 'C0mP1eX_&}<?@*&%` |\"', 'ĩńťėŕňăţĩōŋāĹ');
  2303 +
  2304 + // Check that some passwords that we convert to hashes can
  2305 + // be validated.
  2306 + foreach ($passwords as $password) {
  2307 + $hash = hash_internal_user_password($password);
  2308 + $fasthash = hash_internal_user_password($password, true);
  2309 + $user = new stdClass();
  2310 + $user->auth = 'manual';
  2311 + $user->password = $hash;
  2312 + $this->assertTrue(validate_internal_user_password($user, $password));
  2313 +
  2314 + if (password_compat_not_supported()) {
  2315 + // If bcrypt is not properly supported make sure the passwords are in md5 format.
  2316 + $this->assertTrue(password_is_legacy_hash($hash));
  2317 + } else {
  2318 + // Otherwise they should not be in md5 format.
  2319 + $this->assertFalse(password_is_legacy_hash($hash));
  2320 +
  2321 + // Check that cost factor in hash is correctly set.
  2322 + $this->assertRegExp('/\$10\$/', $hash);
  2323 + $this->assertRegExp('/\$04\$/', $fasthash);