I was staring blankly at my screen in disbelief. My heart was racing, and my fingers were shaking. My alarm clock was ringing, but it didn’t even make me flinch; I ignored it. The dopamine hit I got was thrilling. I … did it. I hacked this company. This is the moment I realized how important secure password storage is.
Some years ago, I was doing a penetration test. I discovered that the system administrator was performing regular database backups. And that he was storing the backup files inside the web folder. I was thus able to download the backup files via a simple URL.
This was catastrophic for that company — I found user passwords in those backup files, in cleartext. This allowed me to log into the application using any identity I wanted.
I also found an exposed CPanel interface on that same server. CPanel is an administration application used to manage the server’s configuration.
After some trial and error, I found, in the database backup file, a password that also allowed me to log into CPanel as an administrator. The administrator used the same password on both platforms.
I was able to set up FTP access for myself and gain control over the whole server.
It was game over. I could give myself discounts, change the website to write anything I wanted. I had unlimited access.
On another penetration test, I was able to retrieve the whole database by exploiting an SQL injection vulnerability. But this time, even though I had access to users’ passwords, I could not use them.
It was so frustrating. Having all the users’ passwords, yet not being able to do anything with them.
They were stored in a secure manner. I found out afterwards that they were using a pepper. We explain further down in this article what that means.
This article discusses the protections that we can put in place to minimize the consequences of a password breach. We will discuss secure password storage.
These are defense in depth mechanisms that don’t protect you from a breach. But they slow down, or even stop the attacker from using the breached passwords to perform further actions.
Why hash the passwords?
Let’s do a simple thought exercise. Suppose we have an application with a login form. What features of the application deal in any way with a user’s password?
I can think of at least the following cases:
- Sign-up / account creation
- Password reset
- Login
- Change password
Let’s do another thought exercise. For these features, do you actually need to read a user’s password? Take them one by one :
Account creation
When we create a user in the database, we just store the password he chose in the database. We don’t need to do anything with his password besides storing it.
Password reset
Here again, we go to the user’s database, we discard the old password and replace it with the new one. We don’t need to read or do anything with the old password
Login
Here’s the part where you would think you need to access the user’s password. You need to take the password that the user put in the login form, and compare it to the password stored in the database.
But, when you think about it, you don’t need to view the cleartext password of the user. What you need is a way to tell if the password the user provided is the same one as the password stored in the user database.
Change password
This one requires a combination of the two previous scenarios.
To start, the user needs to provide his old password. This ensures that we are dealing with the legitimate user, and not an attacker who took over an existing user session.
The server then needs to check that the password provided is equal to the user’s password stored in the database. Here too, access to the user’s clear text password isn’t needed.
When the application confirms the validity of the password, it replaces the old password with the new password.
There is a way to code these features, without storing the passwords in cleartext form. Let’s see how we can store them instead.
Let’s hash things out
We have a dilemma on our hands. We would like to store users’ passwords in a non-reversible format; If a hacker gains access to the database, he would not be able to retrieve the users’ passwords.
But, at the same time, we need to be able to check if a password submitted by a user is the correct password.
Thankfully, we have a cryptographic solution to do that. We will use hashing functions to transform users’ cleartext passwords before storing them.
Hashing functions take a message as input and produce a message of fixed size. For example, the hashing algorithm SHA-256 produces messages of size 32 byte. The produced message is called a hash.
Hashing functions have 2 main properties:
- Hashing functions are one-way. You can produce a hash when you have the original message. But, you can’t retrieve the original message if you only have the hash.
- Hashing functions are deterministic. If you give it the same message, it will always produce the same hash.
These 2 properties allow us to solve our dilemma:
- If an attacker gains access to the database, he would be able to retrieve users’ password hashes. But he would be unable to retrieve their real cleartext passwords.
- When someone attempts to log in, we hash the password that he sent via the login form. We can then compare the resulting hash to the hash stored in the user’s database record. If the 2 hashes are identical, it means that the passwords are identical. Thus, we can check password validity without needing the user’s cleartext password.
Here’s what it looks like on an application :
- When creating a new account, we don’t store the user’s cleartext password in the database. Instead, we store the hash of his password.
- When the user tries to log in, we check if the password he entered is correct. We start by hashing the password he provided. Then, we retrieve his password hash that we stored in the database. Finally, we compare the two hashes. If they are identical then the login attempt succeeds. If not, the login fails.
In its basic form, this is how the process would look like:


On brute force and calculation speed
When an attacker compromises a password hash, he cannot do much with it. That’s because the application requires the cleartext password at login.
His first goal is thus to retrieve the cleartext password. But, as we said earlier, hashing functions are one way. He cannot easily retrieve the cleartext password from the hash.
He needs to try and crack it using bruteforce. Cracking, in this context, is the act of retrieving the cleartext password of a password hash.
The process of brute forcing a password hash is simple:
- We take a list of common passwords. This list is called wordlist in the cracking process.
- For each password:
- Calculate the hash of the password
- Compare the calculated hash to the password hash we are trying to crack
- If the hashes are identical, we stop. The cleartext password is the one we hashed in this step.
- If not, we continue our search. We take the next password and get back to step “a”
The success of the bruteforce attack depends on the strength of your wordlist. The wordlist has to be comprehensive enough to increase the likelihood that the cleartext password is in it.
The only obstacle in the attacker’s way is time. Since he has to try all the passwords in his wordlist, the process of bruteforce takes time.
An attacker could, theoretically, construct a wordlist containing all possible character combinations. This would guarantee that the user’s password is in there.
But, such a bruteforce attack would take hundreds of years to finish. The attacker thus limits his search to a reasonable wordlist size.
Hackers use specialized hardware like GPUs to speed up bruteforce attacks. The most popular software to perform bruteforce attacks are Hashcat and John The Ripper.
Hit where it hurts
As developers, we can make the time constraint more painful. We can increase the time it takes to calculate the hash.
Since bruteforce attacks are a loop of calculating hashes, this would increase the total time of bruteforce attacks.
We can achieve that by choosing a hashing algorithm that uses heavy calculations.
When designing your application, you have a few options to choose from. The following are our recommendations in order of preference:
- Argon2id: This hashing algorithm is one of the variations of the algorithm Argon2. It consumes memory space and processing power to make bruteforce attacks very difficult. This algorithm takes 3 configuration parameters: the minimum memory size (m), the minimum number of iterations (t), and the degree of parallelism (p). You can choose from one of the following secure configurations:
- m=47104 (46 MiB), t=1, p=1
- m=19456 (19 MiB), t=2, p=1
- m=12288 (12 MiB), t=3, p=1
- m=9216 (9 MiB), t=4, p=1
- m=7168 (7 MiB), t=5, p=1
- Scrypt: If Argon2id isn’t available in your programming language/framework, you can use scrypt instead. Like Argon2id, it is a calculation heavy hashing algorithm that also takes 3 configuration parameters. the minimum CPU/memory cost parameter (N), the blocksize (r) and the degree of parallelism (p). You can choose from one of the following secure configurations:
- N=2^17 (128 MiB), r=8 (1024 bytes), p=1
- N=2^16 (64 MiB), r=8 (1024 bytes), p=2
- N=2^15 (32 MiB), r=8 (1024 bytes), p=3
- N=2^14 (16 MiB), r=8 (1024 bytes), p=5
- N=2^13 (8 MiB), r=8 (1024 bytes), p=10
- BCrypt: If neither Argon2id nor Scrypt are available, then you can use Bcrypt to hash your user passwords. This algorithm expects a parameter to configure the cost of calculating a hash. You should give it as a minimum the value of 10. You can increase this value if your server hardware can handle it.
- PBKDF2: This algorithm is required by some regulatory bodies. If your application needs to respect these regulations, then you have no choice but use this algorithm to hash your users’ passwords. This algorithm needs 2 configuration parameters: the name of the hashing algorithm and the number of iterations. You can use one of the following values:
- PBKDF2-HMAC-SHA256: 600,000 iterations
- PBKDF2-HMAC-SHA512: 210,000 iterations
On rainbow tables and adding salt
Hashing is a one way function. You can calculate the hash starting from a password, but you can’t retrieve the password from the hash.
Thus, attackers needed more creative ways to retrieve the passwords once they breached users’ passwords. They could launch a bruteforce attack every time they got a password hash.
But that would take too much time. They found an alternative way by creating something called rainbow tables.
A rainbow table, in its simplest form, is a database table having 2 columns: “password” and “hash”. In each row of this table, we have a common password in the “password” column, and the hash of that password in the “hash” column.
A good rainbow table contains a big number of common passwords and the corresponding hash.
When a hacker needs to crack a password hash, he can simply do a search in the rainbow table. He looks for a row in the database where the hash is equal to the password hash he is looking to crack.
If he finds a row with this criteria, it means that the password in that table row is the cleartext password he is looking for.
The attacker doesn’t have to do any heavy calculations. Rainbow tables can help attackers find the cleartext passwords quickly.
This is a great tool in the attacker’s arsenal. But there is a way to make it unusable in case your application gets breached.
We will add a salt to users’ passwords before hashing them.
Adding salt
A salt is a random string that we concatenate to the user’s password before hashing it. This ensures that the input to the hash function will not be present in the rainbow table.
Let’s take a concrete example. Imagine a user who chose “P@ssw0rd” as his password on your application. The word “P@ssw0rd” is a very common password, so it is very likely to be present in a rainbow table.
But, if you add the salt “aYST7qn4Mmg2gfuaVP3U” to the password, the input to the hash function becomes “aYST7qn4Mmg2gfuaVP3UP@ssw0rd”.
This string is not going to be present in the rainbow table.
But, you might ask, hackers can just create a new rainbow table. They can take all the common passwords, add the prefix “aYST7qn4Mmg2gfuaVP3U“ to them, and calculate the hash. They would then put the prefixed password and the hash into the rainbow table.
That’s where the 2nd property of password salts come in: each user has his own unique password salt.
This way, if an attacker builds a rainbow table with the salt “aYST7qn4Mmg2gfuaVP3U“, it can only be used to crack the password of the user having the salt “aYST7qn4Mmg2gfuaVP3U“.
Another user will have another salt, so this first rainbow table will be useless. The attacker would need to build a second rainbow table.
But building a rainbow table is more costly than performing a bruteforce attack. So rainbow tables here are useless.
As a bonus, salting the password makes bruteforce attacks more costly.
As the salt is unique for every user, an attacker has to crack hashes one at a time using the respective salt. In the absence of salt, the attacker calculates a password hash once, and compares it against every breached hash.
Salt storage
The salt, ie. the random string that we add to users’ passwords, can be stored in cleartext in the database. You can go one of 2 ways:
- Create a column called “salt” in the user table, and add the salt of any particular user to this column.
- Or, you can store the salt with the user’s password hash. You can use a separator to separate the salt from the password’s hash.
Adding pepper for some mystery
If an attacker breaches the users’ database table, he gains access to both the password hash and the salt. He thus has all the elements he needs to launch a bruteforce attack.
Although, thanks to our previous protections, we make it very difficult for the attacker to crack these passwords.
But, there is one last protection we can put in place. We can add an element to the passwords that the attacker cannot find in the database.
Besides the salt, we can prefix the passwords with another random string that we call pepper. We will not store this string in the database. But, it will be accessible to the application whenever it needs to manipulate passwords.
For example, we can set this pepper in an environment variable, a secrets vault, or in the application’s configuration file.
In case of a database compromise, the attacker will not have access to the pepper. Thus, he cannot launch a bruteforce attack.
He would need to compromise the server in another way to access the pepper. This increases the difficulty for the attacker.
Unlike the salt, we will use the same value to prefix all users’ passwords. We just need to make sure to generate it randomly.
Secure password storage in practice
Enough with the theory. Let us get our hands dirty and see how we can implement secure password storage in real code.
The easiest way to calculate password hashes using the Argon2ID Algorithm is to use the Spring Security Crypto library. add the following to your pom.xml file :
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> <version>5.3.2.RELEASE</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.65</version> </dependency>
Calculating the hash of a new password becomes very easy. And so does checking if a password submitted by the user is the right password:
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; @Component public class PasswordManager { @Value("${pepper}") private String pepper; public String hashPassword(String password) { Argon2PasswordEncoder arg2SpringSecurity = new Argon2PasswordEncoder(16, 32, 1, 19456, 2); /* saltLength = 16 bytes, 128-bit hashLength = 32 bytes, 256-bit parallelism = 1 memory = 19456 iterations = 2 */ return arg2SpringSecurity.encode(this.pepper + password); } public boolean checkPassword(String password, String stored_hash) { Argon2PasswordEncoder arg2SpringSecurity = new Argon2PasswordEncoder(16, 32, 1, 19456, 2); return arg2SpringSecurity.matches(this.pepper + password, stored_hash) } }
As a bonus, the Argon2PasswordEncoder class will automatically generate a random salt for us each time we call the function `encode`.
We retrieve the Pepper value from the application’s configuration file.
Upgrading an existing application
You might have an existing application with existing users. You can update it to make use of the recommendations in this article, without resetting all your users’ passwords.
Start off by hashing your existing users’ hashes. To do that, for each user, you take his old password hash, and pass it to the new hash calculation function.
You then take the output and put it in the user’s record in the database, replacing the old password hash.
Afterwards, you can do a passive password hash update. Every time a user logs in, you can check the user’s hash format in the database.
If his password is still stored in the old format, you should update his password using the new hash format.
Since you have his cleartext password at login (when he submits the login form), you can take it to calculate his new hash and put it in his database record.
Stick it to the bad guys
If you implement everything in this article, your passwords will be safe.
I don’t wish your database ever gets hacked. But if it does, you can get some comfort in the thought of the hackers crushing their heads to the wall trying to find the cleartext passwords.
You can tell them with a grin “let’s see if you can make use of these hashes”.
But, make sure your users don’t see you grin, they would be still angry about the hack.
Leave a Reply