7

You have a User table:

UserID (auto-incrementing Integer)
Password hash
LastLogin

All related tables are linked by the UserID.

You also have a Username table:

Username
Salt
IncorrectLoginCount
LockedUntil
etc.

A user creates an account. You take the Username + Password and a unique, random salt and hash it all together with Argon2:

hash = argon2(username + password + salt)

You store the hash and the next generated UserID in the User table and the Username and randomly generated salt in the Username table. There is no way to directly tell which Username corresponds to which UserID.

The user attempts to log in. You take the submitted Username, fetch the record in the Username table (unless the account is locked), grab the salt, take Username + Password and salt and hash it. You then search for the hash in the Password column of the User table. If you don't find it, incorrect login and if you do, you log the user in with the UserID.

Let's say you have 100 users.

You then dump 999,900 bogus records into your Username table with no corresponding record in the User table. They look like Usernames, except they correspond to no user in your database and there is no way to tell which ones are real. Now the attacker has to waste time trying to crack the passwords of non-existent users, which make up 99.99% of the records in the table and will run the full length of the attempt before abandonment because they will fail every check since they have no corresponding record.

I'm trying to create a situation where the attacker has to waste time attempting to crack the password of users that don't actually exist. Also, if the initial attempt to collect the password doesn't succeed, the attacker doesn't know for certain whether it is a dummy record or a user with a strong password.

The Invalid LoginCount and LockedUntil would be cleared once a day.

When a new user account is first created, you search the UserID table, which only has 100 records at the moment, for a matching hash. Let's say you get a hash collision once a decade or even once a year, even one collision as frequently as once a decade is an absurd stretch in my opinion. This is especially the case that you are only generating hashes for the much smaller UserID, not the massive Username table. You simply throw away the hash, generate a new salt and rehash. You then create the User Account.

Would this significantly slow an attacker down if your database and application code was compromised and the attacker knew exactly what you were doing?

If you attempted to crack the hashes in the UserID table itself, you would have to hash each candidate password separately with each Username. Let's say you hashed 30,000 times. Each candidate password would have to be hashed 30,000 times for the first Username, 30,000 times for the second Username, 30,000 times for the third Username, etc. This would have to be done for every candidate password.

1
39

Before getting into the analysis of the process to slow down cracking the hashes, I want to address something far more important first:

If I log in, and my hash happens to match some other user, I will get authenticated to that user. So your whole "look in the Users database to blindly find any match because I don't tie password hashes to users" is a horrifying approach to authentication.

Please don't do this.


Kirchoff's Principle suggests that a system must be secure even if an attacker knows how you do something. So, let's assume the attacker knows that you added fake usernames. Fine, now all the attacker has to do is to look for valid usernames and tie it to UserID before starting to crack hashes.

And to do that, I would look at the logged user activity in the database. I do not know what is logged in your app, but one has to assume that the user's activity will suggest the username associated with it, if it is not stored, specifically at some point in the database. Things like timestamps can make correlation easy.

And since your threat model includes the assumption that the attacker has access to the codebase and the entire database, your approach appears to do nothing but increase your design overhead and database size.

So, your entire approach relies on an attacker never being able to correlate UserId and Username. This is known as "Security by Obscurity" and, while it has its place, it is not a basis for a secure control.


Now let's tie my first point to my second. Let's say that I want to log into UserID 1 because I can see that it's the admin (or an account of interest). I know the password hash. Now I can take all the usernames and their salts to find a hash that might match User 1's hash. It no longer matters which username I use. It might be unlikely to find an exact match like this using Argon2, but this highlights the larger problem with your approach.

2
  • Comments are not for extended discussion; this conversation has been moved to chat. – schroeder Oct 14 '20 at 20:03
  • @Voo anyway, I guess this discussion has run its course and we have exchanged the all relevant viewpoints regarding this answer, I'm out. – Frank Hopkins Oct 14 '20 at 20:04
