How to handle passwords
Over the years, I've interviewed many candidates for a backend developer position. One of the assessments I have included is to show how they would handle passwords. To my surprise, many candidates failed to show a good understanding of how to securely handle passwords. In this article, I'll explain how passwords should be handled (and why) and also briefly cover the fundamental concepts.
TL;DR: Click here to jump to the TLDR version.
Never store passwords in plain text
This is the most basic rule of handling passwords – never store passwords in plain text. This means you should never store passwords as they are.
Reasons:
- If your database is compromised, the attacker will have access to all the passwords.
- Users trust you with their passwords. If you store them in plain text, you are breaking that trust.
- Depending on your country, storing passwords in plain text might be illegal.
Never store passwords in reversible encryption
Some developers think that storing passwords in reversible encryption is secure. Just because "encryption" often screams "secure!" does not mean is true. If you can decrypt the password, so can the attacker.
Reasons:
- If your database is compromised, the attacker will still have access to all the passwords. If they can get to your database, they can also get to your decryption key.
- Users trust you with their passwords. If you store them encrypted and you could still decrypt them, you are still breaking that trust.
Store password in hashed form
The correct way to store passwords is to hash them. Hashing is a one-way function, meaning you can't reverse the process. This way, even you can't know the password – however, to some extent.
Hashing passwords – the "rolling your own" implementation way
Let's say a user signs up with the password password123, and you hash it with SHA-256. The hashed password will look like this:
ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
The above hash is non-reversible, meaning unless you know the password, you can't get the original password from the hash. So problem solved, right? Well... not quite.
Imagine another user came along, and they also signed up with password123, and so you hash it with SHA-256, the hashed password will look like this:
ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
Noticed the above hashes are the same? This is because hashing is deterministic – if you provide the same input, you will get the same output.
In fact, there are databases of precomputed hashes for common passwords, such as CrackStation. If you use a general-purpose hashing algorithm like SHA-256 above, an attacker can easily look up the hash in these databases and find the password.
So, what can we do to mitigate this issue?
Use a salt
One of the ways to mitigate the above issue is to use a salt. A salt is just a unique random string for each user that is appended to the password before hashing. This way, hashes will be less likely to be found in the precomputed hash databases and if you have two or more users having the same password, their hashed passwords will be different.
Let's go back, and hash the password password123 with a salt user_a using SHA-256:
9c52b9479ea42ab5f7fd07abeb851009345d08bcbdc9b96584d9b0fd5f25f56a
Now for the other user, let's use a different salt user_b
082c9a0051e327bfe667c10de10afeaba370be2f3c924acef84c771c447b37d4
Notice hashes are now different, despite each passwords are the same.
You might be thinking, problem solved, right? Not quite.
There is still an issue with using a salt. How are you going to store them? If your database is compromised, the attacker will have access to the salt and try to brute-force the password.
So is there something better than salt alone?
Use a pepper
A pepper is similar to a salt, but it is a secret key you use to append to the password before hashing.
The idea is that this secret key is not stored in the database but exists in memory or is stored in a separate file/environment variable. This way, even if the database is compromised, the attacker will still need the pepper to crack the passwords.
So is the problem solved now? Not quite.
Even if you use a pepper, it doesn't stop a determined attacker from brute-forcing the password.
This is because general-purpose hashing algorithm is designed to be fast. For example, an Apple M1 Ultra is able to generate up to 1,785,000,000 SHA-256 hashes per second (source). The hacker could theoretically generate millions of possible hashes, salts and peppers, and compares them to the hash in the database in a relatively short amount of time.
So even if you use everything above, it is still a possibility for compromised passwords. This is why it is not a good idea to use a general-purpose hashing algorithm like SHA-256 or use your "own solution" to hash passwords.
Use slow hashing algorithms – the right way
One bulletproof way to make it harder for an attacker to brute force passwords is by using a slow hashing algorithm. It is designed to take a long time to compute the hash. This way, even if an attacker has the hash, it will take a long time to crack it.
At the time of writing, OWASP recommends using algorithms like Argon2id, bcrypt, or scrypt. These algorithms are designed to be slow and are resistant to brute force attacks. They are also specially designed for hashing passwords; hence you also get all the good stuff like salting.
Let's go back, and this time we hash the password password123 with Argon2id. Note that this time we are not providing any salt value.
$argon2id$v=19$m=16,t=2,p=1$dXNlcnVzZXJfYQ$Avl3luRY7YYUMWgjzZcTeA
Now we do the same for the other user:
$argon2id$v=19$m=16,t=2,p=1$dXNlcnVzZXJfYg$ZY5DkFT7b3aC7k5IeJ3zOQ
Notice the two hashes are different, similarly to salting. This is because Argon2id (most libraries) automatically generates a random salt for you.
To verify the password, you can also use the same library to verify the password. No salt input required!
argon2.verify(hash, password); // returns true or false
Looks simple and elegant, right? That's because they are and is also why you should never "roll your own" hashing implementation.
TLDR version
NEVER:
- Store passwords in plain text.
- Store passwords encrypted.
- Use general-purpose hashing algorithms like
SHA-256. - "Roll your own" hashing implementation.
ALWAYS:
- Use well-tested, special-purpose hashing algorithms like
Argon2,bcrypt, orscrypt.
13 Nov 2023 • cybersecurity