Commit 68915af4 authored by Patrick Schmalstig's avatar Patrick Schmalstig
Browse files

Implemented MANTIS-6072 (If the password ratchet is changed, update member...

Implemented MANTIS-6072 (If the password ratchet is changed, update member passwords when they next log in)
parent 5ae81920
Loading
Loading
Loading
Loading
+13 −2
Original line number Diff line number Diff line
@@ -250,6 +250,15 @@ function cns_authorise_login(object $this_ref, ?string $username, ?int $member_i
                        $out['error'] = do_lang_tempcode((get_option('login_error_secrecy') == '1') ? 'MEMBER_INVALID_LOGIN' : 'MEMBER_BAD_PASSWORD');
                        return $out;
                    }

                    // Since we verified the password is correct, check if the stored hash is of a different cost than configured. If so, we must re-hash.
                    $this_ratchet = get_ratchet_cost($row['m_pass_hash_salted']);
                    if ($this_ratchet !== null) {
                        $current_ratchet = get_option('crypt_ratchet');
                        if ($current_ratchet != $this_ratchet) {
                            $needs_rehash = true;
                        }
                    }
                    break;

                case '': // LEGACY: v10 style bcrypt passwords (insecure as they use md5)
@@ -310,15 +319,17 @@ function cns_authorise_login(object $this_ref, ?string $username, ?int $member_i
            }
        }

        // LEGACY: Transform old style v10 passwords into v11 passwords (v10 ones were not hashed securely)
        // v10 passwords need re-hashed for v11. Also, v11 passwords hashed with a different ratchet than configured need re-hashed.
        if ($needs_rehash) {
            require_code('crypt');

            // For extra security, let's also change the salt
            $password_salt = get_secure_random_string(32, CRYPT_BASE64);
            $password_salted = ratchet_hash($password_mixed, $password_salt);

            $GLOBALS['FORUM_DB']->query_update('f_members', [
                'm_pass_hash_salted' => $password_salted,
                'm_password_compat_scheme' => ($password_compat_scheme == '') ? 'bcrypt' : ('bcrypt_' . $password_compat_scheme),
                'm_password_compat_scheme' => (($password_compat_scheme == '') || ($password_compat_scheme == 'bcrypt')) ? 'bcrypt' : ('bcrypt_' . $password_compat_scheme),
                'm_pass_salt' => $password_salt,
            ], ['id' => $member_id]);

+16 −1
Original line number Diff line number Diff line
@@ -46,7 +46,7 @@ function init__crypt()
function ratchet_hash(string $password, string $salt) : string
{
    // NB: We don't pass the salt separately, we let password_hash generate its own internal salt also (that builds into the hash). So it is double salted.
    $ratchet = max(10, intval(get_option('crypt_ratchet')));
    $ratchet = intval(get_option('crypt_ratchet'));
    return password_hash($salt . $password, PASSWORD_BCRYPT, ['cost' => $ratchet]);
}

@@ -118,6 +118,21 @@ function calculate_reasonable_ratchet(float $target_time = 0.1, int $minimum_cos
    return ($cost - 1); // We don't want to use the cost that exceeded our target time; use the one below it.
}

/**
 * Given a bcrypt hash, get its cost.
 *
 * @param  SHORT_TEXT $hash The bcrypt hash
 * @return ?integer The cost (null: could not find / invalid bcrypt hash)
 */
function get_ratchet_cost(string $hash) : ?int
{
    if (preg_match('/^\$2[ayb]?\$(\d{2})\$/', $hash, $matches)) {
        return intval($matches[1]);
    }

    return null;
}


/**
 * Gets or cryptographically generates the site-wide salt.