6

After some thinking, I will suggest that there is no significant security improvement.

Let's put the standard account protection: salting the password with a time-consuming algorithm (bcrypt, and so one). What a attacker can do :

  • Reverse the hash: almost impossible
  • Bruteforce the hash: almost impossible if the password is longer than 6 chars (because of bcrypt)
  • wordlist attack: as difficult as the password is far in the wordlist attack (impossible if it is not present)
  • reuse a cracked password against the target: possible
  • reuse a cracked password against another target: possible if the user reuses his password in multiple places (which is a bad practice).

With your solution, the attacks against the hashes are quite identical. For each password attempt, the attacker tries every salt+username and if the result is equal to one of the passwords stored in the User table, he succeeds.

It is correct to say that the dummy entries will slow down his work, but the same level of difficulty could be achieved by simply increasing the number of rounds of bcrypt or Argon2.

Your method permits to add operations for the attacker without adding ones for the real users (if we increase the number of rounds of bcrypt, the normal login will be slowed down too) which is good. But the price is an overcomplicated database representation. Not sure it worths it.

I think it is not interesting to consider the case where only the Username table is compromised and not the User table. As they are stored in a similar way, we must consider that someone able to view one, can see the other one.

Also consider the case when David is a regular user with password UnBr3Akable. The database stores with

UserID=12, password hash=1a2b3c, salt=67890

Adding dummy entries could lead to a case when hash(username=toto, salt=1234, password=helloworld) = 1a2b3c.
Then an attacker could log into David's account without knowing the real password.

The case is as rare as finding a hash collision and I'm not sure it is a real problem. But as every fake account could lead to login with a real account if a collision occurs, I am not sure that we can consider tham as fake as you think.

3
  • 6
    Furthermore, any user can in theory end up logged-in as any other user if they attempt to login with a (not even malicious) wrong password. Or even their valid password could collide. It's not just a malicious attacker, since there's nothing tying User-Username, a normal user can end up accidentally logged into another user's account - David might be typing his password, mistype as "UnBr#Akable", and that ends up hashing out to "11fasd89"... which could well be John or Jacob's hash, at which point David is John, for the purposes of your system. – Delioth Oct 12 '20 at 14:26
  • I edited the question at the top. Before you create a UserID account, you search for a matching hash. If you did find one, you would throw the hash away, make a new salt, and rehash. Since the UserID table will not get huge, you will likely never or almost never get a hash collision in the first place unless your hashing algorithm is ridiculous and if you did, you would simply rehash before creating the account. – user227162 Oct 12 '20 at 21:21
  • 4
    @user227162 - Checking for collisions when setting a password only protects against impersonation by someone entering their password correctly. If I'm logging in and I enter my password incorrectly, which leads to a hash collision, there's no way to protect against that because you don't know whether it was a typo or if I'm actually logging in as the other user. – Dave Sherohman Oct 13 '20 at 11:24
6

