2

we're a payment gateway, similar to PayPal, each user requires an account and recently we've been having resourcing issues from the create account process.

We have narrowed down that it's our duplicate password security function calling the database that's grinding the system to a halt. We have provided the code for the function below. There are >500,000 rows in the user_accounts table. Is there anything we can do to optimise this function?

P.S. We understand that SHA1 is a deprecated way of storing passwords, we are considering upgrades.

function duplicate_passwd_check($users_password) {
  $pass_array = $sql->fetch("SELECT * FROM user_accounts");

  foreach ($pass_array as $db_key => $db_value) {
    // perform hashing
    $salt = md5($users_password . SITE_PEPPER);
    $hashed_users_password = sha1($users_password . $salt);
    // perform check
    if ($db_value['hashed_password'] == $hashed_users_password) {
      $error->add("Password already in use, please use a different one.");
      locate("register"); // redirect to register page
    }
  }
  return 0;
}
Shadow
  • 33,525
  • 10
  • 51
  • 64
True Moola
  • 29
  • 2
  • 3
    Why are you checking duplicates against all records? Surely adding a required complexity level and checking only the actual user is a better option? – ChrisBint Jan 09 '17 at 08:55
  • @ChrisBint Thanks, but I don't understand your comment? How would we ensure people aren't using the same password without comparing their password with the ones already in the database? – True Moola Jan 09 '17 at 09:12
  • 4
    Why on earth is it an issue that more than one person uses the same password, as long as their userid is different, ___Who cares___ – RiggsFolly Jan 09 '17 at 09:12
  • 3
    You do realise that millions of people have bank cards and the pin on a bank card is 4 digits so therefore there must be hundereds of people that have the same pin as you. _Its really does not matter as long as the pin used for this card is correct_ – RiggsFolly Jan 09 '17 at 09:14
  • The best optimisation for this would be to amend the function to `function duplicate_passwd_check($users_password) {return 0;}` As it is completely unnecessary and a waste of processor – RiggsFolly Jan 09 '17 at 09:24
  • @RiggsFolly Thanks for your response, I cannot go into the intricacies of the system; however, this security decision was carefully considered from security experts and deemed appropriate to keep our users safe. We ran in-house tests and were able to deduce the plain-text passwords of many of our users before we implemented this security feature. – True Moola Jan 09 '17 at 09:34
  • _and were able to deduce the plain-text passwords of many of our users_ Well with MD5 that is not surprising. **And making all passwords unique does not actually change that situation** Maybe you should check the credentials of those ___Security Experts___ rather than the uniqueness of each users password – RiggsFolly Jan 09 '17 at 09:36
  • @TrueMoola, you didn't understand Riggs. You are actually checking the password for all accounts. For more accounts you have, if the client need to change the password, it will be harder and harder to avoid the existing password. Which is not necessary. For what facebook did is, they will have a table to store old password that the same client has already used. This table could prevent them to use it again. Is it what you trying to archive for? – AkiEru Jan 09 '17 at 09:38
  • I know this is not you are asking for, but the logic of this code is unnecessary. That's why Riggs gave you a `return 0;`. – AkiEru Jan 09 '17 at 09:40
  • @AkiEru Yes. I cannot imagine how big a pain it must be to create an account on this site now that they have 500,000 unique passwords and I have to think of one that is unique. ___It could take days___ – RiggsFolly Jan 09 '17 at 09:40
  • @RiggsFolly Actually to *"maintain a record of previously used passwords and prevent re-use"* is an **implementation guideline of ISO/IEC 27002**. The difference is that you need to maintain a list of used passwords **per user**. Duplicate passwords amongst users is no problem indeed and should be allowed / not checked. – Code4R7 Mar 20 '18 at 12:55

3 Answers3

0

First, detecting duplicate passwords is almost certainly a mistake. I advise against it.

To elaborate: Duplicate password detection creates an additional side-channel. Here's how to exploit it.

  1. Attempt to register with a username and password.
  2. If the username is already taken, build a list of valid usernames. (Many sites make this public, though, so you may not need it.)
  3. If the password is already taken, build a list of valid passwords.
  4. You can now map usernames to passwords much faster than a traditional brute force.

Consequently, the only winning move is not to play.

Second, use password_hash() and password_verify() and make sure you follow a non-opportunistic strategy for migrating your legacy hashes.

