In this article we talk about secure password handling in depth after I saw a tweet which reignited my specific interest in this subject. Infosec professionals never get bored of talking about passwords and handling them securely, so you will find this article of interest if like me you are curious about the latest consensus.
John Opdenakker (https://twitter.com/j_opdenakker) recently tweeted about "preferred Password Hashing":
So we came into an discussion about how to handle passwords securely and upgrade insecure handling (by the way: If you don't follow John until now - take a look at his tweets, I always have good conversations and intersting input from his side). I would like to explain a few suggestions I made in reply to that tweet.
I usually prefer a defense in depth approach, so let's take a look at the different layers of protection:
User Layer - good UX
If you haven't notice until now, the old password rules by NIST are outdated. Don't force the user to choose a password like:
- 8 Chars minimal
- At least one uppercase Character
- At least one lowercase Character
- One special Character
- 16 Chars maximal
... as they can and will mislead the user to use insecure passwords, like "P@ssword123" or "Winter2018!". Instead let the user choose a password with at least 10/12/14 Chars. It still might be insecure, but to prevent ultra-fast cracking you can search for the chosen Password in a common password list, like the rockyou-List.
You might also consider using Troy Hunts Pwned Password Service. Even if I trust Troy (and I absolutely do!), I would never send password to third parties, no matter if hashed or plain. But Troy is aware of that and let you download the breached and hashed List, so you can do an offline check (or in that case on the local server/your trusted infrastructure).
Since the cracked passwords include weak patterns (aaaaaaa, abcde1234, etc..) , you usually don't need to check them additionally.
Also don't sabotage password managers - in fact, you should make sure, that your site is working with (common) password managers.
In the application design, you can do multiple things, the most important is:
Never make the password (hash) readable.
You just don't need it. Many applications make the Hash internally readable, because they "need" it, to compare it. It's much more secure, to design a database scheme, that never allows the application to read the password hash.
You can use stored procedures or functions to archive this. Basically, you will have an additional table, conaining all the hashes that is only readable by the DBA and the DB itself, but not by the Application DB User.
So even if your applicaiton is vulenrable for SQL Injects, there is no possibility to get the hashes on this way. An attacker would need to get access to the DBA Account, and this is way more work.
So, basically you should have the following SQL/DB-Functions:
(1) Create/Initialize password
- Only allowed if user password is null AND user exists AND user is still in registration phase
This method accepts an User ID and a already salted and hashed password. It will return true or false.
- Only allowed if user password is not null AND user is not locked
This method accepts an User ID and a already salted and hashed password. It will return true or false. It is defined as an procedure in the database and can return true or false.
(3) Reset Password (privileged Function)
This function is considered privileged, as it is usuable by someone else as the user itself to reset the users password.
You don't need more functions, related to logon in a basic setup. So desing the database carefully, make sure that the application DB user is not able to read the table "user_passwords" but is rather allowed to make use of the SQL-functions "set_password()", "logon()" and "reset_password()".
This will protect your users' passwords even if you have an SQL injection vulenrability in your application.
Hashing is probably the most recommended thing about storing passwords and was also the main topic from Johns tweet. Hashing is important, it stores the password in a non-reversible form. For password storing, we should use a cryptographic hash function. While a theoretical desireable attribute of a crpytographic hash function is to be easy to calculate (cost inexpensive) and easy to implement in hardware, we don't want that for our password storing algorithmen. Since most functions are designed to be easy calculated in one way, we need to increase the difficulty.
Some points on how to choose the "best" algorithm:
- Choose an State of the Art algorithm: argon, bcrypt, scrypt, PBKDF
- Make use of indeterminate Iterations - use an arbritary count of iterations per user.
- Check for common crackers like John The Ripper and Hashcat, make sure your combination is not available by default
- User 2 or more (strong) algorithms, as it lower the chance of rendering your protection useless in case one of the algorithm gets broken
- Make the hashing expensive - especially memory expense is crucial as it is harder to scale
- Make the algorithm-combination configurable inside the application/database
Salt & Pepper
Salt and pepper are easy: Salt is a per-User value, that will prevent that 2 users with the same password have the same hash value. The salt is not a secret and can be stored along with the hash.
Pepper is a global secret value, that should be added into the hashing process. The idea behind pepper is, that even if someone obtains your database, he should not be able to crack the passwords.
The pepper value should not be stored along with the passwords and the database. For example, our company uses a one-time startup-derived value. That means, if the applications starts up, an administrators needs to enter a secret phrase. That phrase is used to generate the pepper value and a symmetric key for the "encrypted store". This value and it's derivations are hold only in volatile memory and are never stored on disk or paged.
But what to do, if your hash got cracked or you have a legacy application, that only have a weak hash, like 1 round of SHA? The answer is "Re-encapsulation".
You take the weak hash value and put a strong around it. It doesn't make the strong hash weaker. You might also take that as a chance to let the client do some work. So if the hash right now is "sha()", you simply do "argon(bcrypt(scrypt(sha())))", but since the last interation is already done, you apply "argon(bcrypt(scrypt()))" to all existing hashes.
You should offer alternative and/or additional methods to your users, like webauthn (https://www.w3.org/TR/webauthn/) or U2F.