A random list of concerns without actual security threat estimation:

  • GDPR and similar data protection regulation might be an issue in that it might require you to also delete the username entry when a user requests full deletion of their data; how do you identify both entries? are you asking for the username and the password in the deletion form? or for the user id? if the user can know their user id, an attacker likely can as well
  • you open a separate attack vector with the approach, in that suddenly new users can be a threat to existing users. If the right username entry can be generated an attacker can log into an account of another user on the live system without knowing their actual password and without you knowing which account is used for this, unless you track the connection from login to userid being used -> which then is also accessible to an attacker with system access; yes finding the right combination to insert is likely difficult, but in a normal system this isn't a threat at all.
  • bugs (or deliberate code changes) have a greater risk too to run into the issue that one user might accidentally (or on purpose) log into the account of another user, do you have a way to notice this? In a "normal" system it's easy to have a generic test that makes sure the user id in a user session corresponds to the one associated with the provided username during authentication. In your approach this seems not possible.
  • "The fake users would always have 0 InvalidLogin and NULL lockeduntil. The valid users would be cleared daily." Assuming the clearing happens for all entries and does not distinguish (otherwise that code would tell an attacker who is fake), this means the longer an attacker can listen in to your database the larger the likelihood they can identify all active users by checking the invalid login field for a change.
  • are usernames email addresses? how does password reset work? do you send out mails for the fake users? can attackers identify the real users by trying your recovery method for each username?
  • Notice that usernames are normally not considered high value by endusers or software, they can relatively easily be noticed by glancing over someone's shoulder and are not necessarily encrypted in password stores. So getting hold of them to identify a targeted real user might not be that difficult in targeted attacks.
  • Many non-targeted attacks simply use username+password lists and thus avoid all the fake ones that don't appear in the lists they use, this is not a weakness for your approach,just cases where the additional effort does not pay off, though.
  • if this is a project in a bigger company where responsibility changes, having fake users in the database seems something that someone easily would consider some legacy data that needs to be cleared away; to prevent this additional documentation would need to be written; either that identifies all the fake accounts or just says there are some. In the first case an attacker can use this information too. In the latter case nobody can identify real rubbish entries that got added by some bug.
  • while you save time on the encryption methods compared to just making them more complex to achieve the same cost for attackers, you also spend more time with database inserts and selects - depending on your database of choice and you need to spend more harddrive space
  • this seems in general non-straight forward to understand so if you're not the sole maintainer, you might have additional documentation/mentoring cost and/or risk to accidentally introduce bugs when people try "fixing" things that are not meant to be fixed

After all, I think too that the design identifying the user by the generated hash is risky and the approach will surprise many developers - and surprise always means more cost due to documentation/handover/mentoring and potential bugs. Security wise,yes it can help in some scenarios, but you need to cover a lot of other edge cases that partially wouldn't even exist with a "normal" approach. You have undocumented fake data lying around that could anytime be removed by someone cleaning up and that needs to always get taken care of separately (e.g. no table constraints can be used to clean up the username table). Any log entry or other operation, introduced perhaps later for a cool new feature, that accidentally or on purpose provides a way to connect the two tables makes your approach moot. So unless you have a very specific scenario in mind, I'd say the additional overhead and potential risks that need to be evaluated outweigh the benefit.

2
  • 1
    "Having fake users in the database seems something that someone easily would consider some legacy data that needs to be cleared away; to prevent this additional documentation would need to be written." Much more importantly, that documentation would need to actually be read, consistently, by every single person who ever works with the database in the future. In my experience, this is just not a realistic expectation. Any system that will break, permanently, the first time someone fails to read documentation is not going to last long. – plasticinsect Oct 13 '20 at 19:26
  • @plasticinsect yep, I kept it short for "effort to somehow magically make sure this data survives"; to be fair, I've seen a lot of legacy data lying around unnoticed for a long time, too ;) And still, even if it stays, it might waste peoples' time trying to figure out what it's doing there etc. – Frank Hopkins Oct 13 '20 at 19:31
2

You're using username like a salt and pepper mix with the weaknesses both both. A pepper should be secret. The usernames are, at best, obscured. Both the salt and pepper should be random. The usernames are not random. And it has the worst feature of a pepper: if the username changes the password must also change.

If they forget their password there is no way to delete the old hash; the password hash table will only grow creating more opportunities for false positives.


hash = argon2(username + password + salt)

Consider...

  • Username: bob, Password: 12345.
  • Username: bob1, Password: 2345.

Now only the salt keeps these two from having the same hash. If there's a collision you could try again with a new salt. You could avoid this by adding a separator character which is disallowed in the username and password and salt.

hash = argon2(username + separator + password + separator + salt)

I can't say exactly how this is exploitable, but why risk it?


The extra security you're trying to achieve can be done better and simpler by tuning the cost of argon2. Follow their the "Recommended Parameters" in their paper.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.