If you still want to detect duplicate passwords, don't use a fast hash function. If an attacker gets your database, they'll attack that instead of the proper password hash.

Instead, consider something like this:

class FooBarAuth
{
    const DUPE_SALT = '$2a$13$s1OlQvuC1wmgFMEYsGNzA.';

    public function doSignup(string $username, string $password)
    {
        $passwordHash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
        $dupeHash = crypt($password, static::DUPE_SALT);

        // (Using paragonie/easydb in this example)
        if ($this->db->exists('SELECT count(*) FROM users WHERE dupe_hash = ?', $dupeHash)) {
            throw new Exception('Duplicate password detected');
        }
        // Proceed with the rest of your onboarding logic here
    }
}

Some key points:

  1. Your passwords are hashed using bcrypt, as per the password_hash() default. Each password hash has a unique random salt and an appropriate cost factor (e.g. 12).
  2. Your "duplicate password check" uses bcrypt with a static salt and a higher cost factor (e.g. 12+1 = 13).

Since the duplicate-detecting checksum is provided by bcrypt, attackers don't gain an advantage here like they would if you used e.g. MD5. However, since the salt isn't unique-per-user (otherwise it wouldn't function as intended for detecting duplicates), we increase the cost factor.

Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
  • If you are using this to check for duplicated accounts (a common trick) consider only doing the extra same salt hash for accounts you already suspect (eg through ip sharing) to cut down the attack surface. – user999305 Jan 09 '17 at 15:40
-1

Problem :

Why you are checking duplicate record against all records ? Try to use where query.

Optimized code :

function duplicate_passwd_check($users_password) {
  $salt = md5($users_password . SITE_PEPPER);
  $hashed_users_password = sha1($users_password . $salt);
  $pass_array = $sql->fetch("SELECT COUNT(*) as TotalNumberUsed FROM user_accounts WHERE hashed_password = '".$hashed_users_password."'");
  if((int)$pass_array["TotalNumberUsed"] > 0){
    $error->add("Password already in use, please use a different one.");
    locate("register"); // redirect to register page
    return 1;
  }
  return 0;
}
aprogrammer
  • 1,764
  • 1
  • 10
  • 20
  • Seriously down votes ? Is there any faster way to get this data ? Please can some body explain ? – aprogrammer Jan 09 '17 at 09:29
  • 1. OP is obviously using a DB access library so converting to PDO a) is impracticle b) he may be using PDO anyway! 2) OP get a new row each time, therefore moving re-hash outside the loop is nonsense! – RiggsFolly Jan 09 '17 at 09:34
  • Well but his hash nothing to do with new line's data sir. Look at that : `function duplicate_passwd_check($users_password) {` he is using `$users_password` argument but no other variable from new line. – aprogrammer Jan 09 '17 at 09:37
  • Actually if you remove options 1 & 2 your third suggestion does make sense but change it to use his DB access code – RiggsFolly Jan 09 '17 at 09:47
-1

It is hard to imagine that all users must have different password instead of not using their old password. But if you wish to do that anyway, this should be the best way to archieve:

function duplicate_passwd_check($users_password) {
    $salt = md5($users_password . SITE_PEPPER);
    $hashed_users_password = sha1($users_password . $salt);
    $duplicated_row = $sql->result("SELECT COUNT(user_id) FROM user_accounts WHERE hashed_password = '" . mysql_real_escape_string($hashed_users_password . "'");

    // perform check
    if ($duplicated_row >= 1) {
          $error->add("Password already in use, please use a different one.");
          locate("register"); // redirect to register page
    }
}

Notes

  1. I don't know which SQL class you are using, I assume you are using a MySQL class. To prevent SQL injection I put mysql_real_escape_string on it.
  2. I don't know if you have $sql->result but this function should output the result instead of sorting it as an array.
  3. user_id should be your primary key to store the user information. You should change it if my naming-guess is wrong.
  4. This check will also count the current client that is going to change the password. Please do not use it to check if the old password is corrent or not.

How this code works

You do not need to compare it one by one. It waste both MySQL and PHP process time. You should leave it to MySQL. My code will get how many rows. Normally the if should either return 0 or 1. To make it more strict I used equal or greater.

AkiEru
  • 780
  • 1
  • 9
  • 34