Maybe we thought about this differently. Maybe we were less secure than we realized.
Now, if we lost the source code to the site, we were hosed, they would know the secret sauce.
We basically did it this way:
Given a UserName & Password, we returned a user record ONLY using UserName.
We took Password, and generated the following String:
Salt_Pre + UserName + customHash(userID, Password) + Password + UserID*X + Salt_Post
Sizes: 10 + 3-50 + 8 + 6-unl + 6 + 10 -> 40 - 50 character String, to be MD5'd by the DB.
Now, Salt_Pre and Salt_Post were UNIQUE sitewide salts.
and customHash() was a hash worked out from a Sedgewick book, and the UserID was MODed to give us a really User specific hash value.
the thought process being that if you had the DB we could not envision you getting to the password, without knowing this.
And the login code, of course added: where userId = :UserID and Passwordhash = MD5(:Password);
so what was stored in the DB was the MD5 of that final string.
Obviously you cannot alter the Username, but it is a Key anyways. And it is case sensitive.
But our thought process was to add variables that we would KNOW in the formula, but the hackers would not think of.
If I had to do it today, I would probably have a GUID table where I lookup using UserID variants to get a set of GUIDS
to add to each successive hash (for each GUID G: H = Hash(Pwd)+Hash(H)+Hash(G); ) of course with salt to start and end the process... the goal, for me is to have ENORMOUSLY long strings that get hashed into an MD5() [or much better for the final step]
IE, don't make the user provide the obnoxious length, let the system do it. And it's OKAY if it is COSTLY on the CPU, in fact, that is even better!
PS: Canary accounts are great, but when users can create their own account with their own password. If they do that before they steal the DB, and then see their account. They can HACK on that one account until they find the path, but our extra stuff will seem like Salt. That is why we felt things should change, and be long per customer.