You have a
UserID (auto-incrementing Integer) Password hash LastLogin
All related tables are linked by the UserID.
You also have a
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.